Skip to content

Commit

Permalink
fix(composer): much better specs for composer & quoted text
Browse files Browse the repository at this point in the history
Summary: Fixed a bug bug with the quoted text clearing the bodies on replies

Test Plan: all the tests

Reviewers: dillon, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D1981
  • Loading branch information
emorikawa committed Sep 4, 2015
1 parent 1daeaff commit ee41722
Showing 9 changed files with 252 additions and 148 deletions.
12 changes: 9 additions & 3 deletions internal_packages/composer/lib/contenteditable-component.cjsx
Original file line number Diff line number Diff line change
@@ -106,9 +106,17 @@ class ContenteditableComponent extends React.Component
onInput={@_onInput}
onKeyDown={@_onKeyDown}
dangerouslySetInnerHTML={@_dangerouslySetInnerHTML()}></div>
<a className={@_quotedTextClasses()} onClick={@_onToggleQuotedText}></a>
{@_renderQuotedTextControl()}
</div>

_renderQuotedTextControl: ->
if QuotedHTMLParser.hasQuotedHTML(@props.html)
text = if @props.mode?.showQuotedText then "Hide" else "Show"
<a className="quoted-text-control" onClick={@_onToggleQuotedText}>
<span className="dots">&bull;&bull;&bull;</span>{text} previous
</a>
else return null

focus: =>
@_editableNode().focus()

@@ -1096,7 +1104,5 @@ class ContenteditableComponent extends React.Component

_quotedTextClasses: => classNames
"quoted-text-control": true
"no-quoted-text": not QuotedHTMLParser.hasQuotedHTML(@props.html)
"show-quoted-text": @props.mode?.showQuotedText

module.exports = ContenteditableComponent
75 changes: 73 additions & 2 deletions internal_packages/composer/spec/composer-view-spec.cjsx
Original file line number Diff line number Diff line change
@@ -107,9 +107,11 @@ useDraft = (draftAttributes={}) ->
@draft = new Message _.extend({draft: true, body: ""}, draftAttributes)
draft = @draft
proxy = draftStoreProxyStub(DRAFT_CLIENT_ID, @draft)
@proxy = proxy


spyOn(ComposerView.prototype, "componentWillMount").andCallFake ->
# NOTE: This is called in the context of the component.
@_prepareForDraft(DRAFT_CLIENT_ID)
@_setupSession(proxy)

@@ -118,8 +120,7 @@ useDraft = (draftAttributes={}) ->
# `componentWillMount`, we manually call sessionForClientId to make this
# part of the test synchronous. We need to make the `then` block of the
# sessionForClientId do nothing so `_setupSession` is not called twice!
spyOn(DraftStore, "sessionForClientId").andCallFake ->
then: ->
spyOn(DraftStore, "sessionForClientId").andCallFake -> then: ->

useFullDraft = ->
useDraft.call @,
@@ -141,6 +142,76 @@ describe "populated composer", ->
@isSending = {state: false}
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state

describe "when sending a new message", ->
it 'makes a request with the message contents', ->
useDraft.call @
makeComposer.call @
editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
spyOn(@proxy.changes, "add")
editableNode.innerHTML = "Hello <strong>world</strong>"
ReactTestUtils.Simulate.input(editableNode)
expect(@proxy.changes.add).toHaveBeenCalled()
expect(@proxy.changes.add.calls.length).toBe 1
body = @proxy.changes.add.calls[0].args[0].body
expect(body).toBe "<head></head><body>Hello <strong>world</strong></body>"

describe "when sending a reply-to message", ->
beforeEach ->
@replyBody = """<blockquote class="gmail_quote">On Sep 3 2015, at 12:14 pm, Evan Morikawa &lt;evan@evanmorikawa.com&gt; wrote:<br>This is a test!</blockquote>"""

useDraft.call @,
from: [u1]
to: [u2]
subject: "Test Reply Message 1"
body: @replyBody

makeComposer.call @
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
spyOn(@proxy.changes, "add")

it 'begins with the replying message collapsed', ->
expect(@editableNode.innerHTML).toBe ""

