setup redux app for new announcements page

refs COMMS-554

test plan:
- in a course, create some announcements
- navigate to the announcements page and note it loads the old page
- turn on section specific announcements feature flag at the account
  level
- navigate to the announcements page
- note that you see a 'Announcements (2) items on page x' with
the number in the parens matching the number of announcements
in that course

Change-Id: I6982aa2b20864f96ae61a052b33948f65bf549e2
Reviewed-on: https://gerrit.instructure.com/134425
Reviewed-by: Steven Burnett <sburnett@instructure.com>
QA-Review: Venk Natarajan <vnatarajan@instructure.com>
Tested-by: Jenkins
Product-Review: Aaron Kc Hsu <ahsu@instructure.com>
This commit is contained in:
Felix Milea-Ciobanu 2017-12-01 08:32:38 -07:00 committed by Aaron Kc Hsu
parent e3e94e68a1
commit e069f3ebef
25 changed files with 1466 additions and 4 deletions

View File

@ -42,6 +42,12 @@ class AnnouncementsController < ApplicationController
js_env atom_feed_url: feeds_announcements_format_path((@context_enrollment || @context).feed_code, :atom)
js_env(COURSE_ID: @context.id.to_s) if @context.is_a?(Course)
if @context.account.feature_enabled?(:section_specific_announcements)
js_bundle :announcements_index_v2
else
js_bundle :announcements_index
end
set_tutorial_js_env
end
end

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import I18n from 'i18n!announcements_v2'
import { createActions } from 'redux-actions'
import { getAnnouncements } from './apiClient'
import { notificationActions } from '../shared/reduxNotifications'
import { createPaginationActions } from '../shared/reduxPagination'
function fetchAnnouncements(dispatch, getState, payload) {
return (resolve, reject) => {
getAnnouncements(getState(), payload)
.then((res) => {
resolve(res)
dispatch(notificationActions.notifyInfo(I18n.t('Announcements Loaded!'))) // dummy notification, remove me later
})
.catch(err => reject({ err, message: I18n.t('An error ocurred while loading announcements') }))
}
}
const announcementActions = createPaginationActions('announcements', fetchAnnouncements)
const types = [
...announcementActions.actionTypes,
]
const actions = Object.assign(
createActions(...types),
announcementActions.actionCreators,
)
const actionTypes = types.reduce((typesMap, actionType) =>
Object.assign(typesMap, { [actionType]: actionType }), {})
export { actionTypes, actions as default }

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import axios from 'axios'
import { encodeQueryString } from '../shared/queryString'
export function getAnnouncements ({ contextCodes, announcements }, { page }) {
const params = encodeQueryString([
{ 'context_codes[]': contextCodes },
{ page: page || announcements.currentPage },
])
return axios.get(`/api/v1/announcements?${params}`)
}

View File

@ -0,0 +1,92 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import I18n from 'i18n!announcements_v2'
import React, { Component } from 'react'
import { func, bool, number } from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import Spinner from 'instructure-ui/lib/components/Spinner'
import Heading from 'instructure-ui/lib/components/Heading'
import Typography from 'instructure-ui/lib/components/Typography'
import select from '../../shared/select'
import { selectPaginationState } from '../../shared/reduxPagination'
import propTypes from '../propTypes'
import actions from '../actions'
export default class AnnouncementsIndex extends Component {
static propTypes = {
announcements: propTypes.announcementList.isRequired,
announcementsPage: number.isRequired,
isLoadingAnnouncements: bool.isRequired,
hasLoadedAnnouncements: bool.isRequired,
getAnnouncements: func.isRequired,
}
componentDidMount () {
if (!this.props.hasLoadedAnnouncements) {
this.props.getAnnouncements()
}
}
renderSpinner (condition, title) {
if (condition) {
return (
<div style={{textAlign: 'center'}}>
<Spinner size="small" title={title} />
<Typography size="small" as="p">{title}</Typography>
</div>
)
} else {
return null
}
}
renderAnnouncements () {
if (this.props.hasLoadedAnnouncements) {
return (
<Typography as="p">
{I18n.t('%{count} items on page %{page}', {
count: this.props.announcements.length,
page: this.props.announcementsPage,
})}
</Typography>
)
} else {
return null
}
}
render () {
return (
<div className="announcements-v2__wrapper">
<Heading>{I18n.t('Announcements')}</Heading>
{this.renderSpinner(this.props.isLoadingAnnouncements, I18n.t('Loading Announcements'))}
{this.renderAnnouncements()}
</div>
)
}
}
const connectState = state => Object.assign({
// other props here
}, selectPaginationState(state, 'announcements'))
const connectActions = dispatch => bindActionCreators(select(actions, ['getAnnouncements']), dispatch)
export const ConnectedAnnouncementsIndex = connect(connectState, connectActions)(AnnouncementsIndex)

