Fix AppFilters spec on master build
Test Plan: n/a Change-Id: I210c97d4e24ccbf5e7827103a3ca211b9ca27bee Reviewed-on: https://gerrit.instructure.com/167771 Tested-by: Jenkins Reviewed-by: Weston Dransfield <wdransfield@instructure.com> QA-Review: Weston Dransfield <wdransfield@instructure.com> Product-Review: Marc Phillips <mphillips@instructure.com>
This commit is contained in:
parent
7fb2a50d1a
commit
91fe52ff86
|
@ -18,7 +18,7 @@
|
|||
|
||||
import I18n from 'i18n!external_tools'
|
||||
import React from 'react'
|
||||
import store from '../../external_apps/lib/AppCenterStore'
|
||||
import store from '../lib/AppCenterStore'
|
||||
import $ from 'jquery'
|
||||
import 'compiled/jquery.rails_flash_notifications'
|
||||
|
||||
|
@ -64,7 +64,7 @@ export default class AppFilters extends React.Component {
|
|||
<ul className="nav nav-pills" role="tablist">
|
||||
<li className={activeFilter === 'all' ? 'active' : ''}>
|
||||
<a
|
||||
ref="tabAll"
|
||||
ref={c => (this.tabAll = c)}
|
||||
onClick={this.handleFilterClick.bind(this, 'all')}
|
||||
href="#"
|
||||
role="tab"
|
||||
|
@ -75,7 +75,7 @@ export default class AppFilters extends React.Component {
|
|||
</li>
|
||||
<li className={activeFilter === 'not_installed' ? 'active' : ''}>
|
||||
<a
|
||||
ref="tabNotInstalled"
|
||||
ref={c => (this.tabNotInstalled = c)}
|
||||
onClick={this.handleFilterClick.bind(this, 'not_installed')}
|
||||
href="#"
|
||||
role="tab"
|
||||
|
@ -86,7 +86,7 @@ export default class AppFilters extends React.Component {
|
|||
</li>
|
||||
<li className={activeFilter === 'installed' ? 'active' : ''}>
|
||||
<a
|
||||
ref="tabInstalled"
|
||||
ref={c => (this.tabInstalled = c)}
|
||||
onClick={this.handleFilterClick.bind(this, 'installed')}
|
||||
href="#"
|
||||
role="tab"
|
||||
|
@ -99,7 +99,7 @@ export default class AppFilters extends React.Component {
|
|||
window.ENV.LTI_13_TOOLS_FEATURE_FLAG_ENABLED &&
|
||||
<li className={activeFilter === 'lti_1_3_tools' ? 'active' : ''}>
|
||||
<a
|
||||
ref="tabLti13Tools"
|
||||
ref={c => (this.tabLti13Tools = c)}
|
||||
onClick={this.handleFilterClick.bind(this, 'lti_1_3_tools')}
|
||||
href="#"
|
||||
role="tab"
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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} from 'enzyme'
|
||||
import store from '../../lib/AppCenterStore'
|
||||
import AppFilters from '../AppFilters'
|
||||
|
||||
const defaultApps = () => [
|
||||
{
|
||||
app_type: null,
|
||||
average_rating: 0,
|
||||
banner_image_url:
|
||||
'https://edu-app-center.s3.amazonaws.com/uploads/production/lti_app/banner_image/acclaim_app.png',
|
||||
config_options: [],
|
||||
config_xml_url: 'https://www.eduappcenter.com/configurations/g7lthtepu68qhchz.xml',
|
||||
custom_tags: [
|
||||
'Assessment',
|
||||
'Community',
|
||||
'Content',
|
||||
'Media',
|
||||
'7th-12th Grade',
|
||||
'Postsecondary',
|
||||
'Open Content',
|
||||
'Web 2.0'
|
||||
],
|
||||
description:
|
||||
'\n<p>Acclaim is the easiest way to organize and annotate videos for class.</p>\n\n<p>Instructors and students set up folders, upload recordings or embed relevant web videos, and then securely share access among each other. Moments in each video can then be highlighted and discussed using time-specific comments!</p>\n\n<p>To learn more about how to use Acclaim in your class(es), email Melinda at <a href="mailto:melinda@getacclaim.com">melinda@getacclaim.com</a>.</p>\n\n<p>For general information, please visit <a href="https://getacclaim.com">Acclaim</a>.</p>\n',
|
||||
icon_image_url: null,
|
||||
id: 289,
|
||||
is_certified: false,
|
||||
is_installed: true,
|
||||
logo_image_url: null,
|
||||
name: 'Acclaim',
|
||||
preview_url: '',
|
||||
requires_secret: true,
|
||||
short_description: 'Acclaim is the easiest way to organize and annotate videos.',
|
||||
short_name: 'acclaim_app',
|
||||
status: 'active',
|
||||
total_ratings: 0
|
||||
},
|
||||
{
|
||||
app_type: null,
|
||||
average_rating: 5,
|
||||
banner_image_url:
|
||||
'https://edu-app-center.s3.amazonaws.com/uploads/production/lti_app/banner_image/aleks.png',
|
||||
config_options: [],
|
||||
config_xml_url: 'https://www.eduappcenter.com/configurations/fn4dnjhl0ot1ka4j.xml',
|
||||
custom_tags: ['Community', 'Content', 'K-6th Grade', '7th-12th Grade', 'Postsecondary'],
|
||||
description:
|
||||
'\n<p>ALEKS is an artificial intelligent assessment and learning system which uses adaptive questioning to quickly and accurately determine exactly what a student knows and doesn\u2019t know in a course.</p>\n\n<p>ALEKS\u2019s LTI support included external assignments that pass grades back to the LMS.</p>\n\n<p>Visit the <a href="http://www.aleks.com/highered/math/training_center">ALEKS Training Center</a> for directions on configuring your ALEKS Higher Education school account. Look for the \u2018LTI Integration\u2019 section.</p>\n',
|
||||
icon_image_url: null,
|
||||
id: 66,
|
||||
is_certified: true,
|
||||
is_installed: true,
|
||||
logo_image_url: 'http://www.edu-apps.org/tools/aleks/logo.png',
|
||||
name: 'ALEKS',
|
||||
preview_url: '',
|
||||
requires_secret: true,
|
||||
short_description:
|
||||
'ALEKS is an artificial intelligent assessment and learning system which uses adaptive questioning to quickly and accurately determine exactly what a student kno...',
|
||||
short_name: 'aleks',
|
||||
status: 'active',
|
||||
total_ratings: 2
|
||||
},
|
||||
{
|
||||
app_type: 'custom',
|
||||
average_rating: 4,
|
||||
banner_image_url:
|
||||
'https://edu-app-center.s3.amazonaws.com/uploads/production/lti_app/banner_image/apprennet.png',
|
||||
config_options: [],
|
||||
config_xml_url: 'https://www.apprennet.com/lti-config.xml',
|
||||
custom_tags: ['Community', 'Postsecondary'],
|
||||
description:
|
||||
'\n<p>Integrate hands-on learning exercises into your course. With ApprenNet\u2019s LTI APP, you can add exercises to your course in which participants 1) submit video responses, 2) review their peers, 3) receive feedback and 4) engage with experts. Make e-learning interactive and social.</p>\n\n<p><a href="https://www.apprennet.com/users/sign_up_guide">Start a free trial!</a> and then, if you like what you see, request a key and secret by emailing contact@apprennet.com.</p>\n',
|
||||
icon_image_url: null,
|
||||
id: 127,
|
||||
is_certified: false,
|
||||
logo_image_url:
|
||||
'https://s3.amazonaws.com/assets01.apprennet.com/lti-bounty/apprennet-logo-2-72x72.png',
|
||||
name: 'ApprenNet',
|
||||
preview_url: 'https://www.youtube.com/embed/sJ81INPRNa0',
|
||||
requires_secret: true,
|
||||
short_description:
|
||||
"Integrate hands-on learning exercises into your course. With ApprenNet's LTI APP, you can add exercises to your course in which participants 1) submit video res...",
|
||||
short_name: 'apprennet',
|
||||
status: 'active',
|
||||
total_ratings: 1
|
||||
}
|
||||
]
|
||||
|
||||
let wrapper = 'empty wrapper'
|
||||
|
||||
beforeEach(() => {
|
||||
window.ENV = {LTI_13_TOOLS_FEATURE_FLAG_ENABLED: true}
|
||||
store.setState({apps: defaultApps()})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount()
|
||||
store.reset()
|
||||
})
|
||||
|
||||
it('changes the filter to not_installed on Not Installed tab click', () => {
|
||||
wrapper = mount(<AppFilters />)
|
||||
|
||||
wrapper.find('a[children="Not Installed"]').simulate('click')
|
||||
expect(store.getState().filter).toEqual('not_installed')
|
||||
})
|
||||
|
||||
it('changes the filter to installed on Installed tab click', () => {
|
||||
wrapper = mount(<AppFilters />)
|
||||
|
||||
wrapper.find('a[children="Installed"]').simulate('click')
|
||||
expect(store.getState().filter).toEqual('installed')
|
||||
})
|
||||
|
||||
it('changes the filter to lti_1_3_tools on LTI 1.3 tab click', () => {
|
||||
wrapper = mount(<AppFilters />)
|
||||
|
||||
wrapper.find('a[children="LTI 1.3"]').simulate('click')
|
||||
expect(store.getState().filter).toEqual('lti_1_3_tools')
|
||||
})
|
|
@ -19,8 +19,8 @@
|
|||
import $ from 'jquery'
|
||||
import I18n from 'i18n!external_tools'
|
||||
import _ from 'underscore'
|
||||
import createStore from '../../shared/helpers/createStore'
|
||||
import ExternalAppsStore from '../../external_apps/lib/ExternalAppsStore'
|
||||
import createStore from '../../shared/helpers/createStoreJestCompatible'
|
||||
import ExternalAppsStore from './ExternalAppsStore'
|
||||
import parseLinkHeader from 'compiled/fn/parseLinkHeader'
|
||||
import 'compiled/jquery.rails_flash_notifications'
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import I18n from 'i18n!external_tools'
|
||||
import $ from 'jquery'
|
||||
import _ from 'underscore'
|
||||
import createStore from '../../shared/helpers/createStore'
|
||||
import createStore from '../../shared/helpers/createStoreJestCompatible'
|
||||
import parseLinkHeader from 'compiled/fn/parseLinkHeader'
|
||||
import 'compiled/jquery.rails_flash_notifications'
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a data store with some initial state.
|
||||
*
|
||||
* ```js
|
||||
* var UserStore = createStore({loaded: false, users: []});
|
||||
*
|
||||
* UserStore.load = function() {
|
||||
* $.getJSON('/users', function(users) {
|
||||
* UserStore.setState({loaded: true, users});
|
||||
* });
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* Then in a component:
|
||||
*
|
||||
* ```js
|
||||
* var UsersView = React.createClass({
|
||||
* getInitialState () {
|
||||
* return UserStore.getState();
|
||||
* },
|
||||
*
|
||||
* componentDidMount () {
|
||||
* UserStore.addChangeListener(this.handleStoreChange);
|
||||
* UserStore.load();
|
||||
* },
|
||||
*
|
||||
* handleStoreChange () {
|
||||
* this.setState(UserStore.getState());
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
function createStore(initialState = {}) {
|
||||
let state = initialState
|
||||
const listeners = {}
|
||||
|
||||
return {
|
||||
setState (newState) {
|
||||
state = Object.assign({}, state, newState)
|
||||
this.emitChange()
|
||||
},
|
||||
|
||||
getState () {
|
||||
return state
|
||||
},
|
||||
|
||||
clearState () {
|
||||
state = {}
|
||||
this.emitChange()
|
||||
},
|
||||
|
||||
addChangeListener (listener) {
|
||||
listeners[listener] = listener
|
||||
},
|
||||
|
||||
removeChangeListener (listener) {
|
||||
delete listeners[listener]
|
||||
},
|
||||
|
||||
emitChange () {
|
||||
Object.values(listeners).forEach(listener => listener())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default createStore
|
|
@ -35,6 +35,8 @@ it('sums numbers', () => {
|
|||
All `expect()` matchers supported by Jest are [extensively documented here](http://facebook.github.io/jest/docs/api.html#expect-value).<br>
|
||||
You can also use [`jest.fn()` and `expect(fn).toBeCalled()`](http://facebook.github.io/jest/docs/api.html#tobecalled) to create “spies” or mock functions and in jest tests you should probably use that instead of sinon mocks/spies/stubs like we use in our QUnit tests.
|
||||
|
||||
*NOTE: You cannot run jest if there is anything with AMD, CoffeeScript, or some of the Webpack aliases (which lead to AMD or CoffeeScript).
|
||||
|
||||
### Testing Components
|
||||
|
||||
There is a broad spectrum of component testing techniques. They range from a “smoke test” using a jest snapshot, to shallow rendering and testing some of the output, to full rendering and testing component lifecycle and state changes.
|
||||
|
|
|
@ -1,148 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 - 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 TestUtils from 'react-dom/test-utils'
|
||||
import store from 'jsx/external_apps/lib/AppCenterStore'
|
||||
import AppFilters from 'jsx/external_apps/components/AppFilters'
|
||||
|
||||
const {Simulate} = TestUtils
|
||||
const wrapper = document.getElementById('fixtures')
|
||||
const createElement = () => <AppFilters />
|
||||
const renderComponent = () => ReactDOM.render(createElement(), wrapper)
|
||||
const getDOMNodes = function() {
|
||||
const component = renderComponent()
|
||||
const tabAll = component.refs.tabAll
|
||||
const tabNotInstalled = component.refs.tabNotInstalled
|
||||
const tabInstalled = component.refs.tabInstalled
|
||||
const filterText = component.refs.filterText
|
||||
const lti13tools = component.refs.tabLti13Tools
|
||||
return [component, tabAll, tabNotInstalled, tabInstalled, lti13tools, filterText]
|
||||
}
|
||||
|
||||
QUnit.module('ExternalApps.AppFilters', {
|
||||
setup() {
|
||||
this.apps = [
|
||||
{
|
||||
app_type: null,
|
||||
average_rating: 0,
|
||||
banner_image_url:
|
||||
'https://edu-app-center.s3.amazonaws.com/uploads/production/lti_app/banner_image/acclaim_app.png',
|
||||
config_options: [],
|
||||
config_xml_url: 'https://www.eduappcenter.com/configurations/g7lthtepu68qhchz.xml',
|
||||
custom_tags: [
|
||||
'Assessment',
|
||||
'Community',
|
||||
'Content',
|
||||
'Media',
|
||||
'7th-12th Grade',
|
||||
'Postsecondary',
|
||||
'Open Content',
|
||||
'Web 2.0'
|
||||
],
|
||||
description:
|
||||
'\n<p>Acclaim is the easiest way to organize and annotate videos for class.</p>\n\n<p>Instructors and students set up folders, upload recordings or embed relevant web videos, and then securely share access among each other. Moments in each video can then be highlighted and discussed using time-specific comments!</p>\n\n<p>To learn more about how to use Acclaim in your class(es), email Melinda at <a href="mailto:melinda@getacclaim.com">melinda@getacclaim.com</a>.</p>\n\n<p>For general information, please visit <a href="https://getacclaim.com">Acclaim</a>.</p>\n',
|
||||
icon_image_url: null,
|
||||
id: 289,
|
||||
is_certified: false,
|
||||
is_installed: true,
|
||||
logo_image_url: null,
|
||||
name: 'Acclaim',
|
||||
preview_url: '',
|
||||
requires_secret: true,
|
||||
short_description: 'Acclaim is the easiest way to organize and annotate videos.',
|
||||
short_name: 'acclaim_app',
|
||||
status: 'active',
|
||||
total_ratings: 0
|
||||
},
|
||||
{
|
||||
app_type: null,
|
||||
average_rating: 5,
|
||||
banner_image_url:
|
||||
'https://edu-app-center.s3.amazonaws.com/uploads/production/lti_app/banner_image/aleks.png',
|
||||
config_options: [],
|
||||
config_xml_url: 'https://www.eduappcenter.com/configurations/fn4dnjhl0ot1ka4j.xml',
|
||||
custom_tags: ['Community', 'Content', 'K-6th Grade', '7th-12th Grade', 'Postsecondary'],
|
||||
description:
|
||||
'\n<p>ALEKS is an artificial intelligent assessment and learning system which uses adaptive questioning to quickly and accurately determine exactly what a student knows and doesn\u2019t know in a course.</p>\n\n<p>ALEKS\u2019s LTI support included external assignments that pass grades back to the LMS.</p>\n\n<p>Visit the <a href="http://www.aleks.com/highered/math/training_center">ALEKS Training Center</a> for directions on configuring your ALEKS Higher Education school account. Look for the \u2018LTI Integration\u2019 section.</p>\n',
|
||||
icon_image_url: null,
|
||||
id: 66,
|
||||
is_certified: true,
|
||||
is_installed: true,
|
||||
logo_image_url: 'http://www.edu-apps.org/tools/aleks/logo.png',
|
||||
name: 'ALEKS',
|
||||
preview_url: '',
|
||||
requires_secret: true,
|
||||
short_description:
|
||||
'ALEKS is an artificial intelligent assessment and learning system which uses adaptive questioning to quickly and accurately determine exactly what a student kno...',
|
||||
short_name: 'aleks',
|
||||
status: 'active',
|
||||
total_ratings: 2
|
||||
},
|
||||
{
|
||||
app_type: 'custom',
|
||||
average_rating: 4,
|
||||
banner_image_url:
|
||||
'https://edu-app-center.s3.amazonaws.com/uploads/production/lti_app/banner_image/apprennet.png',
|
||||
config_options: [],
|
||||
config_xml_url: 'https://www.apprennet.com/lti-config.xml',
|
||||
custom_tags: ['Community', 'Postsecondary'],
|
||||
description:
|
||||
'\n<p>Integrate hands-on learning exercises into your course. With ApprenNet\u2019s LTI APP, you can add exercises to your course in which participants 1) submit video responses, 2) review their peers, 3) receive feedback and 4) engage with experts. Make e-learning interactive and social.</p>\n\n<p><a href="https://www.apprennet.com/users/sign_up_guide">Start a free trial!</a> and then, if you like what you see, request a key and secret by emailing contact@apprennet.com.</p>\n',
|
||||
icon_image_url: null,
|
||||
id: 127,
|
||||
is_certified: false,
|
||||
logo_image_url:
|
||||
'https://s3.amazonaws.com/assets01.apprennet.com/lti-bounty/apprennet-logo-2-72x72.png',
|
||||
name: 'ApprenNet',
|
||||
preview_url: 'https://www.youtube.com/embed/sJ81INPRNa0',
|
||||
requires_secret: true,
|
||||
short_description:
|
||||
"Integrate hands-on learning exercises into your course. With ApprenNet's LTI APP, you can add exercises to your course in which participants 1) submit video res...",
|
||||
short_name: 'apprennet',
|
||||
status: 'active',
|
||||
total_ratings: 1
|
||||
}
|
||||
]
|
||||
store.reset()
|
||||
window.ENV.LTI_13_TOOLS_FEATURE_FLAG_ENABLED = true
|
||||
return store.setState({apps: this.apps})
|
||||
},
|
||||
teardown() {
|
||||
store.reset()
|
||||
ReactDOM.unmountComponentAtNode(wrapper)
|
||||
}
|
||||
})
|
||||
|
||||
test('renders', () => {
|
||||
const [component, tabAll, tabNotInstalled, tabInstalled, lti13tools, filterText] = Array.from(getDOMNodes())
|
||||
ok(component)
|
||||
ok(TestUtils.isCompositeComponentWithType(component, AppFilters))
|
||||
})
|
||||
|
||||
test('sets filter on click', () => {
|
||||
const [component, tabAll, tabNotInstalled, tabInstalled, lti13tools, filterText] = Array.from(getDOMNodes())
|
||||
equal(store.getState().filter, 'all')
|
||||
Simulate.click(tabNotInstalled)
|
||||
equal(store.getState().filter, 'not_installed')
|
||||
Simulate.click(tabInstalled)
|
||||
equal(store.getState().filter, 'installed')
|
||||
Simulate.click(lti13tools)
|
||||
equal(store.getState().filter, 'lti_1_3_tools')
|
||||
})
|
|
@ -143,7 +143,7 @@ test('filteredApps', function() {
|
|||
})
|
||||
|
||||
test('filteredApps of lti 1.3 tools', function() {
|
||||
store.setState({filterText: 'e'})
|
||||
store.setState({filterText: 'tool', lti13Tools: this})
|
||||
equal(store.filteredApps(this.lti13Tools).length, 1)
|
||||
})
|
||||
|
||||
|
@ -172,6 +172,7 @@ test('fetch13Tools', function() {
|
|||
})
|
||||
|
||||
test('installTool', function() {
|
||||
store.setState({lti13Tools: this.lti13Tools})
|
||||
notOk(store.getState().lti13Tools.find(tool => tool.app_id === '1').enabled)
|
||||
notOk(store.getState().lti13Tools.find(tool => tool.app_id === '1').installed_locally)
|
||||
this.server.respondWith('POST', /\/create_tool/, [
|
||||
|
@ -186,6 +187,7 @@ test('installTool', function() {
|
|||
})
|
||||
|
||||
test('removeTool', function() {
|
||||
store.setState({lti13Tools: this.lti13Tools})
|
||||
store._toggle_lti_1_3_tool_enabled('1')(true)
|
||||
ok(store.getState().lti13Tools.find(tool => tool.app_id === '1').enabled)
|
||||
ok(store.getState().lti13Tools.find(tool => tool.app_id === '1').installed_locally)
|
||||
|
|
Loading…
Reference in New Issue