it 'saves the full new body, plus quoted text', ->
@editableNode.innerHTML = "Hello <strong>world</strong>"
ReactTestUtils.Simulate.input(@editableNode)
expect(@proxy.changes.add).toHaveBeenCalled()
expect(@proxy.changes.add.calls.length).toBe 1
body = @proxy.changes.add.calls[0].args[0].body
expect(body).toBe """<head></head><body>Hello <strong>world</strong>#{@replyBody}</body>"""

describe "when sending a forwarded message message", ->
beforeEach ->
@fwdBody = """<br><br><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
Begin forwarded message:
<br><br>
From: Evan Morikawa &lt;evan@evanmorikawa.com&gt;<br>Subject: Test Forward Message 1<br>Date: Sep 3 2015, at 12:14 pm<br>To: Evan Morikawa &lt;evan@nylas.com&gt;
<br><br>
<meta content="text/html; charset=us-ascii">This is a test!
</blockquote>"""

useDraft.call @,
from: [u1]
to: [u2]
subject: "Fwd: Test Forward Message 1"
body: @fwdBody

makeComposer.call @
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@composer, 'contentEditable'))
spyOn(@proxy.changes, "add")

it 'begins with the forwarded message expanded', ->
expect(@editableNode.innerHTML).toBe @fwdBody

it 'saves the full new body, plus forwarded text', ->
@editableNode.innerHTML = "Hello <strong>world</strong>#{@fwdBody}"
ReactTestUtils.Simulate.input(@editableNode)
expect(@proxy.changes.add).toHaveBeenCalled()
expect(@proxy.changes.add.calls.length).toBe 1
body = @proxy.changes.add.calls[0].args[0].body
expect(body).toBe """Hello <strong>world</strong>#{@fwdBody}"""

describe "When displaying info from a draft", ->
beforeEach ->
useFullDraft.apply(@)
194 changes: 100 additions & 94 deletions internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx
Original file line number Diff line number Diff line change
@@ -11,106 +11,112 @@ ContenteditableComponent = require "../lib/contenteditable-component",
describe "ContenteditableComponent", ->
beforeEach ->
@onChange = jasmine.createSpy('onChange')
@html = 'Test <strong>HTML</strong><br>'
@component = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@html} onChange={@onChange}/>
)

@htmlWithQuote = 'Test <strong>HTML</strong><br><br><blockquote class="gmail_quote">QUOTE</blockquote>'
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote}
onChange={@onChange}
mode={showQuotedText: false}/>
)

describe "quoted-text-control", ->
it "should be rendered", ->
expect(ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')).toBeDefined()

it "should be visible if the html contains quoted text", ->
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
expect(@toggle.props.className.indexOf('no-quoted-text') >= 0).toBe(false)

it "should be have `show-quoted-text` if showQuotedText is true", ->
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote} onChange={@onChange} mode={showQuotedText: true}/>
)
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
expect(@toggle.props.className.indexOf('show-quoted-text') >= 0).toBe(true)

it "should not have `show-quoted-text` if showQuotedText is false", ->
@componentWithQuote.setState(showQuotedText: false)
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
expect(@toggle.props.className.indexOf('show-quoted-text') >= 0).toBe(false)

it "should be hidden otherwise", ->
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
expect(@toggle.props.className.indexOf('no-quoted-text') >= 0).toBe(true)

describe "when showQuotedText is false", ->
it "should not display quoted text", ->
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
expect(React.findDOMNode(@editDiv).innerHTML).toEqual @html

describe "when showQuotedText is true", ->
@htmlNoQuote = 'Test <strong>HTML</strong><br>'
@htmlWithQuote = 'Test <strong>HTML</strong><br><blockquote class="gmail_quote">QUOTE</blockquote>'

# Must be called with the test's scope
setHTML = (newHTML) ->
@$contentEditable.innerHTML = newHTML
ReactTestUtils.Simulate.input(@$contentEditable, {target: {value: newHTML}})