View File

@ -19,7 +19,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import I18n from 'i18n!announcements'
import FriendlyDatetime from '../shared/FriendlyDatetime'
import FriendlyDatetime from '../../shared/FriendlyDatetime'
import ToggleDetails from 'instructure-ui/lib/components/ToggleDetails'
import Table from 'instructure-ui/lib/components/Table'
import Link from 'instructure-ui/lib/components/Link'

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { subscribeFlashNotifications } from '../shared/reduxNotifications'
import { ConnectedAnnouncementsIndex } from './components/AnnouncementsIndex'
import createStore from './store'
export default function createAnnouncementsIndex (root, data = {}) {
const store = createStore(data)
function unmount () {
ReactDOM.unmountComponentAtNode(root)
}
function render () {
ReactDOM.render(
<Provider store={store}>
<ConnectedAnnouncementsIndex />
</Provider>,
root
)
}
subscribeFlashNotifications(store)
return { unmount, render }
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { shape, arrayOf, string, number, bool, oneOf } from 'prop-types'
const propTypes = {}
propTypes.user = shape({
id: string.isRequired,
display_name: string.isRequired,
avatar_image_url: string,
html_url: string.isRequired,
})
propTypes.announcement = shape({
id: string.isRequired,
position: number.isRequired,
published: bool.isRequired,
title: string.isRequired,
message: string.isRequired,
posted_at: string.isRequired,
author: propTypes.user.isRequired,
read_state: oneOf(['read', 'unread']),
unread_count: number.isRequired,
})
propTypes.announcementList = arrayOf(propTypes.announcement)
export default propTypes

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { combineReducers } from 'redux'
// TODO: import { handleActions } from 'redux-actions'
// TODO: import { actionTypes } from './actions'
import { reduceNotifications } from '../shared/reduxNotifications'
import { createPaginatedReducer } from '../shared/reduxPagination'
const identity = (defaultState = null) => (
state => (state === undefined ? defaultState : state)
)
export default combineReducers({
contextCodes: identity([]),
announcements: createPaginatedReducer('announcements'),
notifications: reduceNotifications,
})

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'
import rootReducer from './reducer'
export default function configStore (initialState) {
const middleware = [
ReduxThunk,
// this is so redux-logger is not included in the production webpack bundle
(process.env.NODE_ENV !== 'production') && require('redux-logger')() // eslint-disable-line global-require
].filter(Boolean)
return applyMiddleware(...middleware)(createStore)(rootReducer, initialState)
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import createAnnIndex from 'jsx/announcements'
const root = document.querySelector('#content')
const app = createAnnIndex(root, {
contextCodes: [ENV.context_asset_string],
})
app.render()

View File

@ -20,7 +20,7 @@ import I18n from 'i18n!announcements_on_home_page'
import React from 'react'
import ReactDOM from 'react-dom'
import axios from 'axios'
import AnnouncementList from 'jsx/announcements/AnnouncementList'
import AnnouncementList from 'jsx/announcements/components/OldAnnouncementList'
import Spinner from 'instructure-ui/lib/components/Spinner'
if (ENV.SHOW_ANNOUNCEMENTS) {

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export function encodeQueryString (params) {
return params.map((param) => {
const key = Object.keys(param)[0]
const value = param[key]
return value !== null ? `${key}=${value}` : null
}).filter(Boolean).join('&')
}
export function decodeQueryString (string) {
return string.split('&')
.map(pair => pair.split('='))
.map(([key, value]) => ({ [key]: value }))
}

View File

@ -0,0 +1,105 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createActions, handleActions } from 'redux-actions'
import { showFlashAlert } from './FlashAlert'
/**
* Exports action action creators for notification actions:
* - notifyInfo(string | { message })
* - notifyError(string | { err, message })
* - clearNotifications()
*/
export const notificationActions = createActions({
NOTIFY_INFO: payload =>
typeof payload === 'string'
? { type: 'info', message: payload }
: { type: 'info', ...payload },
NOTIFY_ERROR: payload =>
typeof payload === 'string'
? { type: 'error', message: payload }
: { type: 'error', ...payload },
}, 'CLEAR_NOTIFICATION')
const createNotification = data => ({
id: Math.random().toString(36).substring(2), // pseudo uuid
timestamp: Date.now(),
type: data.type || (data.err ? 'error' : 'info'),
message: data.message,
err: data.err,
})
const handleNotificationActions = handleActions({
[notificationActions.notifyInfo.toString()]:
(state, action) => state.concat([createNotification({ ...action.payload, type: 'info' })]),
[notificationActions.notifyError.toString()]:
(state, action) => state.concat([createNotification({ ...action.payload, type: 'error' })]),
[notificationActions.clearNotification.toString()]:
(state, action) => state.slice().filter(not => not.id !== action.payload),
}, [])
/**
* Reducer function for notifications array. Add it in your root reducer!
* Will add or remove notifications to the state depending on the action
* It will try to catch generic actions with an `err` and `message` prop on the
* payload as error notifications
*
* @param {array[notification]} state current notifications state
* @param {action} action action to reduce notification with
*
* @example
* combineReducers({
* notifications: reduceNotifications,
* items: handleActions({
* // your reducer here
* }, []),
* users: (state, action) => {
* // your reducer here
* },
* })
*/
export const reduceNotifications = (state, action) => {
let newState = handleNotificationActions(state, action)
const notErr = action.type !== notificationActions.notifyError.toString()
const looksLikeErr = action.payload && action.payload.err && action.payload.message
// duck typing error notifications from structure of _FAIL actions
if (notErr && looksLikeErr) {
newState = newState.concat([createNotification(action.payload)])
}
return newState
}
/**
* This function watches the given store for notifications, and flashes an
* alert component for every one of them
*
* @param {store} store redux store to subscribe to
* @param {string} key optional state key to look look into store state for notifications
*/
export function subscribeFlashNotifications (store, key = 'notifications') {
store.subscribe(() => {
const notifications = store.getState()[key]
notifications.forEach((notification) => {
showFlashAlert(notification)
store.dispatch(notificationActions.clearNotification(notification.id))
})
})
}

View File

@ -0,0 +1,226 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { combineReducers } from 'redux'
import { handleActions } from 'redux-actions'
import parseLinkHeader from 'jsx/shared/helpers/parseLinkHeader'
const DEFAULT_PAGE = 1
// enum-like helper for load states
export const LoadStates = (function initLoadStates () {
const statesList = ['NOT_LOADED', 'LOADING', 'LOADED', 'ERRORED']
const states = statesList.reduce((map, state) =>
Object.assign(map, {
[state]: state,
}), {})
return {
...states,
statesList,
isLoading: state => state === states.LOADING,
hasLoaded: state => state === states.LOADED,
isNotLoaded: state => state === states.NOT_LOADED,
}
}())
function createActionTypes (name) {
const upperName = name.toUpperCase()
return {
select: `SELECT_${upperName}_PAGE`,
start: `GET_${upperName}_START`,
success: `GET_${upperName}_SUCCESS`,
fail: `GET_${upperName}_FAIL`,
}
}
/**
* Creates a reducer for an individual page that keeps track of loading
* state and data for that page
*
* @param {object} actions object returned by createActionTypes
*/
function createReducePage (actions) {
return combineReducers({
loadState: handleActions({
[actions.start]: () => LoadStates.LOADING,
[actions.success]: () => LoadStates.LOADED,
[actions.fail]: () => LoadStates.ERRORED,
}, LoadStates.NOT_LOADED),
items: handleActions({
[actions.success]: (state, action) => action.payload.data,
}, []),
})
}
/**
* Creates a reducer that manages the pages collection for a paginated data set
*
* @param {object} actions object returned by createActionTypes
*/
function createPagesReducer (actions) {
return function reducePages (state = {}, action) {
const page = action.payload ? action.payload.page : null
if (!page) return state // page is a required prop on payload
const pageState = state[page]
return Object.assign({}, state, {
[page]: createReducePage(actions)(pageState, action)
})
}
}
/**
* Creates a reducer that manages the state for paginated data
* It will keep track of data and load state for individual pages as well as
* what page the current page is and the max number of pages
* Add it in your root reducer! The given name is used to determine what
* actions this reducer will respond to
*
* @param {string} name name that is used in pagination action types
*
* @example
* combineReducers({
* users: handleActions({
* // your reducer here
* }, []),
*
* // items will have .currentPage, .lastPage, etc
* // items will respond to GET_ITEMS_START, etc
* items: createPaginatedReducer('items'),
* })
*/
export function createPaginatedReducer (name) {
const actions = createActionTypes(name)
return combineReducers({
currentPage: handleActions({
[actions.select]: (state, action) => action.payload.page,
}, DEFAULT_PAGE),
lastPage: handleActions({
[actions.success]: (state, action) => action.payload.lastPage || state,
}, DEFAULT_PAGE),
pages: createPagesReducer(actions),
})
}
function wrapGetPageThunk (actions, name, thunk) {
/**
* payload params:
* @param {integer} page page to select/fetch
* @param {bool} select whether to select the page we are fetching
* @param {bool} forceGet if page is already loaded, force get it anyway
*/
return (payload = {}) => (dispatch, getState) => {
if (payload.select) {
dispatch({ type: actions.select, payload: { page: payload.page } })
}
const state = getState()
const page = payload.page || state[name].currentPage
const pageData = state[name].pages[page] || {}
// only fetch page data is it has not been loaded or we are force getting it
if (!LoadStates.hasLoaded(pageData.loadState) || payload.forceGet) {
dispatch({ type: actions.start, payload: { page }})
new Promise(thunk(dispatch, getState, { page }))
.then(res => {
const successPayload = { page, data: res.data }
// sometimes the canvas API provides us with link header that gives
// us the URL to the last page. we can try parse that URL to determine
// how many pages there are in total
// works only with axios res objects, aka assumes thunk is axios promise
const links = parseLinkHeader(res)
if (links.last) {
try {
successPayload.lastPage = Number(/&page=([0-9]+)&/.exec(links.last)[1])
} catch (e) {} // eslint-disable-line
}
dispatch({ type: actions.success, payload: successPayload })
})
.catch(err => {
dispatch({ type: actions.fail, payload: { page, ...err } })
})
}
}
}
/**
* Creates actions types and action creators for paginating a set of data
*
* for name "items", action types will be:
* - SELECT_ITEMS_PAGE
* - GET_ITEMS_START
* - GET_ITEMS_SUCCESS
* - GET_ITEMS_FAIL
*
* for name "items", action creators will be:
* - getItems - redux-thunk action creator that will execute the given thunk
*
* @param {string} name name of the data set
* @param {function} thunk function that will get our data
*
* thunk must follow a promise-like interface:
* @example
* thunk = (dispatch, getState) => (resolve, reject) => {
* // your async logic here that calls resolve / reject
* // if actions is success / fail and has access to the store's
* // dispatch / getState just like any other thunk
* }
*
* @example
* function fetchItems (dispatch, getState) {
* return (resolve, reject) =>
* axios.get('/api/v1/items')
* .then(resolve)
* .catch(reject)
* }
* const itemActions = createPaginationActions('items', fetchItems)
*
* // calls fetchItems but dispatches START/SUCCESS/FAIL to store for the page
* itemActions.actionCreators.getItems({ page: 3 })
*/
export function createPaginationActions (name, thunk) {
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1)
const actionTypes = createActionTypes(name)
return {
actionTypes: Object.keys(actionTypes).map(key => actionTypes[key]),
actionCreators: {
[`get${capitalizedName}`]: wrapGetPageThunk(actionTypes, name, thunk),
},
}
}
/**
* Redux state selector function that transforms internal redux pagination
* state into props that are more friendly for a component to use
*
* @param {obj} state redux store state to look into
* @param {string} name key into state for paginated data
*/
export function selectPaginationState (state, name) {
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1)
const itemsState = state[name]
const page = itemsState.pages[itemsState.currentPage] || {}
return {
[name]: page.items || [],
[`${name}Page`]: itemsState.currentPage,
[`isLoading${capitalizedName}`]: LoadStates.isLoading(page.loadState),
[`hasLoaded${capitalizedName}`]: LoadStates.hasLoaded(page.loadState),
}
}

