diff --git a/common/static/coffee/spec/discussion/.gitignore b/common/static/coffee/spec/discussion/.gitignore
deleted file mode 100644
index ac5223af30..0000000000
--- a/common/static/coffee/spec/discussion/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-!view/discussion_thread_edit_view_spec.js
-!view/discussion_topic_menu_view_spec.js
diff --git a/common/static/coffee/spec/discussion/content_spec.coffee b/common/static/coffee/spec/discussion/content_spec.coffee
deleted file mode 100644
index 77b8fd41e2..0000000000
--- a/common/static/coffee/spec/discussion/content_spec.coffee
+++ /dev/null
@@ -1,115 +0,0 @@
-describe 'All Content', ->
- beforeEach ->
- DiscussionSpecHelper.setUpGlobals()
-
- describe 'Staff and TA Content', ->
- beforeEach ->
- DiscussionUtil.loadRoles({"Moderator": [567], "Administrator": [567], "Community TA": [567]})
-
- it 'anonymous thread should not include login role label', ->
- anon_content = new Content
- anon_content.initialize
- expect(anon_content.get 'staff_authored').toBe false
- expect(anon_content.get 'community_ta_authored').toBe false
-
- it 'general thread should include login role label', ->
- anon_content = new Content { user_id: '567' }
- anon_content.initialize
- expect(anon_content.get 'staff_authored').toBe true
- expect(anon_content.get 'community_ta_authored').toBe true
-
- describe 'Content', ->
- beforeEach ->
- @content = new Content {
- id: '01234567',
- user_id: '567',
- course_id: 'edX/999/test',
- body: 'this is some content',
- abuse_flaggers: ['123']
- }
-
- it 'should exist', ->
- expect(Content).toBeDefined()
-
- it 'is initialized correctly', ->
- @content.initialize
- expect(Content.contents['01234567']).toEqual @content
- expect(@content.get 'id').toEqual '01234567'
- expect(@content.get 'user_url').toEqual '/courses/edX/999/test/discussion/forum/users/567'
- expect(@content.get 'children').toEqual []
- expect(@content.get 'comments').toEqual(jasmine.any(Comments))
-
- it 'can update info', ->
- @content.updateInfo {
- ability: {'can_edit': true},
- voted: true,
- subscribed: true
- }
- expect(@content.get 'ability').toEqual {'can_edit': true}
- expect(@content.get 'voted').toEqual true
- expect(@content.get 'subscribed').toEqual true
-
- it 'can be flagged for abuse', ->
- @content.flagAbuse()
- expect(@content.get 'abuse_flaggers').toEqual ['123', '567']
-
- it 'can be unflagged for abuse', ->
- temp_array = []
- temp_array.push(window.user.get('id'))
- @content.set("abuse_flaggers",temp_array)
- @content.unflagAbuse()
- expect(@content.get 'abuse_flaggers').toEqual []
-
- describe 'Comments', ->
- beforeEach ->
- @comment1 = new Comment {id: '123'}
- @comment2 = new Comment {id: '345'}
-
- it 'can contain multiple comments', ->
- myComments = new Comments
- expect(myComments.length).toEqual 0
- myComments.add @comment1
- expect(myComments.length).toEqual 1
- myComments.add @comment2
- expect(myComments.length).toEqual 2
-
- it 'returns results to the find method', ->
- myComments = new Comments
- myComments.add @comment1
- expect(myComments.find('123')).toBe @comment1
-
- it 'can be endorsed', ->
-
- DiscussionUtil.loadRoles(
- {"Moderator": [111], "Administrator": [222], "Community TA": [333]}
- )
- @discussionThread = new Thread({id: 1, thread_type: "discussion", user_id: 99})
- @discussionResponse = new Comment({id: 1, thread: @discussionThread})
- @questionThread = new Thread({id: 1, thread_type: "question", user_id: 99})
- @questionResponse = new Comment({id: 1, thread: @questionThread})
-
- # mod
- window.user = new DiscussionUser({id: 111})
- expect(@discussionResponse.canBeEndorsed()).toBe(true)
- expect(@questionResponse.canBeEndorsed()).toBe(true)
-
- # admin
- window.user = new DiscussionUser({id: 222})
- expect(@discussionResponse.canBeEndorsed()).toBe(true)
- expect(@questionResponse.canBeEndorsed()).toBe(true)
-
- # TA
- window.user = new DiscussionUser({id: 333})
- expect(@discussionResponse.canBeEndorsed()).toBe(true)
- expect(@questionResponse.canBeEndorsed()).toBe(true)
-
- # thread author
- window.user = new DiscussionUser({id: 99})
- expect(@discussionResponse.canBeEndorsed()).toBe(false)
- expect(@questionResponse.canBeEndorsed()).toBe(true)
-
- # anyone else
- window.user = new DiscussionUser({id: 999})
- expect(@discussionResponse.canBeEndorsed()).toBe(false)
- expect(@questionResponse.canBeEndorsed()).toBe(false)
-
diff --git a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee b/common/static/coffee/spec/discussion/discussion_spec_helper.coffee
deleted file mode 100644
index bea233e0b5..0000000000
--- a/common/static/coffee/spec/discussion/discussion_spec_helper.coffee
+++ /dev/null
@@ -1,72 +0,0 @@
-class @DiscussionSpecHelper
- # This is sad. We should avoid dependence on global vars.
- @setUpGlobals = ->
- DiscussionUtil.loadRoles({"Moderator": [], "Administrator": [], "Community TA": []})
- window.$$course_id = "edX/999/test"
- window.user = new DiscussionUser({username: "test_user", id: "567", upvoted_ids: []})
- DiscussionUtil.setUser(window.user)
-
- @makeTA = () ->
- DiscussionUtil.roleIds["Community TA"].push(parseInt(DiscussionUtil.getUser().id))
-
- @makeModerator = () ->
- DiscussionUtil.roleIds["Moderator"].push(parseInt(DiscussionUtil.getUser().id))
-
- @makeAjaxSpy = (fakeAjax) ->
- spyOn($, "ajax").and.callFake(
- (params) ->
- fakeAjax(params)
- {always: ->}
- )
-
- @makeEventSpy = () ->
- jasmine.createSpyObj('event', ['preventDefault', 'target'])
-
- @makeCourseSettings = (is_cohorted=true) ->
- new DiscussionCourseSettings(
- category_map:
- children: ['Test Topic', 'Other Topic']
- entries:
- 'Test Topic':
- is_cohorted: is_cohorted
- id: 'test_topic'
- 'Other Topic':
- is_cohorted: is_cohorted
- id: 'other_topic'
- is_cohorted: is_cohorted
- )
-
- @setUnderscoreFixtures = ->
- templateNames = [
- 'thread', 'thread-show', 'thread-edit',
- 'thread-response', 'thread-response-show', 'thread-response-edit',
- 'response-comment-show', 'response-comment-edit',
- 'thread-list-item', 'discussion-home', 'search-alert',
- 'new-post', 'thread-type', 'new-post-menu-entry',
- 'new-post-menu-category', 'topic', 'post-user-display',
- 'inline-discussion', 'pagination', 'user-profile', 'profile-thread'
- ]
- templateNamesNoTrailingTemplate = [
- 'forum-action-endorse', 'forum-action-answer', 'forum-action-follow',
- 'forum-action-vote', 'forum-action-report', 'forum-action-pin',
- 'forum-action-close', 'forum-action-edit', 'forum-action-delete',
- 'forum-actions',
- ]
-
- for templateName in templateNames
- templateFixture = readFixtures('common/templates/discussion/' + templateName + '.underscore')
- appendSetFixtures($('
- """)
- @threads = [
- DiscussionViewSpecHelper.makeThreadWithProps({
- id: "1",
- title: "Thread1",
- votes: {up_count: '20'},
- pinned: true,
- comments_count: 1,
- created_at: '2013-04-03T20:08:39Z',
- }),
- DiscussionViewSpecHelper.makeThreadWithProps({
- id: "2",
- title: "Thread2",
- votes: {up_count: '42'},
- comments_count: 2,
- created_at: '2013-04-03T20:07:39Z',
- }),
- DiscussionViewSpecHelper.makeThreadWithProps({
- id: "3",
- title: "Thread3",
- votes: {up_count: '12'},
- comments_count: 3,
- created_at: '2013-04-03T20:06:39Z',
- }),
- DiscussionViewSpecHelper.makeThreadWithProps({
- id: "4",
- title: "Thread4",
- votes: {up_count: '25'},
- comments_count: 0,
- pinned: true,
- created_at: '2013-04-03T20:05:39Z',
- }),
- ]
- deferred = $.Deferred()
- spyOn($, "ajax").and.returnValue(deferred);
-
- @discussion = new Discussion([])
- @view = new DiscussionThreadListView(
- collection: @discussion,
- el: $("#fixture-element"),
- courseSettings: new DiscussionCourseSettings({is_cohorted: true})
- )
- @view.render()
-
- setupAjax = (callback) ->
- $.ajax.and.callFake(
- (params) =>
- if callback
- callback(params)
- params.success({discussion_data: [], page: 1, num_pages: 1})
- {always: ->}
- )
-
- renderSingleThreadWithProps = (props) ->
- makeView(new Discussion([new Thread(DiscussionViewSpecHelper.makeThreadWithProps(props))])).render()
-
- makeView = (discussion) ->
- return new DiscussionThreadListView(
- el: $("#fixture-element"),
- collection: discussion,
- courseSettings: new DiscussionCourseSettings({is_cohorted: true})
- )
-
- expectFilter = (filterVal) ->
- $.ajax.and.callFake((params) ->
- _.each(["unread", "unanswered", "flagged"], (paramName)->
- if paramName == filterVal
- expect(params.data[paramName]).toEqual(true)
- else
- expect(params.data[paramName]).toBeUndefined()
- )
- {always: ->}
- )
-
- describe "should filter correctly", ->
- _.each(["all", "unread", "unanswered", "flagged"], (filterVal) ->
- it "for #{filterVal}", ->
- expectFilter(filterVal)
- @view.$(".forum-nav-filter-main-control").val(filterVal).change()
- expect($.ajax).toHaveBeenCalled()
- )
-
- describe "cohort selector", ->
- it "should not be visible to students", ->
- expect(@view.$(".forum-nav-filter-cohort-control:visible")).not.toExist()
-
- it "should allow moderators to select visibility", ->
- DiscussionSpecHelper.makeModerator()
- @view.render()
- expectedGroupId = null
- setupAjax((params) => expect(params.data.group_id).toEqual(expectedGroupId))
- _.each(
- [
- {val: "", expectedGroupId: undefined},
- {val: "1", expectedGroupId: "1"},
- {val: "2", expectedGroupId: "2"}
- ],
- (optionInfo) =>
- expectedGroupId = optionInfo.expectedGroupId
- @view.$(".forum-nav-filter-cohort-control").val(optionInfo.val).change()
- expect($.ajax).toHaveBeenCalled()
- $.ajax.calls.reset()
- )
-
- it "search should clear filter", ->
- expectFilter(null)
- @view.$(".forum-nav-filter-main-control").val("flagged")
- @view.searchFor("foobar")
- expect(@view.$(".forum-nav-filter-main-control").val()).toEqual("all")
-
- checkThreadsOrdering = (view, sort_order, type) ->
- expect(view.$el.find(".forum-nav-thread").children().length).toEqual(4)
- expect(view.$el.find(".forum-nav-thread:nth-child(1) .forum-nav-thread-title").text()).toEqual(sort_order[0])
- expect(view.$el.find(".forum-nav-thread:nth-child(2) .forum-nav-thread-title").text()).toEqual(sort_order[1])
- expect(view.$el.find(".forum-nav-thread:nth-child(3) .forum-nav-thread-title").text()).toEqual(sort_order[2])
- expect(view.$el.find(".forum-nav-thread:nth-child(4) .forum-nav-thread-title").text()).toEqual(sort_order[3])
- expect(view.$el.find(".forum-nav-sort-control").val()).toEqual(type)
-
- describe "thread rendering should be correct", ->
- checkRender = (threads, type, sort_order) ->
- discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: type})
- view = makeView(discussion)
- view.render()
- checkThreadsOrdering(view, sort_order, type)
- expect(view.$el.find(".forum-nav-thread-comments-count:visible").length).toEqual(if type == "votes" then 0 else 4)
- expect(view.$el.find(".forum-nav-thread-votes-count:visible").length).toEqual(if type == "votes" then 4 else 0)
- if type == "votes"
- expect(
- _.map(
- view.$el.find(".forum-nav-thread-votes-count"),
- (element) -> $(element).text().trim()
- )
- ).toEqual(["+25 votes", "+20 votes", "+42 votes", "+12 votes"])
-
- it "with sort preference activity", ->
- checkRender(@threads, "activity", ["Thread1", "Thread2", "Thread3", "Thread4"])
-
- it "with sort preference votes", ->
- checkRender(@threads, "votes", ["Thread4", "Thread1", "Thread2", "Thread3"])
-
- it "with sort preference comments", ->
- checkRender(@threads, "comments", ["Thread1", "Thread4", "Thread3", "Thread2"])
-
- describe "Sort change should be correct", ->
- changeSorting = (threads, selected_type, new_type, sort_order) ->
- discussion = new Discussion(_.map(threads, (thread) -> new Thread(thread)), {pages: 1, sort: selected_type})
- view = makeView(discussion)
- view.render()
- sortControl = view.$el.find(".forum-nav-sort-control")
- expect(sortControl.val()).toEqual(selected_type)
- sorted_threads = []
- if new_type == 'activity'
- sorted_threads = [threads[0], threads[3], threads[1], threads[2]]
- else if new_type == 'comments'
- sorted_threads = [threads[0], threads[3], threads[2], threads[1]]
- else if new_type == 'votes'
- sorted_threads = [threads[3], threads[0], threads[1], threads[2]]
- $.ajax.and.callFake((params) =>
- params.success(
- {"discussion_data":sorted_threads, page:1, num_pages:1}
- )
- {always: ->}
- )
- sortControl.val(new_type).change()
- expect($.ajax).toHaveBeenCalled()
- checkThreadsOrdering(view, sort_order, new_type)
-
- it "with sort preference activity", ->
- changeSorting(@threads, "comments", "activity", ["Thread1", "Thread4", "Thread3", "Thread2"])
-
- it "with sort preference votes", ->
- changeSorting(@threads, "activity", "votes", ["Thread4", "Thread1", "Thread2", "Thread3"])
-
- it "with sort preference comments", ->
- changeSorting(@threads, "votes", "comments", ["Thread1", "Thread4", "Thread3", "Thread2"])
-
- describe "search alerts", ->
-
- testAlertMessages = (expectedMessages) ->
- expect($(".search-alert .message").map( ->
- $(@).html()
- ).get()).toEqual(expectedMessages)
-
- it "renders and removes search alerts", ->
- testAlertMessages []
- foo = @view.addSearchAlert("foo")
- testAlertMessages ["foo"]
- bar = @view.addSearchAlert("bar")
- testAlertMessages ["foo", "bar"]
- @view.removeSearchAlert(foo.cid)
- testAlertMessages ["bar"]
- @view.removeSearchAlert(bar.cid)
- testAlertMessages []
-
- it "clears all search alerts", ->
- @view.addSearchAlert("foo")
- @view.addSearchAlert("bar")
- @view.addSearchAlert("baz")
- testAlertMessages ["foo", "bar", "baz"]
- @view.clearSearchAlerts()
- testAlertMessages []
-
- describe "search spell correction", ->
-
- beforeEach ->
- spyOn(@view, "searchForUser")
-
- testCorrection = (view, correctedText) ->
- spyOn(view, "addSearchAlert")
- $.ajax.and.callFake(
- (params) =>
- params.success(
- {discussion_data: [], page: 42, num_pages: 99, corrected_text: correctedText}, 'success'
- )
- {always: ->}
- )
- view.searchFor("dummy")
- expect($.ajax).toHaveBeenCalled()
-
- it "adds a search alert when an alternate term was searched", ->
- testCorrection(@view, "foo")
- expect(@view.addSearchAlert.calls.count()).toEqual(1)
- expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/foo/)
-
- it "does not add a search alert when no alternate term was searched", ->
- testCorrection(@view, null)
- expect(@view.addSearchAlert.calls.count()).toEqual(1)
- expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/no threads matched/i)
-
- it "clears search alerts when a new search is performed", ->
- spyOn(@view, "clearSearchAlerts")
- spyOn(DiscussionUtil, "safeAjax")
- @view.searchFor("dummy")
- expect(@view.clearSearchAlerts).toHaveBeenCalled()
-
- it "clears search alerts when the underlying collection changes", ->
- spyOn(@view, "clearSearchAlerts")
- spyOn(@view, "renderThread")
- @view.collection.trigger("change", new Thread({id: 1}))
- expect(@view.clearSearchAlerts).toHaveBeenCalled()
-
- describe "Search events", ->
-
- it "perform search when enter pressed inside search textfield", ->
- setupAjax()
- spyOn(@view, "searchFor")
- @view.$el.find(".forum-nav-search-input").trigger($.Event("keydown", {which: 13}))
- expect(@view.searchFor).toHaveBeenCalled()
-
- it "perform search when search icon is clicked", ->
- setupAjax()
- spyOn(@view, "searchFor")
- @view.$el.find(".fa-search").click()
- expect(@view.searchFor).toHaveBeenCalled()
-
- describe "username search", ->
-
- it "makes correct ajax calls", ->
- $.ajax.and.callFake(
- (params) =>
- expect(params.data.username).toEqual("testing-username")
- expect(params.url.path()).toEqual(DiscussionUtil.urlFor("users"))
- params.success(
- {users: []}, 'success'
- )
- {always: ->}
- )
- @view.searchForUser("testing-username")
- expect($.ajax).toHaveBeenCalled()
-
- setAjaxResults = (threadSuccess, userResult) ->
- # threadSuccess is a boolean indicating whether the thread search ajax call should succeed
- # userResult is the value that should be returned as data from the username search ajax call
- $.ajax.and.callFake(
- (params) =>
- if params.data.text and threadSuccess
- params.success(
- {discussion_data: [], page: 42, num_pages: 99, corrected_text: "dummy"},
- "success"
- )
- else if params.data.username
- params.success(
- {users: userResult},
- "success"
- )
- {always: ->}
- )
-
- it "gets called after a thread search succeeds", ->
- spyOn(@view, "searchForUser").and.callThrough()
- setAjaxResults(true, [])
- @view.searchFor("gizmo")
- expect(@view.searchForUser).toHaveBeenCalled()
- expect($.ajax.calls.mostRecent().args[0].data.username).toEqual("gizmo")
-
- it "does not get called after a thread search fails", ->
- spyOn(@view, "searchForUser").and.callThrough()
- setAjaxResults(false, [])
- @view.searchFor("gizmo")
- expect(@view.searchForUser).not.toHaveBeenCalled()
-
- it "adds a search alert when an username was matched", ->
- spyOn(@view, "addSearchAlert")
- setAjaxResults(true, [{username: "gizmo", id: "1"}])
- @view.searchForUser("dummy")
- expect($.ajax).toHaveBeenCalled()
- expect(@view.addSearchAlert).toHaveBeenCalled()
- expect(@view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/gizmo/)
-
- it "does not add a search alert when no username was matched", ->
- spyOn(@view, "addSearchAlert")
- setAjaxResults(true, [])
- @view.searchForUser("dummy")
- expect($.ajax).toHaveBeenCalled()
- expect(@view.addSearchAlert).not.toHaveBeenCalled()
-
- describe "post type renders correctly", ->
- it "for discussion", ->
- renderSingleThreadWithProps({thread_type: "discussion"})
- expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-comments")
- expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("discussion")
-
- it "for answered question", ->
- renderSingleThreadWithProps({thread_type: "question", endorsed: true})
- expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-check-square-o")
- expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("answered question")
-
- it "for unanswered question", ->
- renderSingleThreadWithProps({thread_type: "question", endorsed: false})
- expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-question")
- expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("unanswered question")
-
- describe "post labels render correctly", ->
- beforeEach ->
- @moderatorId = "42"
- @administratorId = "43"
- @communityTaId = "44"
- DiscussionUtil.loadRoles({
- "Moderator": [parseInt(@moderatorId)],
- "Administrator": [parseInt(@administratorId)],
- "Community TA": [parseInt(@communityTaId)],
- })
-
- it "for pinned", ->
- renderSingleThreadWithProps({pinned: true})
- expect($(".post-label-pinned").length).toEqual(1)
-
- it "for following", ->
- renderSingleThreadWithProps({subscribed: true})
- expect($(".post-label-following").length).toEqual(1)
-
- it "for moderator", ->
- renderSingleThreadWithProps({user_id: @moderatorId})
- expect($(".post-label-by-staff").length).toEqual(1)
-
- it "for administrator", ->
- renderSingleThreadWithProps({user_id: @administratorId})
- expect($(".post-label-by-staff").length).toEqual(1)
-
- it "for community TA", ->
- renderSingleThreadWithProps({user_id: @communityTaId})
- expect($(".post-label-by-community-ta").length).toEqual(1)
-
- it "when none should be present", ->
- renderSingleThreadWithProps({})
- expect($(".forum-nav-thread-labels").length).toEqual(0)
-
- describe "browse menu", ->
- afterEach ->
- # Remove handler added to make browse menu disappear
- $("body").unbind("click")
-
- expectBrowseMenuVisible = (isVisible) ->
- expect($(".forum-nav-browse-menu:visible").length).toEqual(if isVisible then 1 else 0)
- expect($(".forum-nav-thread-list-wrapper:visible").length).toEqual(if isVisible then 0 else 1)
-
- it "should not be visible by default", ->
- expectBrowseMenuVisible(false)
-
- it "should show when header button is clicked", ->
- $(".forum-nav-browse").click()
- expectBrowseMenuVisible(true)
-
- describe "when shown", ->
- beforeEach ->
- $(".forum-nav-browse").click()
-
- it "should hide when header button is clicked", ->
- $(".forum-nav-browse").click()
- expectBrowseMenuVisible(false)
-
- it "should hide when a click outside the menu occurs", ->
- $(".forum-nav-search-input").click()
- expectBrowseMenuVisible(false)
-
- it "should hide when a search is executed", ->
- setupAjax()
- $(".forum-nav-search-input").trigger($.Event("keydown", {which: 13}))
- expectBrowseMenuVisible(false)
-
- it "should hide when a category is clicked", ->
- $(".forum-nav-browse-title")[0].click()
- expectBrowseMenuVisible(false)
-
- it "should still be shown when filter input is clicked", ->
- $(".forum-nav-browse-filter-input").click()
- expectBrowseMenuVisible(true)
-
- describe "filtering", ->
- checkFilter = (filterText, expectedItems) ->
- $(".forum-nav-browse-filter-input").val(filterText).keyup()
- visibleItems = $(".forum-nav-browse-title:visible").map(
- (i, elem) -> $(elem).text()
- ).get()
- expect(visibleItems).toEqual(expectedItems)
-
- it "should be case-insensitive", ->
- checkFilter("other", ["Other Category"])
-
- it "should match partial words", ->
- checkFilter("ateg", ["Other Category"])
-
- it "should show ancestors and descendants of matches", ->
- checkFilter("Target", ["Parent", "Target", "Child"])
-
- it "should handle multiple words regardless of order", ->
- checkFilter("Following Posts", ["Posts I'm Following"])
-
- it "should handle multiple words in different depths", ->
- checkFilter("Parent Child", ["Parent", "Target", "Child"])
-
- describe "selecting an item", ->
- it "should clear the search box", ->
- setupAjax()
- $(".forum-nav-search-input").val("foobar")
- $(".forum-nav-browse-menu-following .forum-nav-browse-title").click()
- expect($(".forum-nav-search-input").val()).toEqual("")
-
- it "should change the button text", ->
- setupAjax()
- $(".forum-nav-browse-menu-following .forum-nav-browse-title").click()
- expect($(".forum-nav-browse-current").text()).toEqual("Posts I'm Following")
-
- it "should show/hide the cohort selector", ->
- DiscussionSpecHelper.makeModerator()
- @view.render()
- setupAjax()
- _.each(
- [
- {selector: ".forum-nav-browse-menu-all", cohortVisibility: true},
- {selector: ".forum-nav-browse-menu-following", cohortVisibility: false},
- {
- selector: ".forum-nav-browse-menu-item:has(.forum-nav-browse-menu-item .forum-nav-browse-menu-item)",
- cohortVisibility: false
- },
- {selector: "[data-discussion-id=child]", cohortVisibility: false},
- {selector: "[data-discussion-id=other]", cohortVisibility: true}
- ],
- (itemInfo) =>
- @view.$("#{itemInfo.selector} > .forum-nav-browse-title").click()
- expect(@view.$(".forum-nav-filter-cohort").is(":visible")).toEqual(itemInfo.cohortVisibility)
- )
-
- testSelectionRequest = (callback, itemText) ->
- setupAjax(callback)
- $(".forum-nav-browse-title:contains(#{itemText})").click()
- expect($.ajax).toHaveBeenCalled()
-
- it "should get all discussions", ->
- testSelectionRequest(
- (params) -> expect(params.url.path()).toEqual(DiscussionUtil.urlFor("threads")),
- "All"
- )
-
- it "should get followed threads", ->
- testSelectionRequest(
- (params) ->
- expect(params.url.path()).toEqual(
- DiscussionUtil.urlFor("followed_threads", window.user.id)
- )
- ,
- "Following"
- )
- expect($.ajax.calls.mostRecent().args[0].data.group_id).toBeUndefined();
-
- it "should get threads for the selected leaf", ->
- testSelectionRequest(
- (params) ->
- expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"))
- expect(params.data.commentable_ids).toEqual("child")
- ,
- "Child"
- )
-
- it "should get threads for children of the selected intermediate node", ->
- testSelectionRequest(
- (params) ->
- expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search"))
- expect(params.data.commentable_ids).toEqual("child,sibling")
- ,
- "Parent"
- )
diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee
deleted file mode 100644
index 48572f67ae..0000000000
--- a/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee
+++ /dev/null
@@ -1,110 +0,0 @@
-# -*- coding: utf-8 -*-
-describe "DiscussionThreadProfileView", ->
-
- beforeEach ->
- DiscussionSpecHelper.setUpGlobals()
- DiscussionSpecHelper.setUnderscoreFixtures()
- @threadData = {
- id: "1",
- body: "dummy body",
- discussion: new Discussion()
- abuse_flaggers: [],
- commentable_id: 'dummy_discussion',
- votes: {up_count: "42"},
- created_at: "2014-09-09T20:11:08Z"
- }
- @imageTag = '
'
- window.MathJax = { Hub: { Queue: -> } }
-
- makeView = (thread) ->
- view = new DiscussionThreadProfileView(model: thread)
- spyConvertMath(view)
- return view
-
- makeThread = (threadData) ->
- thread = new Thread(threadData)
- thread.discussion = new Discussion()
- return thread
-
- spyConvertMath = (view) ->
- spyOn(view, "convertMath").and.callFake( ->
- @model.set('markdownBody', @model.get('body'))
- )
-
- checkPostWithImages = (numberOfImages, truncatedText, threadData, imageTag) ->
- expectedHtml = '
' - threadData.body = '
' - testText = '' - expectedText = '' - - if truncatedText - testText = new Array(100).join('test ') - expectedText = testText.substring(0, 139)+ '…' - else - testText = 'Test body' - expectedText = 'Test body' - - for i in [0..numberOfImages-1] - threadData.body = threadData.body + imageTag - if i == 0 - expectedHtml = expectedHtml + imageTag - else - expectedHtml = expectedHtml + 'image omitted' - - threadData.body = threadData.body + '' + testText + '
' - if numberOfImages > 1 - expectedHtml = expectedHtml + '' + expectedText + 'Some images in this post have been omitted
' - else - expectedHtml = expectedHtml + '' + expectedText + '' - - view = makeView(makeThread(threadData)) - view.render() - expect(view.$el.find(".post-body").html()).toEqual(expectedHtml) - - checkBody = (truncated, view, threadData) -> - view.render() - if not truncated - expect(view.model.get("body")).toEqual(view.model.get("abbreviatedBody")) - expect(view.$el.find(".post-body").html()).toEqual(threadData.body) - else - expect(view.model.get("body")).not.toEqual(view.model.get("abbreviatedBody")) - expect(view.$el.find(".post-body").html()).not.toEqual(threadData.body) - outputHtmlStripped = view.$el.find(".post-body").html().replace(/(<([^>]+)>)/ig,""); - outputHtmlStripped = outputHtmlStripped.replace("Some images in this post have been omitted","") - outputHtmlStripped = outputHtmlStripped.replace("image omitted","") - inputHtmlStripped = threadData.body.replace(/(<([^>]+)>)/ig,""); - expectedOutput = inputHtmlStripped.substring(0, 139)+ '…' - expect(outputHtmlStripped).toEqual(expectedOutput) - expect(view.$el.find(".post-body").html().indexOf("…")).toBeGreaterThan(0) - - describe "Body markdown should be correct", -> - - it "untruncated text without markdown body", -> - @threadData.body = "Test body" - view = makeView(makeThread(@threadData)) - checkBody(false, view, @threadData) - - it "truncated text without markdown body", -> - @threadData.body = new Array(100).join("test ") - view = makeView(makeThread(@threadData)) - checkBody(true, view, @threadData) - - it "untruncated text with markdown body", -> - @threadData.body = '' + @imageTag + 'Google top search engine
' - view = makeView(makeThread(@threadData)) - checkBody(false, view, @threadData) - - it "truncated text with markdown body", -> - testText = new Array(100).join("test ") - @threadData.body = '' + @imageTag + @imageTag + '' + testText + '
' - view = makeView(makeThread(@threadData)) - checkBody(true, view, @threadData) - - for numImages in [1, 2, 10] - for truncatedText in [true, false] - it "body with #{numImages} images and #{if truncatedText then "truncated" else "untruncated"} text", -> - checkPostWithImages(numImages, truncatedText, @threadData, @imageTag) - - it "check the thread retrieve url", -> - thread = makeThread(@threadData) - expect(thread.urlFor('retrieve')).toBe('/courses/edX/999/test/discussion/forum/dummy_discussion/threads/1') diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee deleted file mode 100644 index 34036d847b..0000000000 --- a/common/static/coffee/spec/discussion/view/discussion_thread_show_view_spec.coffee +++ /dev/null @@ -1,159 +0,0 @@ -describe "DiscussionThreadShowView", -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - - @user = DiscussionUtil.getUser() - @threadData = { - id: "dummy", - user_id: @user.id, - username: @user.get('username'), - course_id: $$course_id, - title: "dummy title", - body: "this is a thread", - created_at: "2013-04-03T20:08:39Z", - abuse_flaggers: [], - votes: {up_count: 42}, - thread_type: "discussion", - closed: false, - pinned: false, - type: "thread" # TODO - silly that this needs to be explicitly set - } - @thread = new Thread(@threadData) - @view = new DiscussionThreadShowView({ model: @thread }) - @view.setElement($("#fixture-element")) - spyOn(@view, "convertMath") - - describe "voting", -> - - it "renders the vote state correctly", -> - DiscussionViewSpecHelper.checkRenderVote(@view, @thread) - - it "votes correctly via click", -> - DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("click")) - - it "votes correctly via spacebar", -> - DiscussionViewSpecHelper.checkUpvote(@view, @thread, @user, $.Event("keydown", {which: 32})) - - it "unvotes correctly via click", -> - DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("click")) - - it "unvotes correctly via spacebar", -> - DiscussionViewSpecHelper.checkUnvote(@view, @thread, @user, $.Event("keydown", {which: 32})) - - describe "pinning", -> - - expectPinnedRendered = (view, model) -> - pinned = model.get('pinned') - button = view.$el.find(".action-pin") - expect(button.hasClass("is-checked")).toBe(pinned) - expect(button.attr("aria-checked")).toEqual(pinned.toString()) - - it "renders the pinned state correctly", -> - @view.render() - expectPinnedRendered(@view, @thread) - @thread.set('pinned', false) - @view.render() - expectPinnedRendered(@view, @thread) - @thread.set('pinned', true) - @view.render() - expectPinnedRendered(@view, @thread) - - it "exposes the pinning control only to authorized users", -> - @thread.updateInfo({ability: {can_openclose: false}}) - @view.render() - expect(@view.$el.find(".action-pin").closest(".is-hidden")).toExist() - @thread.updateInfo({ability: {can_openclose: true}}) - @view.render() - expect(@view.$el.find(".action-pin").closest(".is-hidden")).not.toExist() - - it "handles events correctly", -> - @view.render() - DiscussionViewSpecHelper.checkButtonEvents(@view, "togglePin", ".action-pin") - - describe "labels", -> - - expectOneElement = (view, selector, visible=true) => - view.render() - elements = view.$el.find(selector) - expect(elements.length).toEqual(1) - if visible - expect(elements).not.toHaveClass("is-hidden") - else - expect(elements).toHaveClass("is-hidden") - - it 'displays the closed label when appropriate', -> - expectOneElement(@view, '.post-label-closed', false) - @thread.set('closed', true) - expectOneElement(@view, '.post-label-closed') - - it 'displays the pinned label when appropriate', -> - expectOneElement(@view, '.post-label-pinned', false) - @thread.set('pinned', true) - expectOneElement(@view, '.post-label-pinned') - - it 'displays the reported label when appropriate for a non-staff user', -> - expectOneElement(@view, '.post-label-reported', false) - # flagged by current user - should be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]) - expectOneElement(@view, '.post-label-reported') - # flagged by some other user but not the current one - should not be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]) - expectOneElement(@view, '.post-label-reported', false) - - it 'displays the reported label when appropriate for a flag moderator', -> - DiscussionSpecHelper.makeModerator() - expectOneElement(@view, '.post-label-reported', false) - # flagged by current user - should be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]) - expectOneElement(@view, '.post-label-reported') - # flagged by some other user but not the current one - should still be labelled - @thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]) - expectOneElement(@view, '.post-label-reported') - - describe "author display", -> - - beforeEach -> - @thread.set('user_url', 'test_user_url') - - checkUserLink = (element, is_ta, is_staff) -> - expect(element.find('a.username').length).toEqual(1) - expect(element.find('a.username').text()).toEqual('test_user') - expect(element.find('a.username').attr('href')).toEqual('test_user_url') - expect(element.find('.user-label-community-ta').length).toEqual(if is_ta then 1 else 0) - expect(element.find('.user-label-staff').length).toEqual(if is_staff then 1 else 0) - - it "renders correctly for a student-authored thread", -> - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - checkUserLink($el, false, false) - - it "renders correctly for a community TA-authored thread", -> - @thread.set('community_ta_authored', true) - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - checkUserLink($el, true, false) - - it "renders correctly for a staff-authored thread", -> - @thread.set('staff_authored', true) - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - checkUserLink($el, false, true) - - it "renders correctly for an anonymously-authored thread", -> - @thread.set('username', null) - $el = $('#fixture-element').html(@view.getAuthorDisplay()) - expect($el.find('a.username').length).toEqual(0) - expect($el.text()).toMatch(/^(\s*)anonymous(\s*)$/) - - describe "cohorting", -> - it "renders correctly for an uncohorted thread", -> - @view.render() - expect(@view.$('.group-visibility-label').text().trim()).toEqual( - 'This post is visible to everyone.' - ) - - it "renders correctly for a cohorted thread", -> - @thread.set('group_id', '1') - @thread.set('group_name', 'Mock Cohort') - @view.render() - expect(@view.$('.group-visibility-label').text().trim()).toEqual( - 'This post is visible only to Mock Cohort.' - ) diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee deleted file mode 100644 index 53904d9b87..0000000000 --- a/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee +++ /dev/null @@ -1,410 +0,0 @@ -describe "DiscussionThreadView", -> - beforeEach -> - DiscussionSpecHelper.setUpGlobals() - DiscussionSpecHelper.setUnderscoreFixtures() - - jasmine.clock().install() - @threadData = DiscussionViewSpecHelper.makeThreadWithProps({}) - @thread = new Thread(@threadData) - @discussion = new Discussion(@thread) - deferred = $.Deferred(); - spyOn($, "ajax").and.returnValue(deferred); - # Avoid unnecessary boilerplate - spyOn(DiscussionThreadShowView.prototype, "convertMath") - spyOn(DiscussionContentView.prototype, "makeWmdEditor") - spyOn(DiscussionUtil, "makeWmdEditor") - spyOn(DiscussionUtil, "setWmdContent") - spyOn(ThreadResponseShowView.prototype, "convertMath") - - afterEach -> - $.ajax.calls.reset() - jasmine.clock().uninstall() - - renderWithContent = (view, content) -> - $.ajax.and.callFake((params) => - params.success( - createAjaxResponseJson(content, false), - 'success' - ) - {always: ->} - ) - view.render() - jasmine.clock().tick(100) - - renderWithTestResponses = (view, count, options) -> - renderWithContent( - view, - _.extend( - { - resp_total: count, - children: if count > 0 then (createTestResponseJson(index) for index in [1..count]) else [] - }, - options - ) - ) - - createTestResponseJson = (index) -> - { - user_id: window.user.id, - body: "Response " + index, - id: "id_" + index, - created_at: "2015-01-01T22:20:28Z" - } - - assertContentVisible = (view, selector, visible) -> - content = view.$el.find(selector) - expect(content.length).toBeGreaterThan(0) - content.each (i, elem) -> - expect($(elem).is(":visible")).toEqual(visible) - - assertExpandedContentVisible = (view, expanded) -> - expect(view.$el.hasClass("expanded")).toEqual(expanded) - assertContentVisible(view, ".post-extended-content", expanded) - assertContentVisible(view, ".forum-thread-expand", not expanded) - assertContentVisible(view, ".forum-thread-collapse", expanded) - - assertResponseCountAndPaginationCorrect = (view, countText, displayCountText, buttonText) -> - expect(view.$el.find(".response-count").text()).toEqual(countText) - if displayCountText - expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText) - else - expect(view.$el.find(".response-display-count").length).toEqual(0) - if buttonText - expect(view.$el.find(".load-response-button").text()).toEqual(buttonText) - else - expect(view.$el.find(".load-response-button").length).toEqual(0) - - createAjaxResponseJson = (content, can_act) -> - { - content: content, - annotated_content_info: { - ability: { - editable: can_act, - can_delete: can_act, - can_reply: can_act, - can_vote: can_act - } - } - } - - postResponse = (view, index) -> - testResponseJson = createTestResponseJson(index) - responseText = testResponseJson.body - spyOn(view, "getWmdContent").and.returnValue(responseText) - $.ajax.and.callFake((params) => - expect(params.type).toEqual("POST") - expect(params.data.body).toEqual(responseText) - params.success( - createAjaxResponseJson(testResponseJson, true), - 'success' - ) - {always: ->} - ) - view.$(".discussion-submit-post").click() - - describe "closed and open Threads", -> - - createDiscussionThreadView = (originallyClosed, mode) -> - threadData = DiscussionViewSpecHelper.makeThreadWithProps({closed: originallyClosed}) - thread = new Thread(threadData) - discussion = new Discussion(thread) - view = new DiscussionThreadView( - model: thread - el: $("#fixture-element") - mode: mode - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - renderWithTestResponses(view, 1) - if mode == "inline" - view.expand() - spyOn(DiscussionUtil, "updateWithUndo").and.callFake( - (model, updates, safeAjaxParams, errorMsg) -> - model.set(updates) - ) - view - - checkCommentForm = (originallyClosed, mode) -> - view = createDiscussionThreadView(originallyClosed, mode) - expect(view.$('.comment-form').closest('li').is(":visible")).toBe(not originallyClosed) - expect(view.$(".discussion-reply-new").is(":visible")).toBe(not originallyClosed) - view.$(".action-close").click() - expect(view.$('.comment-form').closest('li').is(":visible")).toBe(originallyClosed) - expect(view.$(".discussion-reply-new").is(":visible")).toBe(originallyClosed) - - checkVoteDisplay = (originallyClosed, mode) -> - view = createDiscussionThreadView(originallyClosed, mode) - expect(view.$('.thread-main-wrapper .action-vote').is(":visible")).toBe(not originallyClosed) - expect(view.$('.thread-main-wrapper .display-vote').is(":visible")).toBe(originallyClosed) - view.$(".action-close").click() - expect(view.$('.action-vote').is(":visible")).toBe(originallyClosed) - expect(view.$('.display-vote').is(":visible")).toBe(not originallyClosed) - - _.each(["tab", "inline"], (mode) => - it "Test that in #{mode} mode when a closed thread is opened the comment form is displayed", -> - checkCommentForm(true, mode) - - it "Test that in #{mode} mode when a open thread is closed the comment form is hidden", -> - checkCommentForm(false, mode) - - it "Test that in #{mode} mode when a closed thread is opened the vote button is displayed and vote count is hidden", -> - checkVoteDisplay(true, mode) - - it "Test that in #{mode} mode when a open thread is closed the vote button is hidden and vote count is displayed", -> - checkVoteDisplay(false, mode) - ) - - describe "tab mode", -> - beforeEach -> - @view = new DiscussionThreadView( - model: @thread - el: $("#fixture-element") - mode: "tab" - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - - describe "responses", -> - it "can post a first response", -> - # Initially render a test post (made by someone else) with zero responses - renderWithTestResponses(@view, 0) - postResponse(@view, 1) - expect(@view.$(".forum-response").length).toBe(1) - # At this point, there are 2 DiscussionContentViews, the main post and the response. - # Each an .action-edit button, but only 1 (the response) should be available. - expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1) - expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(1) - - it "can post a second response", -> - # Initially render a test post (made by someone else) with a single response (made by the current learner) - renderWithTestResponses(@view, 1) - expect(@view.$(".forum-response").length).toBe(1) - # Post should not be editable, response should be - expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1) - expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(1) - - # Now make a second response. Prior to TNL-3788, a bug would hide the edit button for the first response - postResponse(@view, 2) - expect(@view.$(".forum-response").length).toBe(2) - # Post should not be editable, responses should be - expect(@view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1) - expect(@view.$(".response-actions-list").find(".action-edit").parent().not(".is-hidden").length).toBe(2) - - describe "response count and pagination", -> - it "correctly render for a thread with no responses", -> - renderWithTestResponses(@view, 0) - assertResponseCountAndPaginationCorrect(@view, "0 responses", null, null) - - it "correctly render for a thread with one response", -> - renderWithTestResponses(@view, 1) - assertResponseCountAndPaginationCorrect(@view, "1 response", "Showing all responses", null) - - it "correctly render for a thread with one additional page", -> - renderWithTestResponses(@view, 1, {resp_total: 2}) - assertResponseCountAndPaginationCorrect(@view, "2 responses", "Showing first response", "Load all responses") - - it "correctly render for a thread with multiple additional pages", -> - renderWithTestResponses(@view, 2, {resp_total: 111}) - assertResponseCountAndPaginationCorrect(@view, "111 responses", "Showing first 2 responses", "Load next 100 responses") - - describe "on clicking the load more button", -> - beforeEach -> - renderWithTestResponses(@view, 1, {resp_total: 5}) - assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing first response", "Load all responses") - - it "correctly re-render when all threads have loaded", -> - renderWithTestResponses(@view, 5, {resp_total: 5}) - @view.$el.find(".load-response-button").click() - assertResponseCountAndPaginationCorrect(@view, "5 responses", "Showing all responses", null) - - it "correctly re-render when one page remains", -> - renderWithTestResponses(@view, 3, {resp_total: 42}) - @view.$el.find(".load-response-button").click() - assertResponseCountAndPaginationCorrect(@view, "42 responses", "Showing first 3 responses", "Load all responses") - - it "correctly re-render when multiple pages remain", -> - renderWithTestResponses(@view, 3, {resp_total: 111}) - @view.$el.find(".load-response-button").click() - assertResponseCountAndPaginationCorrect(@view, "111 responses", "Showing first 3 responses", "Load next 100 responses") - - describe "inline mode", -> - beforeEach -> - @view = new DiscussionThreadView( - model: @thread - el: $("#fixture-element") - mode: "inline" - course_settings: DiscussionSpecHelper.makeCourseSettings() - ) - - describe "render", -> - it "shows content that should be visible when collapsed", -> - @view.render() - assertExpandedContentVisible(@view, false) - - it "does not render any responses by default", -> - @view.render() - expect($.ajax).not.toHaveBeenCalled() - expect(@view.$el.find(".responses li").length).toEqual(0) - - describe "focus", -> - it "sends focus to the conversation when opened", (done) -> - DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []}) - @view.render() - @view.expand() - self = @ - jasmine.waitUntil(-> - # This is the implementation of "toBeFocused". However, simply calling that method - # with no wait seems to be flaky. - article = self.view.$el.find('.discussion-article') - return article[0] == article[0].ownerDocument.activeElement - ).then -> - done() - - describe "expand/collapse", -> - it "shows/hides appropriate content", -> - DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []}) - @view.render() - @view.expand() - assertExpandedContentVisible(@view, true) - @view.collapse() - assertExpandedContentVisible(@view, false) - - it "switches between the abbreviated and full body", -> - DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []}) - longBody = new Array(100).join("test ") - expectedAbbreviation = DiscussionUtil.abbreviateString(longBody, 140) - @thread.set("body", longBody) - - @view.render() - expect($(".post-body").text()).toEqual(expectedAbbreviation) - expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled() - DiscussionThreadShowView.prototype.convertMath.calls.reset() - - @view.expand() - expect($(".post-body").text()).toEqual(longBody) - expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled() - DiscussionThreadShowView.prototype.convertMath.calls.reset() - - @view.collapse() - expect($(".post-body").text()).toEqual(expectedAbbreviation) - expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled() - - it "strips script tags appropriately", -> - DiscussionViewSpecHelper.setNextResponseContent({resp_total: 0, children: []}) - longMaliciousBody = new Array(100).join("\n") - @thread.set("body", longMaliciousBody) - maliciousAbbreviation = DiscussionUtil.abbreviateString(@thread.get('body'), 140) - - # The nodes' html should be different than the strings, but - # their texts should be the same, indicating that they've been - # properly escaped. To be safe, make sure the string ""); + /* jshint +W101 */ + this.threads = [ + DiscussionViewSpecHelper.makeThreadWithProps({ + id: "1", + title: "Thread1", + votes: { + up_count: '20' + }, + pinned: true, + comments_count: 1, + created_at: '2013-04-03T20:08:39Z' + }), DiscussionViewSpecHelper.makeThreadWithProps({ + id: "2", + title: "Thread2", + votes: { + up_count: '42' + }, + comments_count: 2, + created_at: '2013-04-03T20:07:39Z' + }), DiscussionViewSpecHelper.makeThreadWithProps({ + id: "3", + title: "Thread3", + votes: { + up_count: '12' + }, + comments_count: 3, + created_at: '2013-04-03T20:06:39Z' + }), DiscussionViewSpecHelper.makeThreadWithProps({ + id: "4", + title: "Thread4", + votes: { + up_count: '25' + }, + comments_count: 0, + pinned: true, + created_at: '2013-04-03T20:05:39Z' + }) + ]; + deferred = $.Deferred(); + spyOn($, "ajax").and.returnValue(deferred); + this.discussion = new Discussion([]); + this.view = new DiscussionThreadListView({ + collection: this.discussion, + el: $("#fixture-element"), + courseSettings: new DiscussionCourseSettings({ + is_cohorted: true + }) + }); + return this.view.render(); + }); + setupAjax = function(callback) { + return $.ajax.and.callFake(function(params) { + if (callback) { + callback(params); + } + params.success({ + discussion_data: [], + page: 1, + num_pages: 1 + }); + return { + always: function() { + } + }; + }); + }; + renderSingleThreadWithProps = function(props) { + return makeView(new Discussion([new Thread(DiscussionViewSpecHelper.makeThreadWithProps(props))])).render(); + }; + makeView = function(discussion) { + return new DiscussionThreadListView({ + el: $("#fixture-element"), + collection: discussion, + courseSettings: new DiscussionCourseSettings({ + is_cohorted: true + }) + }); + }; + expectFilter = function(filterVal) { + return $.ajax.and.callFake(function(params) { + _.each(["unread", "unanswered", "flagged"], function(paramName) { + if (paramName === filterVal) { + return expect(params.data[paramName]).toEqual(true); + } else { + return expect(params.data[paramName]).toBeUndefined(); + } + }); + return { + always: function() { + } + }; + }); + }; + + describe("should filter correctly", function() { + return _.each(["all", "unread", "unanswered", "flagged"], function(filterVal) { + it("for " + filterVal, function() { + expectFilter(filterVal); + this.view.$(".forum-nav-filter-main-control").val(filterVal).change(); + return expect($.ajax).toHaveBeenCalled(); + }); + }); + }); + + describe("cohort selector", function() { + it("should not be visible to students", function() { + return expect(this.view.$(".forum-nav-filter-cohort-control:visible")).not.toExist(); + }); + it("should allow moderators to select visibility", function() { + var expectedGroupId, + self = this; + DiscussionSpecHelper.makeModerator(); + this.view.render(); + expectedGroupId = null; + setupAjax(function(params) { + return expect(params.data.group_id).toEqual(expectedGroupId); + }); + return _.each([ + { + val: "", + expectedGroupId: void 0 + }, { + val: "1", + expectedGroupId: "1" + }, { + val: "2", + expectedGroupId: "2" + } + ], function(optionInfo) { + expectedGroupId = optionInfo.expectedGroupId; + self.view.$(".forum-nav-filter-cohort-control").val(optionInfo.val).change(); + expect($.ajax).toHaveBeenCalled(); + return $.ajax.calls.reset(); + }); + }); + }); + + it("search should clear filter", function() { + expectFilter(null); + this.view.$(".forum-nav-filter-main-control").val("flagged"); + this.view.searchFor("foobar"); + return expect(this.view.$(".forum-nav-filter-main-control").val()).toEqual("all"); + }); + + checkThreadsOrdering = function(view, sort_order, type) { + expect(view.$el.find(".forum-nav-thread").children().length).toEqual(4); + expect(view.$el.find(".forum-nav-thread:nth-child(1) .forum-nav-thread-title").text()) + .toEqual(sort_order[0]); + expect(view.$el.find(".forum-nav-thread:nth-child(2) .forum-nav-thread-title").text()) + .toEqual(sort_order[1]); + expect(view.$el.find(".forum-nav-thread:nth-child(3) .forum-nav-thread-title").text()) + .toEqual(sort_order[2]); + expect(view.$el.find(".forum-nav-thread:nth-child(4) .forum-nav-thread-title").text()) + .toEqual(sort_order[3]); + return expect(view.$el.find(".forum-nav-sort-control").val()).toEqual(type); + }; + + describe("thread rendering should be correct", function() { + var checkRender; + checkRender = function(threads, type, sort_order) { + var discussion, view; + discussion = new Discussion(_.map(threads, function(thread) { + return new Thread(thread); + }), { + pages: 1, + sort: type + }); + view = makeView(discussion); + view.render(); + checkThreadsOrdering(view, sort_order, type); + expect(view.$el.find(".forum-nav-thread-comments-count:visible").length) + .toEqual(type === "votes" ? 0 : 4); + expect(view.$el.find(".forum-nav-thread-votes-count:visible").length) + .toEqual(type === "votes" ? 4 : 0); + if (type === "votes") { + return expect(_.map(view.$el.find(".forum-nav-thread-votes-count"), function(element) { + return $(element).text().trim(); + })).toEqual(["+25 votes", "+20 votes", "+42 votes", "+12 votes"]); + } + }; + + it("with sort preference activity", function() { + return checkRender(this.threads, "activity", ["Thread1", "Thread2", "Thread3", "Thread4"]); + }); + + it("with sort preference votes", function() { + return checkRender(this.threads, "votes", ["Thread4", "Thread1", "Thread2", "Thread3"]); + }); + + it("with sort preference comments", function() { + return checkRender(this.threads, "comments", ["Thread1", "Thread4", "Thread3", "Thread2"]); + }); + }); + + describe("Sort change should be correct", function() { + var changeSorting; + changeSorting = function(threads, selected_type, new_type, sort_order) { + var discussion, sortControl, sorted_threads, view; + discussion = new Discussion(_.map(threads, function(thread) { + return new Thread(thread); + }), { + pages: 1, + sort: selected_type + }); + view = makeView(discussion); + view.render(); + sortControl = view.$el.find(".forum-nav-sort-control"); + expect(sortControl.val()).toEqual(selected_type); + sorted_threads = []; + if (new_type === 'activity') { + sorted_threads = [threads[0], threads[3], threads[1], threads[2]]; + } else if (new_type === 'comments') { + sorted_threads = [threads[0], threads[3], threads[2], threads[1]]; + } else if (new_type === 'votes') { + sorted_threads = [threads[3], threads[0], threads[1], threads[2]]; + } + $.ajax.and.callFake(function(params) { + params.success({ + "discussion_data": sorted_threads, + page: 1, + num_pages: 1 + }); + return { + always: function() { + } + }; + }); + sortControl.val(new_type).change(); + expect($.ajax).toHaveBeenCalled(); + checkThreadsOrdering(view, sort_order, new_type); + }; + + it("with sort preference activity", function() { + changeSorting( + this.threads, "comments", "activity", ["Thread1", "Thread4", "Thread3", "Thread2"] + ); + }); + + it("with sort preference votes", function() { + changeSorting(this.threads, "activity", "votes", ["Thread4", "Thread1", "Thread2", "Thread3"]); + }); + + it("with sort preference comments", function() { + changeSorting(this.threads, "votes", "comments", ["Thread1", "Thread4", "Thread3", "Thread2"]); + }); + }); + describe("search alerts", function() { + var testAlertMessages; + + testAlertMessages = function(expectedMessages) { + return expect($(".search-alert .message").map(function() { + return $(this).html(); + }).get()).toEqual(expectedMessages); + }; + + it("renders and removes search alerts", function() { + var bar, foo; + testAlertMessages([]); + foo = this.view.addSearchAlert("foo"); + testAlertMessages(["foo"]); + bar = this.view.addSearchAlert("bar"); + testAlertMessages(["foo", "bar"]); + this.view.removeSearchAlert(foo.cid); + testAlertMessages(["bar"]); + this.view.removeSearchAlert(bar.cid); + return testAlertMessages([]); + }); + + it("clears all search alerts", function() { + this.view.addSearchAlert("foo"); + this.view.addSearchAlert("bar"); + this.view.addSearchAlert("baz"); + testAlertMessages(["foo", "bar", "baz"]); + this.view.clearSearchAlerts(); + return testAlertMessages([]); + }); + }); + + describe("search spell correction", function() { + var testCorrection; + + beforeEach(function() { + return spyOn(this.view, "searchForUser"); + }); + + testCorrection = function(view, correctedText) { + spyOn(view, "addSearchAlert"); + $.ajax.and.callFake(function(params) { + params.success({ + discussion_data: [], + page: 42, + num_pages: 99, + corrected_text: correctedText + }, 'success'); + return { + always: function() { + } + }; + }); + view.searchFor("dummy"); + return expect($.ajax).toHaveBeenCalled(); + }; + + it("adds a search alert when an alternate term was searched", function() { + testCorrection(this.view, "foo"); + expect(this.view.addSearchAlert.calls.count()).toEqual(1); + return expect(this.view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/foo/); + }); + + it("does not add a search alert when no alternate term was searched", function() { + testCorrection(this.view, null); + expect(this.view.addSearchAlert.calls.count()).toEqual(1); + return expect(this.view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/no threads matched/i); + }); + + it("clears search alerts when a new search is performed", function() { + spyOn(this.view, "clearSearchAlerts"); + spyOn(DiscussionUtil, "safeAjax"); + this.view.searchFor("dummy"); + return expect(this.view.clearSearchAlerts).toHaveBeenCalled(); + }); + + it("clears search alerts when the underlying collection changes", function() { + spyOn(this.view, "clearSearchAlerts"); + spyOn(this.view, "renderThread"); + this.view.collection.trigger("change", new Thread({ + id: 1 + })); + return expect(this.view.clearSearchAlerts).toHaveBeenCalled(); + }); + }); + + describe("Search events", function() { + it("perform search when enter pressed inside search textfield", function() { + setupAjax(); + spyOn(this.view, "searchFor"); + this.view.$el.find(".forum-nav-search-input").trigger($.Event("keydown", { + which: 13 + })); + return expect(this.view.searchFor).toHaveBeenCalled(); + }); + + it("perform search when search icon is clicked", function() { + setupAjax(); + spyOn(this.view, "searchFor"); + this.view.$el.find(".fa-search").click(); + return expect(this.view.searchFor).toHaveBeenCalled(); + }); + }); + + describe("username search", function() { + var setAjaxResults; + + it("makes correct ajax calls", function() { + $.ajax.and.callFake(function(params) { + expect(params.data.username).toEqual("testing-username"); + expect(params.url.path()).toEqual(DiscussionUtil.urlFor("users")); + params.success({ + users: [] + }, 'success'); + return { + always: function() { + } + }; + }); + this.view.searchForUser("testing-username"); + return expect($.ajax).toHaveBeenCalled(); + }); + + setAjaxResults = function(threadSuccess, userResult) { + return $.ajax.and.callFake(function(params) { + if (params.data.text && threadSuccess) { + params.success({ + discussion_data: [], + page: 42, + num_pages: 99, + corrected_text: "dummy" + }, "success"); + } else if (params.data.username) { + params.success({ + users: userResult + }, "success"); + } + return { + always: function() { + } + }; + }); + }; + + it("gets called after a thread search succeeds", function() { + spyOn(this.view, "searchForUser").and.callThrough(); + setAjaxResults(true, []); + this.view.searchFor("gizmo"); + expect(this.view.searchForUser).toHaveBeenCalled(); + return expect($.ajax.calls.mostRecent().args[0].data.username).toEqual("gizmo"); + }); + + it("does not get called after a thread search fails", function() { + spyOn(this.view, "searchForUser").and.callThrough(); + setAjaxResults(false, []); + this.view.searchFor("gizmo"); + return expect(this.view.searchForUser).not.toHaveBeenCalled(); + }); + + it("adds a search alert when an username was matched", function() { + spyOn(this.view, "addSearchAlert"); + setAjaxResults(true, [ + { + username: "gizmo", + id: "1" + } + ]); + this.view.searchForUser("dummy"); + expect($.ajax).toHaveBeenCalled(); + expect(this.view.addSearchAlert).toHaveBeenCalled(); + return expect(this.view.addSearchAlert.calls.mostRecent().args[0]).toMatch(/gizmo/); + }); + + it("does not add a search alert when no username was matched", function() { + spyOn(this.view, "addSearchAlert"); + setAjaxResults(true, []); + this.view.searchForUser("dummy"); + expect($.ajax).toHaveBeenCalled(); + return expect(this.view.addSearchAlert).not.toHaveBeenCalled(); + }); + }); + + describe("post type renders correctly", function() { + it("for discussion", function() { + renderSingleThreadWithProps({ + thread_type: "discussion" + }); + expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-comments"); + return expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("discussion"); + }); + + it("for answered question", function() { + renderSingleThreadWithProps({ + thread_type: "question", + endorsed: true + }); + expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-check-square-o"); + return expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("answered question"); + }); + + it("for unanswered question", function() { + renderSingleThreadWithProps({ + thread_type: "question", + endorsed: false + }); + expect($(".forum-nav-thread-wrapper-0 .icon")).toHaveClass("fa-question"); + return expect($(".forum-nav-thread-wrapper-0 .sr")).toHaveText("unanswered question"); + }); + }); + + describe("post labels render correctly", function() { + beforeEach(function() { + this.moderatorId = "42"; + this.administratorId = "43"; + this.communityTaId = "44"; + return DiscussionUtil.loadRoles({ + "Moderator": [parseInt(this.moderatorId)], + "Administrator": [parseInt(this.administratorId)], + "Community TA": [parseInt(this.communityTaId)] + }); + }); + + it("for pinned", function() { + renderSingleThreadWithProps({ + pinned: true + }); + return expect($(".post-label-pinned").length).toEqual(1); + }); + + it("for following", function() { + renderSingleThreadWithProps({ + subscribed: true + }); + return expect($(".post-label-following").length).toEqual(1); + }); + + it("for moderator", function() { + renderSingleThreadWithProps({ + user_id: this.moderatorId + }); + return expect($(".post-label-by-staff").length).toEqual(1); + }); + + it("for administrator", function() { + renderSingleThreadWithProps({ + user_id: this.administratorId + }); + return expect($(".post-label-by-staff").length).toEqual(1); + }); + + it("for community TA", function() { + renderSingleThreadWithProps({ + user_id: this.communityTaId + }); + return expect($(".post-label-by-community-ta").length).toEqual(1); + }); + + it("when none should be present", function() { + renderSingleThreadWithProps({}); + return expect($(".forum-nav-thread-labels").length).toEqual(0); + }); + }); + + describe("browse menu", function() { + var expectBrowseMenuVisible; + afterEach(function() { + return $("body").unbind("click"); + }); + + expectBrowseMenuVisible = function(isVisible) { + expect($(".forum-nav-browse-menu:visible").length).toEqual(isVisible ? 1 : 0); + return expect($(".forum-nav-thread-list-wrapper:visible").length).toEqual(isVisible ? 0 : 1); + }; + + it("should not be visible by default", function() { + return expectBrowseMenuVisible(false); + }); + + it("should show when header button is clicked", function() { + $(".forum-nav-browse").click(); + return expectBrowseMenuVisible(true); + }); + + describe("when shown", function() { + beforeEach(function() { + return $(".forum-nav-browse").click(); + }); + + it("should hide when header button is clicked", function() { + $(".forum-nav-browse").click(); + return expectBrowseMenuVisible(false); + }); + + it("should hide when a click outside the menu occurs", function() { + $(".forum-nav-search-input").click(); + return expectBrowseMenuVisible(false); + }); + + it("should hide when a search is executed", function() { + setupAjax(); + $(".forum-nav-search-input").trigger($.Event("keydown", { + which: 13 + })); + return expectBrowseMenuVisible(false); + }); + + it("should hide when a category is clicked", function() { + $(".forum-nav-browse-title")[0].click(); + return expectBrowseMenuVisible(false); + }); + + it("should still be shown when filter input is clicked", function() { + $(".forum-nav-browse-filter-input").click(); + return expectBrowseMenuVisible(true); + }); + + describe("filtering", function() { + var checkFilter; + checkFilter = function(filterText, expectedItems) { + var visibleItems; + $(".forum-nav-browse-filter-input").val(filterText).keyup(); + visibleItems = $(".forum-nav-browse-title:visible").map(function(i, elem) { + return $(elem).text(); + }).get(); + return expect(visibleItems).toEqual(expectedItems); + }; + + it("should be case-insensitive", function() { + return checkFilter("other", ["Other Category"]); + }); + + it("should match partial words", function() { + return checkFilter("ateg", ["Other Category"]); + }); + + it("should show ancestors and descendants of matches", function() { + return checkFilter("Target", ["Parent", "Target", "Child"]); + }); + + it("should handle multiple words regardless of order", function() { + return checkFilter("Following Posts", ["Posts I'm Following"]); + }); + + it("should handle multiple words in different depths", function() { + return checkFilter("Parent Child", ["Parent", "Target", "Child"]); + }); + }); + }); + + describe("selecting an item", function() { + var testSelectionRequest; + + it("should clear the search box", function() { + setupAjax(); + $(".forum-nav-search-input").val("foobar"); + $(".forum-nav-browse-menu-following .forum-nav-browse-title").click(); + return expect($(".forum-nav-search-input").val()).toEqual(""); + }); + + it("should change the button text", function() { + setupAjax(); + $(".forum-nav-browse-menu-following .forum-nav-browse-title").click(); + return expect($(".forum-nav-browse-current").text()).toEqual("Posts I'm Following"); + }); + + it("should show/hide the cohort selector", function() { + var self = this; + DiscussionSpecHelper.makeModerator(); + this.view.render(); + setupAjax(); + return _.each([ + { + selector: ".forum-nav-browse-menu-all", + cohortVisibility: true + }, { + selector: ".forum-nav-browse-menu-following", + cohortVisibility: false + }, { + selector: ".forum-nav-browse-menu-item:" + + "has(.forum-nav-browse-menu-item .forum-nav-browse-menu-item)", + cohortVisibility: false + }, { + selector: "[data-discussion-id=child]", + cohortVisibility: false + }, { + selector: "[data-discussion-id=other]", + cohortVisibility: true + } + ], function(itemInfo) { + self.view.$("" + itemInfo.selector + " > .forum-nav-browse-title").click(); + return expect(self.view.$(".forum-nav-filter-cohort").is(":visible")) + .toEqual(itemInfo.cohortVisibility); + }); + }); + + testSelectionRequest = function(callback, itemText) { + setupAjax(callback); + $(".forum-nav-browse-title:contains(" + itemText + ")").click(); + return expect($.ajax).toHaveBeenCalled(); + }; + + it("should get all discussions", function() { + return testSelectionRequest(function(params) { + return expect(params.url.path()).toEqual(DiscussionUtil.urlFor("threads")); + }, "All"); + }); + + it("should get followed threads", function() { + testSelectionRequest(function(params) { + return expect(params.url.path()) + .toEqual(DiscussionUtil.urlFor("followed_threads", window.user.id)); + }, "Following"); + return expect($.ajax.calls.mostRecent().args[0].data.group_id).toBeUndefined(); + }); + + it("should get threads for the selected leaf", function() { + return testSelectionRequest(function(params) { + expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search")); + return expect(params.data.commentable_ids).toEqual("child"); + }, "Child"); + }); + + it("should get threads for children of the selected intermediate node", function() { + return testSelectionRequest(function(params) { + expect(params.url.path()).toEqual(DiscussionUtil.urlFor("search")); + return expect(params.data.commentable_ids).toEqual("child,sibling"); + }, "Parent"); + }); + }); + }); + }); +}).call(this); diff --git a/common/static/common/js/spec/discussion/view/discussion_thread_profile_view_spec.js b/common/static/common/js/spec/discussion/view/discussion_thread_profile_view_spec.js new file mode 100644 index 0000000000..1f72753efd --- /dev/null +++ b/common/static/common/js/spec/discussion/view/discussion_thread_profile_view_spec.js @@ -0,0 +1,156 @@ +/* globals Discussion, DiscussionSpecHelper, DiscussionThreadProfileView, Thread */ +(function() { + 'use strict'; + describe("DiscussionThreadProfileView", function() { + var checkBody, checkPostWithImages, makeThread, makeView, spyConvertMath; + beforeEach(function() { + DiscussionSpecHelper.setUpGlobals(); + DiscussionSpecHelper.setUnderscoreFixtures(); + this.threadData = { + id: "1", + body: "dummy body", + discussion: new Discussion(), + abuse_flaggers: [], + commentable_id: 'dummy_discussion', + votes: { + up_count: "42" + }, + created_at: "2014-09-09T20:11:08Z" + }; + this.imageTag = '
';
+ window.MathJax = {
+ Hub: {
+ Queue: function() {
+ }
+ }
+ };
+ });
+ makeView = function(thread) {
+ var view;
+ view = new DiscussionThreadProfileView({
+ model: thread
+ });
+ spyConvertMath(view);
+ return view;
+ };
+ makeThread = function(threadData) {
+ var thread;
+ thread = new Thread(threadData);
+ thread.discussion = new Discussion();
+ return thread;
+ };
+ spyConvertMath = function(view) {
+ return spyOn(view, "convertMath").and.callFake(function() {
+ return this.model.set('markdownBody', this.model.get('body'));
+ });
+ };
+ checkPostWithImages = function(numberOfImages, truncatedText, threadData, imageTag) {
+ var expectedHtml, expectedText, i, testText, view, _i, _ref;
+ expectedHtml = ''; + threadData.body = '
'; + testText = ''; + expectedText = ''; + if (truncatedText) { + testText = new Array(100).join('test '); + expectedText = testText.substring(0, 139) + '…'; + } else { + testText = 'Test body'; + expectedText = 'Test body'; + // I really have no idea what it is supposed to mean - probably just iteration, but better be safe + for ( + i = _i = 0, _ref = numberOfImages - 1; + 0 <= _ref ? _i <= _ref : _i >= _ref; + i = 0 <= _ref ? ++_i : --_i + ) { + threadData.body = threadData.body + imageTag; + if (i === 0) { + expectedHtml = expectedHtml + imageTag; + } else { + expectedHtml = expectedHtml + 'image omitted'; + } + } + } + threadData.body = threadData.body + '' + testText + '
'; + if (numberOfImages > 1) { + expectedHtml = expectedHtml + '' + expectedText + + 'Some images in this post have been omitted
'; + } else { + expectedHtml = expectedHtml + '' + expectedText + ''; + } + view = makeView(makeThread(threadData)); + view.render(); + return expect(view.$el.find(".post-body").html()).toEqual(expectedHtml); + }; + checkBody = function(truncated, view, threadData) { + var expectedOutput, inputHtmlStripped, outputHtmlStripped; + view.render(); + if (!truncated) { + expect(view.model.get("body")).toEqual(view.model.get("abbreviatedBody")); + return expect(view.$el.find(".post-body").html()).toEqual(threadData.body); + } else { + expect(view.model.get("body")).not.toEqual(view.model.get("abbreviatedBody")); + expect(view.$el.find(".post-body").html()).not.toEqual(threadData.body); + outputHtmlStripped = view.$el.find(".post-body").html().replace(/(<([^>]+)>)/ig, ""); + outputHtmlStripped = outputHtmlStripped.replace("Some images in this post have been omitted", ""); + outputHtmlStripped = outputHtmlStripped.replace("image omitted", ""); + inputHtmlStripped = threadData.body.replace(/(<([^>]+)>)/ig, ""); + expectedOutput = inputHtmlStripped.substring(0, 139) + '…'; + expect(outputHtmlStripped).toEqual(expectedOutput); + return expect(view.$el.find(".post-body").html().indexOf("…")).toBeGreaterThan(0); + } + }; + describe("Body markdown should be correct", function() { + var numImages, truncatedText, _i, _j, _len, _len1, _ref, _ref1; + it("untruncated text without markdown body", function() { + var view; + this.threadData.body = "Test body"; + view = makeView(makeThread(this.threadData)); + return checkBody(false, view, this.threadData); + }); + it("truncated text without markdown body", function() { + var view; + this.threadData.body = new Array(100).join("test "); + view = makeView(makeThread(this.threadData)); + return checkBody(true, view, this.threadData); + }); + it("untruncated text with markdown body", function() { + var view; + this.threadData.body = '' + this.imageTag + 'Google top search engine
'; + view = makeView(makeThread(this.threadData)); + return checkBody(false, view, this.threadData); + }); + it("truncated text with markdown body", function() { + var testText, view; + testText = new Array(100).join("test "); + this.threadData.body = '' + this.imageTag + this.imageTag + '' + testText + '
'; + view = makeView(makeThread(this.threadData)); + return checkBody(true, view, this.threadData); + }); + _ref = [1, 2, 10]; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + numImages = _ref[_i]; + _ref1 = [true, false]; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + truncatedText = _ref1[_j]; + it( + "body with " + numImages + " images and " + (truncatedText ? "truncated" : "untruncated") + + " text", + // suppressing Don't make functions within a loop. + /* jshint -W083 */ + function() { + return checkPostWithImages(numImages, truncatedText, this.threadData, this.imageTag); + } + /* jshint +W083 */ + ); + } + } + it("check the thread retrieve url", function() { + var thread; + thread = makeThread(this.threadData); + return expect(thread.urlFor('retrieve')) + .toBe('/courses/edX/999/test/discussion/forum/dummy_discussion/threads/1'); + }); + }); + }); + +}).call(this); diff --git a/common/static/common/js/spec/discussion/view/discussion_thread_show_view_spec.js b/common/static/common/js/spec/discussion/view/discussion_thread_show_view_spec.js new file mode 100644 index 0000000000..7e9df2b269 --- /dev/null +++ b/common/static/common/js/spec/discussion/view/discussion_thread_show_view_spec.js @@ -0,0 +1,191 @@ +/* globals DiscussionSpecHelper, DiscussionThreadShowView, DiscussionUtil, DiscussionViewSpecHelper, Thread */ +(function() { + 'use strict'; + + var $$course_id = "$$course_id"; + describe("DiscussionThreadShowView", function() { + beforeEach(function() { + DiscussionSpecHelper.setUpGlobals(); + DiscussionSpecHelper.setUnderscoreFixtures(); + this.user = DiscussionUtil.getUser(); + this.threadData = { + id: "dummy", + user_id: this.user.id, + username: this.user.get('username'), + course_id: $$course_id, + title: "dummy title", + body: "this is a thread", + created_at: "2013-04-03T20:08:39Z", + abuse_flaggers: [], + votes: { + up_count: 42 + }, + thread_type: "discussion", + closed: false, + pinned: false, + type: "thread" + }; + this.thread = new Thread(this.threadData); + this.view = new DiscussionThreadShowView({ + model: this.thread + }); + this.view.setElement($("#fixture-element")); + return spyOn(this.view, "convertMath"); + }); + describe("voting", function() { + it("renders the vote state correctly", function() { + return DiscussionViewSpecHelper.checkRenderVote(this.view, this.thread); + }); + it("votes correctly via click", function() { + return DiscussionViewSpecHelper.checkUpvote(this.view, this.thread, this.user, $.Event("click")); + }); + it("votes correctly via spacebar", function() { + return DiscussionViewSpecHelper.checkUpvote(this.view, this.thread, this.user, $.Event("keydown", { + which: 32 + })); + }); + it("unvotes correctly via click", function() { + return DiscussionViewSpecHelper.checkUnvote(this.view, this.thread, this.user, $.Event("click")); + }); + it("unvotes correctly via spacebar", function() { + return DiscussionViewSpecHelper.checkUnvote(this.view, this.thread, this.user, $.Event("keydown", { + which: 32 + })); + }); + }); + describe("pinning", function() { + var expectPinnedRendered; + expectPinnedRendered = function(view, model) { + var button, pinned; + pinned = model.get('pinned'); + button = view.$el.find(".action-pin"); + expect(button.hasClass("is-checked")).toBe(pinned); + return expect(button.attr("aria-checked")).toEqual(pinned.toString()); + }; + it("renders the pinned state correctly", function() { + this.view.render(); + expectPinnedRendered(this.view, this.thread); + this.thread.set('pinned', false); + this.view.render(); + expectPinnedRendered(this.view, this.thread); + this.thread.set('pinned', true); + this.view.render(); + return expectPinnedRendered(this.view, this.thread); + }); + it("exposes the pinning control only to authorized users", function() { + this.thread.updateInfo({ + ability: { + can_openclose: false + } + }); + this.view.render(); + expect(this.view.$el.find(".action-pin").closest(".is-hidden")).toExist(); + this.thread.updateInfo({ + ability: { + can_openclose: true + } + }); + this.view.render(); + return expect(this.view.$el.find(".action-pin").closest(".is-hidden")).not.toExist(); + }); + it("handles events correctly", function() { + this.view.render(); + return DiscussionViewSpecHelper.checkButtonEvents(this.view, "togglePin", ".action-pin"); + }); + }); + describe("labels", function() { + var expectOneElement; + expectOneElement = function(view, selector, visible) { + var elements; + if (typeof visible === "undefined" || visible === null) { + visible = true; + } + view.render(); + elements = view.$el.find(selector); + expect(elements.length).toEqual(1); + if (visible) { + return expect(elements).not.toHaveClass("is-hidden"); + } else { + return expect(elements).toHaveClass("is-hidden"); + } + }; + it('displays the closed label when appropriate', function() { + expectOneElement(this.view, '.post-label-closed', false); + this.thread.set('closed', true); + return expectOneElement(this.view, '.post-label-closed'); + }); + it('displays the pinned label when appropriate', function() { + expectOneElement(this.view, '.post-label-pinned', false); + this.thread.set('pinned', true); + return expectOneElement(this.view, '.post-label-pinned'); + }); + it('displays the reported label when appropriate for a non-staff user', function() { + expectOneElement(this.view, '.post-label-reported', false); + this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]); + expectOneElement(this.view, '.post-label-reported'); + this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]); + return expectOneElement(this.view, '.post-label-reported', false); + }); + it('displays the reported label when appropriate for a flag moderator', function() { + DiscussionSpecHelper.makeModerator(); + expectOneElement(this.view, '.post-label-reported', false); + this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id]); + expectOneElement(this.view, '.post-label-reported'); + this.thread.set('abuse_flaggers', [DiscussionUtil.getUser().id + 1]); + return expectOneElement(this.view, '.post-label-reported'); + }); + }); + describe("author display", function() { + var checkUserLink; + beforeEach(function() { + return this.thread.set('user_url', 'test_user_url'); + }); + checkUserLink = function(element, is_ta, is_staff) { + expect(element.find('a.username').length).toEqual(1); + expect(element.find('a.username').text()).toEqual('test_user'); + expect(element.find('a.username').attr('href')).toEqual('test_user_url'); + expect(element.find('.user-label-community-ta').length).toEqual(is_ta ? 1 : 0); + return expect(element.find('.user-label-staff').length).toEqual(is_staff ? 1 : 0); + }; + it("renders correctly for a student-authored thread", function() { + var $el; + $el = $('#fixture-element').html(this.view.getAuthorDisplay()); + return checkUserLink($el, false, false); + }); + it("renders correctly for a community TA-authored thread", function() { + var $el; + this.thread.set('community_ta_authored', true); + $el = $('#fixture-element').html(this.view.getAuthorDisplay()); + return checkUserLink($el, true, false); + }); + it("renders correctly for a staff-authored thread", function() { + var $el; + this.thread.set('staff_authored', true); + $el = $('#fixture-element').html(this.view.getAuthorDisplay()); + return checkUserLink($el, false, true); + }); + it("renders correctly for an anonymously-authored thread", function() { + var $el; + this.thread.set('username', null); + $el = $('#fixture-element').html(this.view.getAuthorDisplay()); + expect($el.find('a.username').length).toEqual(0); + return expect($el.text()).toMatch(/^(\s*)anonymous(\s*)$/); + }); + }); + describe("cohorting", function() { + it("renders correctly for an uncohorted thread", function() { + this.view.render(); + return expect(this.view.$('.group-visibility-label').text().trim()) + .toEqual('This post is visible to everyone.'); + }); + it("renders correctly for a cohorted thread", function() { + this.thread.set('group_id', '1'); + this.thread.set('group_name', 'Mock Cohort'); + this.view.render(); + return expect(this.view.$('.group-visibility-label').text().trim()) + .toEqual('This post is visible only to Mock Cohort.'); + }); + }); + }); + +}).call(this); diff --git a/common/static/common/js/spec/discussion/view/discussion_thread_view_spec.js b/common/static/common/js/spec/discussion/view/discussion_thread_view_spec.js new file mode 100644 index 0000000000..cb7b7a346e --- /dev/null +++ b/common/static/common/js/spec/discussion/view/discussion_thread_view_spec.js @@ -0,0 +1,482 @@ +/* global + Discussion, DiscussionThreadShowView, DiscussionViewSpecHelper, DiscussionSpecHelper, DiscussionThreadView, + DiscussionUtil, Thread, DiscussionContentView, ThreadResponseShowView +*/ +(function() { + 'use strict'; + describe("DiscussionThreadView", function() { + var assertContentVisible, assertExpandedContentVisible, assertResponseCountAndPaginationCorrect, + createAjaxResponseJson, createTestResponseJson, postResponse, renderWithContent, renderWithTestResponses; + beforeEach(function() { + var deferred; + DiscussionSpecHelper.setUpGlobals(); + DiscussionSpecHelper.setUnderscoreFixtures(); + jasmine.clock().install(); + this.threadData = DiscussionViewSpecHelper.makeThreadWithProps({}); + this.thread = new Thread(this.threadData); + this.discussion = new Discussion(this.thread); + deferred = $.Deferred(); + spyOn($, "ajax").and.returnValue(deferred); + spyOn(DiscussionThreadShowView.prototype, "convertMath"); + spyOn(DiscussionContentView.prototype, "makeWmdEditor"); + spyOn(DiscussionUtil, "makeWmdEditor"); + spyOn(DiscussionUtil, "setWmdContent"); + return spyOn(ThreadResponseShowView.prototype, "convertMath"); + }); + afterEach(function() { + $.ajax.calls.reset(); + return jasmine.clock().uninstall(); + }); + renderWithContent = function(view, content) { + $.ajax.and.callFake(function(params) { + params.success(createAjaxResponseJson(content, false), 'success'); + return { + always: function() { + } + }; + }); + view.render(); + return jasmine.clock().tick(100); + }; + renderWithTestResponses = function(view, count, options) { + var index; + return renderWithContent(view, _.extend({ + resp_total: count, + children: count > 0 ? (function() { + var _i, _results; + _results = []; + for (index = _i = 1; 1 <= count ? _i <= count : _i >= count; index = 1 <= count ? ++_i : --_i) { + _results.push(createTestResponseJson(index)); + } + return _results; + })() : [] + }, options)); + }; + createTestResponseJson = function(index) { + return { + user_id: window.user.id, + body: "Response " + index, + id: "id_" + index, + created_at: "2015-01-01T22:20:28Z" + }; + }; + assertContentVisible = function(view, selector, visible) { + var content; + content = view.$el.find(selector); + expect(content.length).toBeGreaterThan(0); + return content.each(function(i, elem) { + return expect($(elem).is(":visible")).toEqual(visible); + }); + }; + assertExpandedContentVisible = function(view, expanded) { + expect(view.$el.hasClass("expanded")).toEqual(expanded); + assertContentVisible(view, ".post-extended-content", expanded); + assertContentVisible(view, ".forum-thread-expand", !expanded); + return assertContentVisible(view, ".forum-thread-collapse", expanded); + }; + assertResponseCountAndPaginationCorrect = function(view, countText, displayCountText, buttonText) { + expect(view.$el.find(".response-count").text()).toEqual(countText); + if (displayCountText) { + expect(view.$el.find(".response-display-count").text()).toEqual(displayCountText); + } else { + expect(view.$el.find(".response-display-count").length).toEqual(0); + } + if (buttonText) { + return expect(view.$el.find(".load-response-button").text()).toEqual(buttonText); + } else { + return expect(view.$el.find(".load-response-button").length).toEqual(0); + } + }; + createAjaxResponseJson = function(content, can_act) { + return { + content: content, + annotated_content_info: { + ability: { + editable: can_act, + can_delete: can_act, + can_reply: can_act, + can_vote: can_act + } + } + }; + }; + postResponse = function(view, index) { + var responseText, testResponseJson; + testResponseJson = createTestResponseJson(index); + responseText = testResponseJson.body; + spyOn(view, "getWmdContent").and.returnValue(responseText); + $.ajax.and.callFake(function(params) { + expect(params.type).toEqual("POST"); + expect(params.data.body).toEqual(responseText); + params.success(createAjaxResponseJson(testResponseJson, true), 'success'); + return { + always: function() { + } + }; + }); + return view.$(".discussion-submit-post").click(); + }; + describe("closed and open Threads", function() { + var checkCommentForm, checkVoteDisplay, createDiscussionThreadView; + createDiscussionThreadView = function(originallyClosed, mode) { + var discussion, thread, threadData, view; + threadData = DiscussionViewSpecHelper.makeThreadWithProps({ + closed: originallyClosed + }); + thread = new Thread(threadData); + discussion = new Discussion(thread); + view = new DiscussionThreadView({ + model: thread, + el: $("#fixture-element"), + mode: mode, + course_settings: DiscussionSpecHelper.makeCourseSettings() + }); + renderWithTestResponses(view, 1); + if (mode === "inline") { + view.expand(); + } + spyOn(DiscussionUtil, "updateWithUndo").and.callFake(function(model, updates) { + return model.set(updates); + }); + return view; + }; + checkCommentForm = function(originallyClosed, mode) { + var view; + view = createDiscussionThreadView(originallyClosed, mode); + expect(view.$('.comment-form').closest('li').is(":visible")).toBe(!originallyClosed); + expect(view.$(".discussion-reply-new").is(":visible")).toBe(!originallyClosed); + view.$(".action-close").click(); + expect(view.$('.comment-form').closest('li').is(":visible")).toBe(originallyClosed); + return expect(view.$(".discussion-reply-new").is(":visible")).toBe(originallyClosed); + }; + checkVoteDisplay = function(originallyClosed, mode) { + var view; + view = createDiscussionThreadView(originallyClosed, mode); + expect(view.$('.thread-main-wrapper .action-vote').is(":visible")).toBe(!originallyClosed); + expect(view.$('.thread-main-wrapper .display-vote').is(":visible")).toBe(originallyClosed); + view.$(".action-close").click(); + expect(view.$('.action-vote').is(":visible")).toBe(originallyClosed); + return expect(view.$('.display-vote').is(":visible")).toBe(!originallyClosed); + }; + return _.each(["tab", "inline"], function(mode) { + it( + "Test that in " + mode + " mode when a closed thread is opened the comment form is displayed", + function() { return checkCommentForm(true, mode); } + ); + it( + "Test that in " + mode + " mode when a open thread is closed the comment form is hidden", + function() { return checkCommentForm(false, mode); } + ); + it( + "Test that in " + mode + " mode when a closed thread is opened the vote button is displayed and " + + "vote count is hidden", + function() { return checkVoteDisplay(true, mode); } + ); + it( + "Test that in " + mode + " mode when a open thread is closed the vote button is hidden and " + + "vote count is displayed", + function() { return checkVoteDisplay(false, mode); } + ); + }); + }); + describe("tab mode", function() { + beforeEach(function() { + this.view = new DiscussionThreadView({ + model: this.thread, + el: $("#fixture-element"), + mode: "tab", + course_settings: DiscussionSpecHelper.makeCourseSettings() + }); + }); + describe("responses", function() { + it("can post a first response", function() { + renderWithTestResponses(this.view, 0); + postResponse(this.view, 1); + expect(this.view.$(".forum-response").length).toBe(1); + expect(this.view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1); + return expect(this.view.$(".response-actions-list").find(".action-edit") + .parent().not(".is-hidden").length).toBe(1); + }); + it("can post a second response", function() { + renderWithTestResponses(this.view, 1); + expect(this.view.$(".forum-response").length).toBe(1); + expect(this.view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1); + expect(this.view.$(".response-actions-list").find(".action-edit").parent() + .not(".is-hidden").length).toBe(1); + postResponse(this.view, 2); + expect(this.view.$(".forum-response").length).toBe(2); + expect(this.view.$(".post-actions-list").find(".action-edit").parent(".is-hidden").length).toBe(1); + return expect(this.view.$(".response-actions-list").find(".action-edit").parent() + .not(".is-hidden").length).toBe(2); + }); + }); + describe("response count and pagination", function() { + it("correctly render for a thread with no responses", function() { + renderWithTestResponses(this.view, 0); + return assertResponseCountAndPaginationCorrect(this.view, "0 responses", null, null); + }); + it("correctly render for a thread with one response", function() { + renderWithTestResponses(this.view, 1); + return assertResponseCountAndPaginationCorrect( + this.view, "1 response", "Showing all responses", null + ); + }); + it("correctly render for a thread with one additional page", function() { + renderWithTestResponses(this.view, 1, { + resp_total: 2 + }); + return assertResponseCountAndPaginationCorrect( + this.view, "2 responses", "Showing first response", "Load all responses" + ); + }); + it("correctly render for a thread with multiple additional pages", function() { + renderWithTestResponses(this.view, 2, { + resp_total: 111 + }); + return assertResponseCountAndPaginationCorrect( + this.view, "111 responses", "Showing first 2 responses", "Load next 100 responses" + ); + }); + describe("on clicking the load more button", function() { + beforeEach(function() { + renderWithTestResponses(this.view, 1, { + resp_total: 5 + }); + return assertResponseCountAndPaginationCorrect( + this.view, "5 responses", "Showing first response", "Load all responses" + ); + }); + it("correctly re-render when all threads have loaded", function() { + renderWithTestResponses(this.view, 5, { + resp_total: 5 + }); + this.view.$el.find(".load-response-button").click(); + return assertResponseCountAndPaginationCorrect( + this.view, "5 responses", "Showing all responses", null + ); + }); + it("correctly re-render when one page remains", function() { + renderWithTestResponses(this.view, 3, { + resp_total: 42 + }); + this.view.$el.find(".load-response-button").click(); + return assertResponseCountAndPaginationCorrect( + this.view, "42 responses", "Showing first 3 responses", "Load all responses" + ); + }); + it("correctly re-render when multiple pages remain", function() { + renderWithTestResponses(this.view, 3, { + resp_total: 111 + }); + this.view.$el.find(".load-response-button").click(); + return assertResponseCountAndPaginationCorrect( + this.view, "111 responses", "Showing first 3 responses", "Load next 100 responses" + ); + }); + }); + }); + }); + describe("inline mode", function() { + beforeEach(function() { + this.view = new DiscussionThreadView({ + model: this.thread, + el: $("#fixture-element"), + mode: "inline", + course_settings: DiscussionSpecHelper.makeCourseSettings() + }); + }); + describe("render", function() { + it("shows content that should be visible when collapsed", function() { + this.view.render(); + return assertExpandedContentVisible(this.view, false); + }); + it("does not render any responses by default", function() { + this.view.render(); + expect($.ajax).not.toHaveBeenCalled(); + return expect(this.view.$el.find(".responses li").length).toEqual(0); + }); + }); + describe("focus", function() { + it("sends focus to the conversation when opened", function(done) { + var self; + DiscussionViewSpecHelper.setNextResponseContent({ + resp_total: 0, + children: [] + }); + this.view.render(); + this.view.expand(); + self = this; + return jasmine.waitUntil(function() { + var article; + article = self.view.$el.find('.discussion-article'); + return article[0] === article[0].ownerDocument.activeElement; + }).then(function() { + return done(); + }); + }); + }); + describe("expand/collapse", function() { + it("shows/hides appropriate content", function() { + DiscussionViewSpecHelper.setNextResponseContent({ + resp_total: 0, + children: [] + }); + this.view.render(); + this.view.expand(); + assertExpandedContentVisible(this.view, true); + this.view.collapse(); + return assertExpandedContentVisible(this.view, false); + }); + it("switches between the abbreviated and full body", function() { + var expectedAbbreviation, longBody; + DiscussionViewSpecHelper.setNextResponseContent({ + resp_total: 0, + children: [] + }); + longBody = new Array(100).join("test "); + expectedAbbreviation = DiscussionUtil.abbreviateString(longBody, 140); + this.thread.set("body", longBody); + this.view.render(); + expect($(".post-body").text()).toEqual(expectedAbbreviation); + expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled(); + DiscussionThreadShowView.prototype.convertMath.calls.reset(); + this.view.expand(); + expect($(".post-body").text()).toEqual(longBody); + expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled(); + DiscussionThreadShowView.prototype.convertMath.calls.reset(); + this.view.collapse(); + expect($(".post-body").text()).toEqual(expectedAbbreviation); + return expect(DiscussionThreadShowView.prototype.convertMath).toHaveBeenCalled(); + }); + it("strips script tags appropriately", function() { + var longMaliciousBody, maliciousAbbreviation; + DiscussionViewSpecHelper.setNextResponseContent({ + resp_total: 0, + children: [] + }); + longMaliciousBody = new Array(100).join( + "\n" + ); + this.thread.set("body", longMaliciousBody); + maliciousAbbreviation = DiscussionUtil.abbreviateString(this.thread.get('body'), 140); + this.view.render(); + expect($(".post-body").html()).not.toEqual(maliciousAbbreviation); + expect($(".post-body").text()).toEqual(maliciousAbbreviation); + expect($(".post-body").html()).not.toContain("