describe "quoted-text-control toggle button", ->

describe "when there's no quoted text", ->
beforeEach ->
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote}
@contentEditable = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlNoQuote}
onChange={@onChange}
mode={showQuotedText: true}/>
)
mode={showQuotedText: true}/>)
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))

it "should display all the HTML", ->
@componentWithQuote.setState(showQuotedText: true)
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
expect(React.findDOMNode(@editDiv).innerHTML.indexOf('gmail_quote') >= 0).toBe(true)
it 'should not display any quoted text', ->
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote

describe "showQuotedText", ->
it "should default to false", ->
expect(@component.props.mode?.showQuotedText).toBeUndefined()
it "allows the text to update", ->
textToAdd = "MORE <strong>TEXT</strong>!"
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
setHTML.call(@, textToAdd + @htmlNoQuote)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(textToAdd + @htmlNoQuote)

describe "when the html is changed", ->
it 'should not render the quoted-text-control toggle', ->
toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@contentEditable, 'quoted-text-control')
expect(toggles.length).toBe 0


describe 'when showQuotedText is true', ->
beforeEach ->
@changedHtmlWithoutQuote = '<head></head><body>Changed <strong>NEW 1 HTML</strong><br><br></body>'
@changedHtmlWithQuote = 'Changed <strong>NEW 1 HTML</strong><br><br><blockquote class="gmail_quote">QUOTE</blockquote>'
@contentEditable = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote}
onChange={@onChange}
mode={showQuotedText: true}/>)
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))

it 'should display the quoted text', ->
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote

it "should call `props.onChange` with the entire HTML string", ->
textToAdd = "MORE <strong>TEXT</strong>!"
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
setHTML.call(@, textToAdd + @htmlWithQuote)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(textToAdd + @htmlWithQuote)

it "should allow the quoted text to be changed", ->
newText = 'Test <strong>NEW 1 HTML</strong><blockquote class="gmail_quote">QUOTE CHANGED!!!</blockquote>'
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
setHTML.call(@, newText)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(newText)

describe 'quoted text control toggle button', ->
beforeEach ->
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@contentEditable, 'quoted-text-control')

it 'should be rendered', ->
expect(@toggle).toBeDefined()

@performEdit = (newHTML, component = @componentWithQuote) =>
editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(component, 'contentEditable')
React.findDOMNode(editDiv).innerHTML = newHTML
ReactTestUtils.Simulate.input(editDiv, {target: {value: newHTML}})
it 'prompts to hide the quote', ->
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Hide previous"

describe "when showQuotedText is true", ->
describe 'when showQuotedText is false', ->
beforeEach ->
@contentEditable = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote}
onChange={@onChange}
mode={showQuotedText: false}/>)
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))

# The quoted text dom parser wraps stuff inertly in body tags
wrapBody = (html) -> "<head></head><body>#{html}</body>"

it 'should not display any quoted text', ->
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote

it "should let you change the text, and then append the quoted text part to the end before firing `onChange`", ->
textToAdd = "MORE <strong>TEXT</strong>!"
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
setHTML.call(@, textToAdd + @htmlNoQuote)
ev = @onChange.mostRecentCall.args[0]
# Note that we expect the version WITH a quote while setting the
# version withOUT a quote.
expect(ev.target.value).toEqual(wrapBody(textToAdd + @htmlWithQuote))

it "should let you add more html that looks like quoted text, and still properly appends the old quoted text", ->
textToAdd = "Yo <blockquote class=\"gmail_quote\">I'm a fake quote</blockquote>"
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
setHTML.call(@, textToAdd + @htmlNoQuote)
ev = @onChange.mostRecentCall.args[0]
# Note that we expect the version WITH a quote while setting the
# version withOUT a quote.
expect(ev.target.value).toEqual(wrapBody(textToAdd + @htmlWithQuote))

describe 'quoted text control toggle button', ->
beforeEach ->
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote}
onChange={@onChange}
mode={showQuotedText: true}/>
)