View File

@ -20,7 +20,6 @@
@body_classes << 'hide-content-while-scripts-not-loaded right-side-optional'
content_for :page_title, join_title(t('#titles.announcements', "Announcements"), @context.name)
feed_code = @context_enrollment.try(:feed_code) || (@context.available? && @context.feed_code)
js_bundle :announcements_index
css_bundle :discussions_list
%>
<% content_for :auto_discovery do %>

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
import { mount, shallow } from 'enzyme'
import AnnouncementsIndex from 'jsx/announcements/components/AnnouncementsIndex'
const defaultProps = () => ({
announcements: [],
announcementsPage: 1,
isLoadingAnnouncements: false,
hasLoadedAnnouncements: false,
getAnnouncements: () => {},
})
QUnit.module('AnnouncementsIndex component')
test('renders the component', () => {
const tree = mount(<AnnouncementsIndex {...defaultProps()} />)
const node = tree.find('AnnouncementsIndex')
ok(node.exists())
})
test('displays spinner when loading announcements', () => {
const props = defaultProps()
props.isLoadingAnnouncements = true
const tree = shallow(<AnnouncementsIndex {...props} />)
const node = tree.find('Spinner')
ok(node.exists())
})
test('calls getAnnouncements if hasLoadedAnnouncements is false', () => {
const props = defaultProps()
props.getAnnouncements = sinon.spy()
mount(<AnnouncementsIndex {...props} />)
equal(props.getAnnouncements.callCount, 1)
})

