mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-01 08:54:52 +08:00
feat(composer): blocks multiple sending & lots of tests
Summary: fix in composer sending Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Differential Revision: https://review.inboxapp.com/D1286
This commit is contained in:
parent
818bca892c
commit
6a03c6a034
10 changed files with 395 additions and 56 deletions
|
@ -72,7 +72,7 @@ AccountSidebarStore = Reflux.createStore
|
|||
# Remove this when JOIN query speed is fixed!
|
||||
_populateUnreadCountsDebounced: _.debounce ->
|
||||
@_populateUnreadCounts()
|
||||
, 750
|
||||
, 2000
|
||||
|
||||
_refetchFromAPI: ->
|
||||
namespace = NamespaceStore.current()
|
||||
|
|
|
@ -31,6 +31,7 @@ ComposerView = React.createClass
|
|||
bcc: []
|
||||
body: ""
|
||||
subject: ""
|
||||
isSending: DraftStore.sendingState(@props.localId)
|
||||
state
|
||||
|
||||
getComponentRegistryState: ->
|
||||
|
@ -42,6 +43,7 @@ ComposerView = React.createClass
|
|||
# @_checkForKnownFrames()
|
||||
|
||||
componentDidMount: ->
|
||||
@_draftStoreUnlisten = DraftStore.listen @_onSendingStateChanged
|
||||
@keymap_unsubscriber = atom.commands.add '.composer-outer-wrap', {
|
||||
'composer:show-and-focus-bcc': @_showAndFocusBcc
|
||||
'composer:show-and-focus-cc': @_showAndFocusCc
|
||||
|
@ -59,6 +61,7 @@ ComposerView = React.createClass
|
|||
|
||||
componentWillUnmount: ->
|
||||
@_teardownForDraft()
|
||||
@_draftStoreUnlisten() if @_draftStoreUnlisten
|
||||
@keymap_unsubscriber.dispose()
|
||||
|
||||
componentWillUpdate: ->
|
||||
|
@ -114,6 +117,10 @@ ComposerView = React.createClass
|
|||
_renderComposer: ->
|
||||
<div className="composer-inner-wrap" onDragOver={@_onDragNoop} onDragLeave={@_onDragNoop} onDragEnd={@_onDragNoop} onDrop={@_onDrop}>
|
||||
|
||||
<div className="composer-cover"
|
||||
style={display: (if @state.isSending then "block" else "none")}>
|
||||
</div>
|
||||
|
||||
<div className="composer-action-bar-wrap">
|
||||
<div className="composer-action-bar-content">
|
||||
<button className="btn btn-toolbar pull-right btn-trash"
|
||||
|
@ -127,6 +134,7 @@ ComposerView = React.createClass
|
|||
onClick={@_popoutComposer}><RetinaImg name="toolbar-popout.png"/></button>
|
||||
|
||||
<button className="btn btn-toolbar btn-send"
|
||||
ref="sendButton"
|
||||
onClick={@_sendDraft}><RetinaImg name="toolbar-send.png" /></button>
|
||||
{@_footerComponents()}
|
||||
</div>
|
||||
|
@ -277,6 +285,7 @@ ComposerView = React.createClass
|
|||
Actions.composePopoutDraft @props.localId
|
||||
|
||||
_sendDraft: (options = {}) ->
|
||||
return if @state.isSending
|
||||
draft = @_proxy.draft()
|
||||
remote = require('remote')
|
||||
dialog = remote.require('dialog')
|
||||
|
@ -293,7 +302,7 @@ ComposerView = React.createClass
|
|||
warnings = []
|
||||
if draft.subject.length is 0
|
||||
warnings.push('without a subject line')
|
||||
if draft.body.toLowerCase().indexOf('attachment') != -1 and draft.files?.length is 0
|
||||
if (draft.files ? []).length is 0 and draft.body.toLowerCase().indexOf('attach') >= 0
|
||||
warnings.push('without an attachment')
|
||||
|
||||
if warnings.length > 0 and not options.force
|
||||
|
@ -334,6 +343,9 @@ ComposerView = React.createClass
|
|||
@_precalcComposerCss =
|
||||
minHeight: mheight - INLINE_COMPOSER_OTHER_HEIGHT
|
||||
|
||||
_onSendingStateChanged: ->
|
||||
@setState isSending: DraftStore.sendingState(@props.localId)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -4,9 +4,11 @@ proxyquire = require "proxyquire"
|
|||
React = require "react/addons"
|
||||
ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
{Contact,
|
||||
{Actions,
|
||||
Contact,
|
||||
Message,
|
||||
Namespace,
|
||||
DraftStore,
|
||||
DatabaseStore,
|
||||
InboxTestUtils,
|
||||
NamespaceStore} = require "inbox-exports"
|
||||
|
@ -28,9 +30,9 @@ textFieldStub = (className) ->
|
|||
render: -> <div className={className}>{@props.children}</div>
|
||||
focus: ->
|
||||
|
||||
draftStoreProxyStub = (localId) ->
|
||||
draftStoreProxyStub = (localId, returnedDraft) ->
|
||||
listen: -> ->
|
||||
draft: -> new Message()
|
||||
draft: -> (returnedDraft ? new Message(draft: true))
|
||||
changes:
|
||||
add: ->
|
||||
commit: ->
|
||||
|
@ -41,8 +43,6 @@ searchContactStub = (email) ->
|
|||
|
||||
ComposerView = proxyquire "../lib/composer-view.cjsx",
|
||||
"./file-uploads.cjsx": reactStub("file-uploads")
|
||||
"./draft-store-proxy": draftStoreProxyStub
|
||||
"./composer-participants.cjsx": reactStub("composer-participants")
|
||||
"./participants-text-field.cjsx": textFieldStub("")
|
||||
"inbox-exports":
|
||||
ContactStore:
|
||||
|
@ -51,6 +51,7 @@ ComposerView = proxyquire "../lib/composer-view.cjsx",
|
|||
listen: -> ->
|
||||
findViewByName: (component) -> reactStub(component)
|
||||
findAllViewsByRole: (role) -> [reactStub('a'),reactStub('b')]
|
||||
DraftStore: DraftStore
|
||||
|
||||
beforeEach ->
|
||||
# The NamespaceStore isn't set yet in the new window, populate it first.
|
||||
|
@ -78,71 +79,250 @@ describe "A blank composer view", ->
|
|||
expect(ReactTestUtils.isCompositeComponentWithType @composer, ComposerView).toBe true
|
||||
|
||||
describe "testing keyboard inputs", ->
|
||||
beforeEach ->
|
||||
spyOn(@composer, "_sendDraft")
|
||||
InboxTestUtils.loadKeymap "internal_packages/composer/keymaps/composer.cson"
|
||||
|
||||
it "sends the draft on cmd-enter", ->
|
||||
InboxTestUtils.keyPress("cmd-enter", @composer.getDOMNode())
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
it "does not send the draft on enter if the button isn't in focus", ->
|
||||
InboxTestUtils.keyPress("enter", @composer.getDOMNode())
|
||||
expect(@composer._sendDraft).not.toHaveBeenCalled()
|
||||
|
||||
it "sends the draft on enter when the button is in focus", ->
|
||||
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
|
||||
InboxTestUtils.keyPress("enter", sendBtn.getDOMNode())
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
it "shows and focuses on bcc field", ->
|
||||
|
||||
it "shows and focuses on cc field", ->
|
||||
|
||||
it "shows and focuses on bcc field when already open", ->
|
||||
|
||||
describe "should show subject", ->
|
||||
describe "populated composer", ->
|
||||
# This will setup the mocks necessary to make the composer element (once
|
||||
# mounted) think it's attached to the given draft. This mocks out the
|
||||
# proxy system used by the composer.
|
||||
DRAFT_LOCAL_ID = "local-123"
|
||||
useDraft = (draftAttributes={}) ->
|
||||
@draft = new Message _.extend({draft: true}, draftAttributes)
|
||||
spyOn(DraftStore, "sessionForLocalId").andCallFake (localId) =>
|
||||
return draftStoreProxyStub(localId, @draft)
|
||||
|
||||
useFullDraft = ->
|
||||
useDraft.call @,
|
||||
from: [u1]
|
||||
to: [u2]
|
||||
cc: [u3, u4]
|
||||
bcc: [u5]
|
||||
subject: "Test Message 1"
|
||||
body: "Hello <b>World</b><br/> This is a test"
|
||||
|
||||
makeComposer = ->
|
||||
@composer = ReactTestUtils.renderIntoDocument(
|
||||
<ComposerView localId={DRAFT_LOCAL_ID} />
|
||||
)
|
||||
|
||||
describe "When displaying info from a draft", ->
|
||||
beforeEach ->
|
||||
useFullDraft.apply(@)
|
||||
makeComposer.call(@)
|
||||
|
||||
it "attaches the draft to the proxy", ->
|
||||
expect(@draft).toBeDefined()
|
||||
expect(@composer._proxy.draft()).toBe @draft
|
||||
|
||||
it "set the state based on the draft", ->
|
||||
expect(@composer.state.from).toBeUndefined()
|
||||
expect(@composer.state.to).toEqual [u2]
|
||||
expect(@composer.state.cc).toEqual [u3, u4]
|
||||
expect(@composer.state.bcc).toEqual [u5]
|
||||
expect(@composer.state.subject).toEqual "Test Message 1"
|
||||
expect(@composer.state.body).toEqual "Hello <b>World</b><br/> This is a test"
|
||||
|
||||
describe "when deciding whether or not to show the subject", ->
|
||||
it "shows the subject when the subject is empty", ->
|
||||
msg = new Message
|
||||
subject: ""
|
||||
spyOn(@composer._proxy, "draft").andReturn msg
|
||||
useDraft.call @, subject: ""
|
||||
makeComposer.call @
|
||||
expect(@composer._shouldShowSubject()).toBe true
|
||||
|
||||
it "shows the subject when the subject looks like a fwd", ->
|
||||
msg = new Message
|
||||
subject: "Fwd: This is the message"
|
||||
spyOn(@composer._proxy, "draft").andReturn msg
|
||||
useDraft.call @, subject: "Fwd: This is the message"
|
||||
makeComposer.call @
|
||||
expect(@composer._shouldShowSubject()).toBe true
|
||||
|
||||
it "shows the subject when the subject looks like a fwd", ->
|
||||
msg = new Message
|
||||
subject: "fwd foo"
|
||||
spyOn(@composer._proxy, "draft").andReturn msg
|
||||
useDraft.call @, subject: "fwd foo"
|
||||
makeComposer.call @
|
||||
expect(@composer._shouldShowSubject()).toBe true
|
||||
|
||||
it "doesn't show subject when subject has fwd text in it", ->
|
||||
msg = new Message
|
||||
subject: "Trick fwd"
|
||||
spyOn(@composer._proxy, "draft").andReturn msg
|
||||
useDraft.call @, subject: "Trick fwd"
|
||||
makeComposer.call @
|
||||
expect(@composer._shouldShowSubject()).toBe false
|
||||
|
||||
it "doesn't show the subject otherwise", ->
|
||||
msg = new Message
|
||||
subject: "Foo bar Baz"
|
||||
spyOn(@composer._proxy, "draft").andReturn msg
|
||||
useDraft.call @, subject: "Foo bar baz"
|
||||
makeComposer.call @
|
||||
expect(@composer._shouldShowSubject()).toBe false
|
||||
|
||||
describe "When composing a new message", ->
|
||||
it "Can add someone in the to field", ->
|
||||
describe "when deciding whether or not to show cc and bcc", ->
|
||||
it "doesn't show cc when there's no one to cc", ->
|
||||
useDraft.call @, cc: []
|
||||
makeComposer.call @
|
||||
expect(@composer.state.showcc).toBe false
|
||||
|
||||
it "Can add someone in the cc field", ->
|
||||
it "shows cc when populated", ->
|
||||
useDraft.call @, cc: [u1,u2]
|
||||
makeComposer.call @
|
||||
expect(@composer.state.showcc).toBe true
|
||||
|
||||
it "Can add someone in the bcc field", ->
|
||||
it "doesn't show bcc when there's no one to bcc", ->
|
||||
useDraft.call @, bcc: []
|
||||
makeComposer.call @
|
||||
expect(@composer.state.showbcc).toBe false
|
||||
|
||||
describe "When replying to a message", ->
|
||||
it "shows bcc when populated", ->
|
||||
useDraft.call @, bcc: [u2,u3]
|
||||
makeComposer.call @
|
||||
expect(@composer.state.showbcc).toBe true
|
||||
|
||||
describe "When replying all to a message", ->
|
||||
describe "When sending a message", ->
|
||||
beforeEach ->
|
||||
remote = require('remote')
|
||||
@dialog = remote.require('dialog')
|
||||
spyOn(remote, "getCurrentWindow")
|
||||
spyOn(@dialog, "showMessageBox")
|
||||
spyOn(Actions, "sendDraft")
|
||||
DraftStore._sendingState = {}
|
||||
|
||||
describe "When forwarding a message", ->
|
||||
it "shows a warning if there are no recipients", ->
|
||||
useDraft.call @, subject: "no recipients"
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.buttons).toEqual ['Edit Message']
|
||||
|
||||
describe "When changing the subject of a message", ->
|
||||
it "shows a warning if there's no subject", ->
|
||||
useDraft.call @, to: [u1], subject: ""
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.buttons).toEqual ['Cancel', 'Send Anyway']
|
||||
|
||||
it "doesn't show a warning if requirements are satisfied", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
@composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
describe "Checking for attachments", ->
|
||||
warn = (body) ->
|
||||
useDraft.call @, subject: "Subject", to: [u1], body: body
|
||||
makeComposer.call(@); @composer._sendDraft()
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
expect(@dialog.showMessageBox).toHaveBeenCalled()
|
||||
dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]
|
||||
expect(dialogArgs.buttons).toEqual ['Cancel', 'Send Anyway']
|
||||
|
||||
noWarn = (body) ->
|
||||
useDraft.call @, subject: "Subject", to: [u1], body: "Sup yo"
|
||||
makeComposer.call(@); @composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
it "warns", -> warn.call(@, "Check out the attached file")
|
||||
it "warns", -> warn.call(@, "I've added an attachment")
|
||||
it "warns", -> warn.call(@, "I'm going to attach the file")
|
||||
|
||||
it "doesn't warn", -> noWarn.call(@, "sup yo")
|
||||
it "doesn't warn", -> noWarn.call(@, "Look at the file")
|
||||
|
||||
it "doesn't show a warning if you've attached a file", ->
|
||||
useDraft.call @,
|
||||
subject: "Subject"
|
||||
to: [u1]
|
||||
body: "Check out attached file"
|
||||
files: [{filename:"abc"}]
|
||||
makeComposer.call(@); @composer._sendDraft()
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
it "bypasses the warning if force bit is set", ->
|
||||
useDraft.call @, to: [u1], subject: ""
|
||||
makeComposer.call(@)
|
||||
@composer._sendDraft(force: true)
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(@dialog.showMessageBox).not.toHaveBeenCalled()
|
||||
|
||||
it "sends when you click the send button", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID)
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
simulateDraftStore = ->
|
||||
DraftStore._sendingState[DRAFT_LOCAL_ID] = true
|
||||
DraftStore.trigger()
|
||||
|
||||
it "doesn't send twice if you double click", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
simulateDraftStore()
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID)
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
it "disables the composer once sending has started", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
cover = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "composer-cover")
|
||||
expect(cover.getDOMNode().style.display).toBe "none"
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
simulateDraftStore()
|
||||
expect(cover.getDOMNode().style.display).toBe "block"
|
||||
expect(@composer.state.isSending).toBe true
|
||||
|
||||
it "re-enables the composer if sending threw an error", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
simulateDraftStore()
|
||||
expect(@composer.state.isSending).toBe true
|
||||
Actions.sendDraftError("oh no")
|
||||
DraftStore._sendingState[DRAFT_LOCAL_ID] = false
|
||||
DraftStore.trigger()
|
||||
expect(@composer.state.isSending).toBe false
|
||||
|
||||
describe "when sending a message with keyboard inputs", ->
|
||||
beforeEach ->
|
||||
useFullDraft.apply(@)
|
||||
makeComposer.call(@)
|
||||
spyOn(@composer, "_sendDraft")
|
||||
InboxTestUtils.loadKeymap "internal_packages/composer/keymaps/composer.cson"
|
||||
|
||||
it "sends the draft on cmd-enter", ->
|
||||
InboxTestUtils.keyPress("cmd-enter", @composer.getDOMNode())
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
it "does not send the draft on enter if the button isn't in focus", ->
|
||||
InboxTestUtils.keyPress("enter", @composer.getDOMNode())
|
||||
expect(@composer._sendDraft).not.toHaveBeenCalled()
|
||||
|
||||
it "sends the draft on enter when the button is in focus", ->
|
||||
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
|
||||
InboxTestUtils.keyPress("enter", sendBtn.getDOMNode())
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
it "doesn't let you send twice", ->
|
||||
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
|
||||
InboxTestUtils.keyPress("enter", sendBtn.getDOMNode())
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
|
||||
describe "When composing a new message", ->
|
||||
it "Can add someone in the to field", ->
|
||||
|
||||
it "Can add someone in the cc field", ->
|
||||
|
||||
it "Can add someone in the bcc field", ->
|
||||
|
||||
describe "When replying to a message", ->
|
||||
|
||||
describe "When replying all to a message", ->
|
||||
|
||||
describe "When forwarding a message", ->
|
||||
|
||||
describe "When changing the subject of a message", ->
|
||||
|
|
|
@ -14,7 +14,16 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.composer-cover {
|
||||
position: absolute;
|
||||
top: -1 * @spacing-double; right: 0; bottom: 0; left: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.composer-action-bar-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border-bottom: 0;
|
||||
|
@ -36,6 +45,7 @@
|
|||
|
||||
.composer-content-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
width: 100%;
|
||||
max-width: @compose-width;
|
||||
|
|
|
@ -4,6 +4,8 @@ Contact = require '../../src/flux/models/contact'
|
|||
NamespaceStore = require '../../src/flux/stores/namespace-store.coffee'
|
||||
DatabaseStore = require '../../src/flux/stores/database-store.coffee'
|
||||
DraftStore = require '../../src/flux/stores/draft-store.coffee'
|
||||
SendDraftTask = require '../../src/flux/tasks/send-draft'
|
||||
Actions = require '../../src/flux/actions'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
fakeThread = null
|
||||
|
@ -264,3 +266,72 @@ describe "DraftStore", ->
|
|||
, (thread, message) ->
|
||||
expect(message).toEqual(fakeMessage1)
|
||||
{}
|
||||
|
||||
describe "sending a draft", ->
|
||||
draftLocalId = "local-123"
|
||||
beforeEach ->
|
||||
DraftStore._sendingState = {}
|
||||
DraftStore._draftSessions = {}
|
||||
DraftStore._draftSessions[draftLocalId] =
|
||||
changes:
|
||||
commit: -> Promise.resolve()
|
||||
spyOn(DraftStore, "trigger")
|
||||
|
||||
afterEach ->
|
||||
atom.state.mode = "editor" # reset to default
|
||||
|
||||
it "sets the sending state when sending", ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
expect(DraftStore.sendingState(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", ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
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", ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe true
|
||||
DraftStore._onSendDraftError(draftLocalId)
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe false
|
||||
expect(DraftStore.trigger).toHaveBeenCalled()
|
||||
|
||||
it "closes the window if it's a popout", ->
|
||||
atom.state.mode = "composer"
|
||||
spyOn(atom, "close")
|
||||
waitsForPromise ->
|
||||
DraftStore._onSendDraft(draftLocalId).then ->
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
|
||||
it "doesn't close the window if it's inline", ->
|
||||
atom.state.mode = "other"
|
||||
spyOn(atom, "close")
|
||||
waitsForPromise ->
|
||||
DraftStore._onSendDraft(draftLocalId).then ->
|
||||
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
|
||||
|
||||
it "queues a SendDraftTask with popout info", ->
|
||||
atom.state.mode = "composer"
|
||||
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
|
||||
|
|
|
@ -108,13 +108,33 @@ describe "SendDraftTask", ->
|
|||
to:
|
||||
name: 'Dummy'
|
||||
email: 'dummy@inboxapp.com'
|
||||
@task = new SendDraftTask(@draft)
|
||||
@draftLocalId = "local-123"
|
||||
@task = new SendDraftTask(@draftLocalId)
|
||||
spyOn(atom.inbox, 'makeRequest').andCallFake (options) ->
|
||||
options.success() if options.success
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) =>
|
||||
Promise.resolve(@draft)
|
||||
spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) =>
|
||||
spyOn(DatabaseStore, 'unpersistModel').andCallFake (draft) ->
|
||||
Promise.resolve()
|
||||
spyOn(atom, "playSound")
|
||||
spyOn(Actions, "postNotification")
|
||||
spyOn(Actions, "sendDraftSuccess")
|
||||
|
||||
it "should unpersist when successfully sent", ->
|
||||
waitsForPromise => @task.performRemote().then =>
|
||||
expect(DatabaseStore.unpersistModel).toHaveBeenCalledWith(@draft)
|
||||
|
||||
it "should notify the draft was sent", ->
|
||||
waitsForPromise => @task.performRemote().then =>
|
||||
expect(Actions.sendDraftSuccess).toHaveBeenCalledWith(@draftLocalId)
|
||||
|
||||
it "should play a sound", ->
|
||||
waitsForPromise => @task.performRemote().then ->
|
||||
expect(atom.playSound).toHaveBeenCalledWith("mail_sent.ogg")
|
||||
|
||||
it "post a notification", ->
|
||||
waitsForPromise => @task.performRemote().then ->
|
||||
expect(Actions.postNotification).toHaveBeenCalled()
|
||||
|
||||
it "should start an API request to /send", ->
|
||||
waitsForPromise =>
|
||||
|
@ -143,7 +163,7 @@ describe "SendDraftTask", ->
|
|||
to:
|
||||
name: 'Dummy'
|
||||
email: 'dummy@inboxapp.com'
|
||||
@task = new SendDraftTask(@draft)
|
||||
@task = new SendDraftTask(@draftLocalId)
|
||||
|
||||
it "should send the draft JSON", ->
|
||||
waitsForPromise =>
|
||||
|
@ -170,7 +190,9 @@ describe "SendDraftTask", ->
|
|||
to:
|
||||
name: 'Dummy'
|
||||
email: 'dummy@inboxapp.com'
|
||||
@task = new SendDraftTask(@draft)
|
||||
@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) ->
|
||||
|
@ -192,3 +214,26 @@ describe "SendDraftTask", ->
|
|||
waitsForPromise =>
|
||||
@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)
|
||||
|
|
|
@ -21,6 +21,8 @@ globalActions = [
|
|||
"multiWindowNotification",
|
||||
|
||||
# Draft actions
|
||||
"sendDraftError",
|
||||
"sendDraftSuccess",
|
||||
"destroyDraftSuccess",
|
||||
"destroyDraftError"
|
||||
]
|
||||
|
|
|
@ -100,6 +100,7 @@ class DraftStoreProxy
|
|||
unlisten() for unlisten in @unlisteners
|
||||
|
||||
_onDraftChanged: (change) ->
|
||||
return if not change?
|
||||
# We don't accept changes unless our draft object is loaded
|
||||
return unless @_draft
|
||||
|
||||
|
|
|
@ -41,9 +41,13 @@ DraftStore = Reflux.createStore
|
|||
@listenTo Actions.removeFile, @_onRemoveFile
|
||||
@listenTo Actions.attachFileComplete, @_onAttachFileComplete
|
||||
|
||||
@listenTo Actions.sendDraftError, @_onSendDraftSuccess
|
||||
@listenTo Actions.sendDraftSuccess, @_onSendDraftError
|
||||
|
||||
@listenTo Actions.destroyDraftSuccess, @_closeWindow
|
||||
@_drafts = []
|
||||
@_draftSessions = {}
|
||||
@_sendingState = {}
|
||||
|
||||
# TODO: Doesn't work if we do window.addEventListener, but this is
|
||||
# fragile. Pending an Atom fix perhaps?
|
||||
|
@ -85,6 +89,8 @@ DraftStore = Reflux.createStore
|
|||
@_draftSessions[localId] ?= new DraftStoreProxy(localId)
|
||||
@_draftSessions[localId]
|
||||
|
||||
sendingState: (draftLocalId) -> @_sendingState[draftLocalId] ? false
|
||||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
_onDataChanged: (change) ->
|
||||
|
@ -213,7 +219,9 @@ DraftStore = Reflux.createStore
|
|||
# Queue the task to destroy the draft
|
||||
Actions.queueTask(new DestroyDraftTask(draftLocalId))
|
||||
|
||||
_onSendDraft: (draftLocalId) ->
|
||||
_onSendDraft: (draftLocalId) -> new Promise (resolve, reject) =>
|
||||
@_sendingState[draftLocalId] = true
|
||||
@trigger()
|
||||
# Immediately save any pending changes so we don't save after sending
|
||||
save = @_draftSessions[draftLocalId]?.changes.commit()
|
||||
save.then =>
|
||||
|
@ -223,6 +231,15 @@ DraftStore = Reflux.createStore
|
|||
# Queue the task to send the draft
|
||||
fromPopout = atom.state.mode is "composer"
|
||||
Actions.queueTask(new SendDraftTask(draftLocalId, fromPopout: fromPopout))
|
||||
resolve()
|
||||
|
||||
_onSendDraftError: (draftLocalId) ->
|
||||
@_sendingState[draftLocalId] = false
|
||||
@trigger()
|
||||
|
||||
_onSendDraftSuccess: (draftLocalId) ->
|
||||
@_sendingState[draftLocalId] = false
|
||||
@trigger()
|
||||
|
||||
_onAttachFileComplete: ({file, messageLocalId}) ->
|
||||
@sessionForLocalId(messageLocalId).prepare().then (proxy) ->
|
||||
|
@ -235,4 +252,3 @@ DraftStore = Reflux.createStore
|
|||
files = proxy.draft().files ? []
|
||||
files = _.reject files, (f) -> f.id is file.id
|
||||
proxy.changes.add({files}, true)
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class SendDraftTask extends Task
|
|||
new Promise (resolve, reject) =>
|
||||
# Fetch the latest draft data to make sure we make the request with the most
|
||||
# recent draft version
|
||||
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) ->
|
||||
DatabaseStore.findByLocalId(Message, @draftLocalId).then (draft) =>
|
||||
# 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
|
||||
|
||||
|
@ -47,9 +47,10 @@ class SendDraftTask extends Task
|
|||
method: 'POST'
|
||||
body: body
|
||||
returnsModel: true
|
||||
success: ->
|
||||
success: =>
|
||||
atom.playSound('mail_sent.ogg')
|
||||
Actions.postNotification({message: "Sent!", type: 'success'})
|
||||
Actions.sendDraftSuccess(@draftLocalId)
|
||||
DatabaseStore.unpersistModel(draft).then(resolve)
|
||||
error: reject
|
||||
.catch(reject)
|
||||
|
@ -73,5 +74,6 @@ class SendDraftTask extends Task
|
|||
Actions.dequeueTask(@)
|
||||
|
||||
_notifyError: (msg) ->
|
||||
Actions.sendDraftError(@draftLocalId, msg)
|
||||
if @fromPopout then atom.displayComposer(@draftLocalId, error: msg)
|
||||
@notifyErrorMessage(msg)
|
||||
|
|
Loading…
Add table
Reference in a new issue