it "should call `props.onChange` with the entire HTML string", ->
@componentWithQuote.setState(showQuotedText: true)
@performEdit(@changedHtmlWithQuote)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(@changedHtmlWithQuote)

it "should allow the quoted text to be changed", ->
changed = 'Test <strong>NEW 1 HTML</strong><blockquote class="gmail_quote">QUOTE CHANGED!!!</blockquote>'
@componentWithQuote.setState(showQuotedText: true)
@performEdit(changed)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(changed)

describe "when showQuotedText is false", ->
it "should let you change the text, and then append the quoted text part to the end before firing `onChange`", ->
@componentWithQuote.setState(showQuotedText: false)
@performEdit(@changedHtmlWithoutQuote)
ev = @onChange.mostRecentCall.args[0]
withQuote = "<head></head><body>#{@changedHtmlWithQuote}</body>"
expect(ev.target.value).toEqual(withQuote)

it "should work if the component does not contain quoted text", ->
changed = '<head></head><body>Hallooo! <strong>NEW 1 HTML HTML HTML</strong><br></body>'
@component.setState(showQuotedText: true)
@performEdit(changed, @component)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(changed)
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@contentEditable, 'quoted-text-control')

it 'should be rendered', ->
expect(@toggle).toBeDefined()

it 'prompts to hide the quote', ->
expect(React.findDOMNode(@toggle).textContent).toEqual "•••Show previous"
5 changes: 2 additions & 3 deletions internal_packages/message-list/lib/email-frame.cjsx
Original file line number Diff line number Diff line change
@@ -35,7 +35,6 @@ class EmailFrame extends React.Component
!_.isEqual(newProps, @props)

_writeContent: =>
wrapperClass = if @props.showQuotedText then "show-quoted-text" else ""
doc = React.findDOMNode(@).contentDocument
doc.open()

@@ -50,7 +49,7 @@ class EmailFrame extends React.Component
EmailFixingStyles = EmailFixingStyles.replace(/.ignore-in-parent-frame/g, '')
if (EmailFixingStyles)
doc.write("<style>#{EmailFixingStyles}</style>")
doc.write("<div id='inbox-html-wrapper' class='#{wrapperClass}'>#{@_emailContent()}</div>")
doc.write("<div id='inbox-html-wrapper'>#{@_emailContent()}</div>")
doc.close()

# Notify the EventedIFrame that we've replaced it's document (with `open`)
@@ -80,7 +79,7 @@ class EmailFrame extends React.Component
if @props.showQuotedText
@props.content
else
QuotedHTMLParser.hideQuotedHTML(@props.content)
QuotedHTMLParser.removeQuotedHTML(@props.content, keepIfWholeBodyIsQuote: true)


module.exports = EmailFrame
15 changes: 9 additions & 6 deletions internal_packages/message-list/lib/message-item.cjsx
Original file line number Diff line number Diff line change
@@ -80,13 +80,21 @@ class MessageItem extends React.Component
<div className="message-item-area">
{@_renderHeader()}
<EmailFrame showQuotedText={@state.showQuotedText} content={@_formatBody()}/>
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
{@_renderQuotedTextControl()}
{@_renderEvents()}
{@_renderAttachments()}
</div>
</div>
</div>

_renderQuotedTextControl: ->
if QuotedHTMLParser.hasQuotedHTML(@props.message.body)
text = if @state.showQuotedText then "Hide" else "Show"
<a className="quoted-text-control" onClick={@_toggleQuotedText}>
<span className="dots">&bull;&bull;&bull;</span>{text} previous
</a>
else return null

_renderHeader: =>
classes = classNames
"message-header": true
@@ -168,11 +176,6 @@ class MessageItem extends React.Component
else
<div></div>

_quotedTextClasses: => classNames
"quoted-text-control": true
'no-quoted-text': not QuotedHTMLParser.hasQuotedHTML(@props.message.body)
'show-quoted-text': @state.showQuotedText

_renderHeaderSideItems: ->
styles =
position: "absolute"
Loading

0 comments on commit ee41722

Please sign in to comment.