View File

@ -17,7 +17,7 @@
*/
define([
'react', 'react-addons-test-utils', 'jsx/announcements/AnnouncementList',
'react', 'react-addons-test-utils', 'jsx/announcements/components/OldAnnouncementList',
], (React, TestUtils, AnnouncementList) => {
QUnit.module('AnnouncementList')

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import createAnnIndex from 'jsx/announcements/index'
let app = null
const container = document.getElementById('fixtures')
QUnit.module('Announcements app', {
teardown: () => {
if (app) {
app.unmount()
app = null
}
container.innerHTML = ''
}
})
const defaultData = () => ({
contextCodes: ['course_1'],
})
test('mounts Announcements to container component', () => {
app = createAnnIndex(container, defaultData())
app.render()
ok(container.querySelector('.announcements-v2__wrapper'))
})
test('unmounts Announcements from container component', () => {
app = createAnnIndex(container, defaultData())
app.render()
app.unmount()
notOk(document.querySelector('.announcements-v2__wrapper'))
})

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2016 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createStore, applyMiddleware } from 'redux'
import ReduxThunk from 'redux-thunk'
import rootReducer from 'jsx/announcements/reducer'
export default function mockStore (initialState) {
return applyMiddleware(ReduxThunk)(createStore)(rootReducer, initialState)
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import actions from 'jsx/announcements/actions'
import reducer from 'jsx/announcements/reducer'
import sampleData from './sampleData'
QUnit.module('Announcements reducer')
const reduce = (action, state = {}) => reducer(state, action)
// test('does something on SOME_ACTION', () => {
// const newState = reduce(actions.someAction())
// deepEqual(newState.foo, 'bar')
// })

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const data = {
announcements: [{
id: '1',
position: 2,
published: true,
title: 'hello world',
posted_at: (new Date).toString(),
author: {
display_name: 'John Doe',
},
read_state: 'read',
unread_count: 0,
}],
}
export default data

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { encodeQueryString, decodeQueryString } from 'jsx/shared/queryString'
QUnit.module('Query String util')
QUnit.module('encodeQueryString')
test('encodes a query string from array of params', () => {
const params = [{ foo: 'bar' }, { hello: 'world' }]
const query = encodeQueryString(params)
equal(query, 'foo=bar&hello=world')
})
test('encodes a query string from array of params with duplicate keys', () => {
const params = [{ 'foo[]': 'bar' }, { 'foo[]': 'world' }]
const query = encodeQueryString(params)
equal(query, 'foo[]=bar&foo[]=world')
})
QUnit.module('decodeQueryString')
test('decodes a query string into an array of params', () => {
const params = decodeQueryString('foo=bar&hello=world')
deepEqual(params, [{ foo: 'bar' }, { hello: 'world' }])
})
test('decodes a query string with duplicate keys into an array of params', () => {
const params = decodeQueryString('foo[]=bar&foo[]=world')
deepEqual(params, [{ 'foo[]': 'bar' }, { 'foo[]': 'world' }])
})

View File

@ -0,0 +1,124 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { subscribeFlashNotifications, notificationActions, reduceNotifications } from 'jsx/shared/reduxNotifications'
import * as FlashAlert from 'jsx/shared/FlashAlert'
const createMockStore = state => ({
subs: [],
subscribe (cb) { this.subs.push(cb) },
getState: () => state,
dispatch: () => {},
mockStateChange () { this.subs.forEach(sub => sub()) },
})
QUnit.module('Redux Notifications')
QUnit.module('subscribeFlashNotifications', {
teardown () {
FlashAlert.destroyContainer()
}
})
test('subscribes to a store and calls showFlashAlert for each notification in state', (assert) => {
const done = assert.async()
const flashAlertSpy = sinon.spy(FlashAlert, 'showFlashAlert')
const mockStore = createMockStore({
notifications: [{ id: '1', message: 'hello' }, { id: '2', message: 'world' }]
})
subscribeFlashNotifications(mockStore)
mockStore.mockStateChange()
setTimeout(() => {
equal(flashAlertSpy.callCount, 2)
deepEqual(flashAlertSpy.firstCall.args, [{ id: '1', message: 'hello' }])
deepEqual(flashAlertSpy.secondCall.args, [{ id: '2', message: 'world' }])
flashAlertSpy.restore()
done()
}, 1)
})
test('subscribes to a store and dispatches clearNotifications for each notification in state', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
notifications: [{ id: '1', message: 'hello' }, { id: '2', message: 'world' }]
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
subscribeFlashNotifications(mockStore)
mockStore.mockStateChange()
setTimeout(() => {
equal(dispatchSpy.callCount, 2)
deepEqual(dispatchSpy.firstCall.args, [notificationActions.clearNotification('1')])
deepEqual(dispatchSpy.secondCall.args, [notificationActions.clearNotification('2')])
dispatchSpy.restore()
done()
}, 1)
})
QUnit.module('notificationActions')
test('notifyInfo creates action NOTIFY_INFO with type "info" and payload', () => {
const action = notificationActions.notifyInfo({ message: 'test' })
deepEqual(action, { type: 'NOTIFY_INFO', payload: { type: 'info', message: 'test' } })
})
test('notifyError creates action NOTIFY_ERROR with type "error" and payload', () => {
const action = notificationActions.notifyError({ message: 'test' })
deepEqual(action, { type: 'NOTIFY_ERROR', payload: { type: 'error', message: 'test' } })
})
test('clearNotification creates action CLEAR_NOTIFICATION', () => {
const action = notificationActions.clearNotification()
deepEqual(action, { type: 'CLEAR_NOTIFICATION' })
})
QUnit.module('reduceNotifications')
test('catches any action with err and message and treats it as an error notification', () => {
const action = { type: '_NOT_A_REAL_ACTION_', payload: { message: 'hello world', err: 'bad things happened' } }
const newState = reduceNotifications([], action)
equal(newState.length, 1)
equal(newState[0].type, 'error')
equal(newState[0].message, 'hello world')
equal(newState[0].err, 'bad things happened')
})
test('adds new info notification on NOTIFY_INFO', () => {
const newState = reduceNotifications([], notificationActions.notifyInfo({ message: 'hello world' }))
equal(newState.length, 1)
equal(newState[0].type, 'info')
equal(newState[0].message, 'hello world')
})
test('adds new error notification on NOTIFY_ERROR', () => {
const newState = reduceNotifications([], notificationActions.notifyError({ message: 'hello world', err: 'bad things happened' }))
equal(newState.length, 1)
equal(newState[0].type, 'error')
equal(newState[0].message, 'hello world')
equal(newState[0].err, 'bad things happened')
})
test('removes notification on CLEAR_NOTIFICATION', () => {
const newState = reduceNotifications([
{ id: '1', message: 'hello world', type: 'info' }
], notificationActions.clearNotification('1'))
equal(newState.length, 0)
})

View File

@ -0,0 +1,338 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { createPaginatedReducer, createPaginationActions, selectPaginationState, LoadStates } from 'jsx/shared/reduxPagination'
const createMockStore = state => ({
subs: [],
subscribe (cb) { this.subs.push(cb) },
getState: () => state,
dispatch: () => {},
mockStateChange () { this.subs.forEach(sub => sub()) },
})
QUnit.module('Redux Pagination')
QUnit.module('createPaginationActions')
test('creates proper actionTypes', () => {
const { actionTypes } = createPaginationActions('things')
deepEqual(actionTypes, ['SELECT_THINGS_PAGE', 'GET_THINGS_START', 'GET_THINGS_SUCCESS', 'GET_THINGS_FAIL'])
})
test('creates get action creator', () => {
const { actionCreators } = createPaginationActions('things')
equal(typeof actionCreators.getThings, 'function')
})
test('creates get action creator from thunk that calls start with current page', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 3,
pages: { 3: { item: [], loadState: LoadStates.NOT_LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, _reject) => resolve({ data: [] })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings()(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 2)
deepEqual(dispatchSpy.firstCall.args, [{ type: 'GET_THINGS_START', payload: { page: 3 } }])
dispatchSpy.restore()
done()
})
})
test('creates get action creator from thunk that calls start with a payload page', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 1,
pages: { 1: { item: [], loadState: LoadStates.NOT_LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, _reject) => resolve({ data: [] })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings({ page: 5 })(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 2)
deepEqual(dispatchSpy.firstCall.args, [{ type: 'GET_THINGS_START', payload: { page: 5 } }])
dispatchSpy.restore()
done()
})
})
test('creates get action creator from thunk that calls success', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 1,
pages: { 1: { item: [], loadState: LoadStates.NOT_LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, _reject) => resolve({ data: ['item1'] })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings({ page: 5 })(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 2)
deepEqual(dispatchSpy.secondCall.args, [{ type: 'GET_THINGS_SUCCESS', payload: { page: 5, data: ['item1'] } }])
dispatchSpy.restore()
done()
})
})
test('creates get action creator from thunk that calls success with lastPage is provided in response link header', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 1,
pages: { 1: { item: [], loadState: LoadStates.NOT_LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, _reject) => resolve({ data: ['item1'], headers: { link: '<http://canvas.example.com/api/v1/someendpoint&page=5&per_page=50>; rel="last"' } })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings()(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 2)
deepEqual(dispatchSpy.secondCall.args, [{ type: 'GET_THINGS_SUCCESS', payload: { page: 1, data: ['item1'], lastPage: 5 } }])
dispatchSpy.restore()
done()
})
})
test('creates get action creator from thunk that calls fail', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 1,
pages: { 1: { item: [], loadState: LoadStates.NOT_LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, reject) => reject({ message: 'oops error' })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings({ page: 5 })(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 2)
deepEqual(dispatchSpy.secondCall.args, [{ type: 'GET_THINGS_FAIL', payload: { page: 5, message: 'oops error' } }])
dispatchSpy.restore()
done()
})
})
test('creates get action creator from thunk that does not call thunk if page is already loaded', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 1,
pages: { 1: { item: [], loadState: LoadStates.LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, _reject) => resolve({ data: ['item1'] })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings()(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 0)
dispatchSpy.restore()
done()
})
})
test('creates get action creator from thunk that calls thunk if page is already loaded and forgetGet is true', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 1,
pages: { 1: { item: [], loadState: LoadStates.LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, _reject) => resolve({ data: ['item1'] })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings({ forceGet: true })(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 2)
deepEqual(dispatchSpy.secondCall.args, [{ type: 'GET_THINGS_SUCCESS', payload: { page: 1, data: ['item1'] } }])
dispatchSpy.restore()
done()
})
})
test('creates get action creator from thunk that selects the page if select is true', (assert) => {
const done = assert.async()
const mockStore = createMockStore({
things: {
currentPage: 1,
pages: { 1: { item: [], loadState: LoadStates.LOADED } }
},
})
const dispatchSpy = sinon.spy(mockStore, 'dispatch')
const thunk = () => (resolve, _reject) => resolve({ data: ['item1'] })
const { actionCreators } = createPaginationActions('things', thunk)
actionCreators.getThings({ select: true, page: 5 })(mockStore.dispatch, mockStore.getState)
setTimeout(() => {
equal(dispatchSpy.callCount, 3)
deepEqual(dispatchSpy.firstCall.args, [{ type: 'SELECT_THINGS_PAGE', payload: { page: 5 } }])
dispatchSpy.restore()
done()
})
})
QUnit.module('createPaginatedReducer')
test('sets current page on SELECT_PAGE', () => {
const state = {
currentPage: 1,
pages: {
1: {
items: [],
loadState: LoadStates.NOT_LOADED,
},
},
}
const reduce = createPaginatedReducer('things')
const action = { type: 'SELECT_THINGS_PAGE', payload: { page: 5 } }
const newState = reduce(state, action)
equal(newState.currentPage, 5)
})
test('sets last page on GET_SUCCESS', () => {
const state = {
currentPage: 1,
pages: {
1: {
items: [],
loadState: LoadStates.NOT_LOADED,
},
},
}
const reduce = createPaginatedReducer('things')
const action = { type: 'GET_THINGS_SUCCESS', payload: { lastPage: 5, page: 1, data: ['item1'] } }
const newState = reduce(state, action)
equal(newState.lastPage, 5)
})
test('sets items for page on GET_SUCCESS', () => {
const state = {
currentPage: 1,
pages: {
1: {
items: [],
loadState: LoadStates.NOT_LOADED,
},
},
}
const reduce = createPaginatedReducer('things')
const action = { type: 'GET_THINGS_SUCCESS', payload: { page: 1, data: ['item1'] } }
const newState = reduce(state, action)
deepEqual(newState.pages[1].items, ['item1'])
})
test('sets loadState for page to LOADING on GET_START', () => {
const state = {
currentPage: 1,
pages: {
1: {
items: [],
loadState: LoadStates.NOT_LOADED,
},
},
}
const reduce = createPaginatedReducer('things')
const action = { type: 'GET_THINGS_START', payload: { page: 1 } }
const newState = reduce(state, action)
equal(newState.pages[1].loadState, LoadStates.LOADING)
})
test('sets loadState for page to LOADED on GET_SUCCESS', () => {
const state = {
currentPage: 1,
pages: {
1: {
items: [],
loadState: LoadStates.LOADING,
},
},
}
const reduce = createPaginatedReducer('things')
const action = { type: 'GET_THINGS_SUCCESS', payload: { page: 1, data: [] } }
const newState = reduce(state, action)
equal(newState.pages[1].loadState, LoadStates.LOADED)
})
test('sets loadState for page to ERRORED on GET_FAIL', () => {
const state = {
currentPage: 1,
pages: {
1: {
items: [],
loadState: LoadStates.LOADING,
},
},
}
const reduce = createPaginatedReducer('things')
const action = { type: 'GET_THINGS_FAIL', payload: { page: 1, message: 'oops error' } }
const newState = reduce(state, action)
equal(newState.pages[1].loadState, LoadStates.ERRORED)
})
QUnit.module('selectPaginationState')
test('derives state for existing page', () => {
const state = {
things: {
currentPage: 1,
pages: {
1: {
items: ['item1'],
loadState: LoadStates.LOADING,
},
},
}
}
const derivedState = selectPaginationState(state, 'things')
deepEqual(derivedState.things, ['item1'])
deepEqual(derivedState.thingsPage, 1)
deepEqual(derivedState.isLoadingThings, true)
deepEqual(derivedState.hasLoadedThings, false)
})
test('derives state for not yet existing page', () => {
const state = {
things: {
currentPage: 5,
pages: {
1: {
items: ['item1'],
loadState: LoadStates.LOADING,
},
},
}
}
const derivedState = selectPaginationState(state, 'things')
deepEqual(derivedState.things, [])
deepEqual(derivedState.thingsPage, 5)
deepEqual(derivedState.isLoadingThings, false)
deepEqual(derivedState.hasLoadedThings, false)
})

View File

@ -0,0 +1,46 @@
#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative '../common'
describe "announcements index v2" do
include_context "in-process server selenium tests"
let(:url) { "/courses/#{@course.id}/announcements/" }
context "announcements as a teacher" do
before :once do
@teacher = user_with_pseudonym(active_user: true)
course_with_teacher(user: @teacher, active_course: true, active_enrollment: true)
end
before :each do
user_session(@teacher)
end
it 'should display the old announcements if the feature flas is off' do
@course.account.set_feature_flag! :section_specific_announcements, 'off'
get url
expect(f('#external_feed_url')).not_to be_nil
end
it 'should display the new announcements if the feature flas is on' do
@course.account.set_feature_flag! :section_specific_announcements, 'on'
get url
expect(f('.announcements-v2__wrapper')).not_to be_nil
end
end
end