Necessary tweaks I discovered adding the CanvasRce component to a page
closes MAT-277 flag=rce_enhancements Factored these changes out of the changes I had to make for MAT-253 where the CanvasRce component was put on a real page and tested. 1. update jest.config.js to mock the tinymce-react Editor component 2. add a missing dependency to package.json 3. tweak tests to use the new mock 4. use safe-dereference operator in places where specs might not have a fully formed ENV test plan: - this is a tough one. It has to pass jenkins, but beyond that it will take the changes for MAT-253 to prove these changes do all we need Change-Id: I5804d1a1f13dbfbc21a3213db92d7afe74e576e6 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/267427 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Gary Mei <gmei@instructure.com> QA-Review: Gary Mei <gmei@instructure.com> Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
parent
9bc1cd6353
commit
c7af7b7e02
|
@ -24,7 +24,14 @@ module.exports = {
|
|||
'\\.svg$': '<rootDir>/jest/imageMock.js',
|
||||
'node_modules-version-of-backbone': require.resolve('backbone'),
|
||||
'node_modules-version-of-react-modal': require.resolve('react-modal'),
|
||||
'^Backbone$': '<rootDir>/public/javascripts/Backbone.js'
|
||||
'^Backbone$': '<rootDir>/public/javascripts/Backbone.js',
|
||||
// jest can't import the icons
|
||||
'@instructure/ui-icons/es/svg': '<rootDir>/packages/canvas-rce/src/rce/__tests__/_mockIcons.js',
|
||||
// redirect import from es/rce/CanvasRce to lib
|
||||
'@instructure/canvas-rce/es/rce/CanvasRce':
|
||||
'<rootDir>/packages/canvas-rce/lib/rce/CanvasRce.js',
|
||||
// mock the tinymce-react Editor react component
|
||||
'@tinymce/tinymce-react': '<rootDir>/packages/canvas-rce/src/rce/__mocks__/tinymceReact.js'
|
||||
},
|
||||
roots: ['ui', 'gems/plugins', 'public/javascripts'],
|
||||
moduleDirectories: ['ui/shims', 'public/javascripts', 'node_modules'],
|
||||
|
|
|
@ -258,6 +258,7 @@
|
|||
"jest-localstorage-mock": "^2",
|
||||
"jest-moxios-utils": "^1",
|
||||
"jest-raw-loader": "^1",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"json-loader": "^0.5.7",
|
||||
"karma": "^3",
|
||||
"karma-chrome-launcher": "^2",
|
||||
|
|
|
@ -32,10 +32,12 @@ module.exports = {
|
|||
setupFilesAfterEnv: ['<rootDir>/jest/jest-setup-framework.js'],
|
||||
testPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/lib', '<rootDir>/canvas'],
|
||||
testMatch: ['**/__tests__/**/?(*.)(spec|test).js'],
|
||||
modulePathIgnorePatterns: ['<rootDir>/lib', '<rootDir>/canvas'],
|
||||
modulePathIgnorePatterns: ['<rootDir>/es', '<rootDir>/lib', '<rootDir>/canvas'],
|
||||
testEnvironment: 'jest-environment-jsdom-fourteen',
|
||||
moduleNameMapper: {
|
||||
// jest can't import the icons
|
||||
'@instructure/ui-icons/es/svg': '<rootDir>/src/rce/__tests__/_mockIcons.js'
|
||||
'@instructure/ui-icons/es/svg': '<rootDir>/src/rce/__tests__/_mockIcons.js',
|
||||
// mock the tinymce-react Editor component
|
||||
'@tinymce/tinymce-react': '<rootDir>/src/rce/__mocks__/tinymceReact.js'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,6 @@ rm -rf canvas/*
|
|||
|
||||
yarn installTranslations
|
||||
|
||||
JEST_WORKER_ID=true ./node_modules/.bin/babel --out-dir lib src --ignore '**/__tests__'
|
||||
./node_modules/.bin/babel --out-dir es src --ignore '**/__tests__'
|
||||
JEST_WORKER_ID=true ./node_modules/.bin/babel --out-dir lib src --ignore '**/__tests__,**/__mocks__'
|
||||
./node_modules/.bin/babel --out-dir es src --ignore '**/__tests__,**/__mocks__'
|
||||
cp -r lib locales README.md package.json canvas
|
||||
|
|
|
@ -662,6 +662,8 @@ class RCEWrapper extends React.Component {
|
|||
|
||||
focus() {
|
||||
this.onTinyMCEInstance('mceFocus')
|
||||
// tinymce doesn't always call the focus handler.
|
||||
this.handleFocusEditor(new Event('focus', {target: this.mceInstance()}))
|
||||
}
|
||||
|
||||
focusCurrentView() {
|
||||
|
@ -1125,7 +1127,7 @@ class RCEWrapper extends React.Component {
|
|||
// If the editor is invisible for some reason, don't show the autosave modal
|
||||
// This doesn't apply if the editor is off-screen or has visibility:hidden;
|
||||
// only if it isn't rendered or has display:none;
|
||||
const editorVisible = this.editor.container.offsetParent
|
||||
const editorVisible = this.editor.getContainer().offsetParent
|
||||
|
||||
return (
|
||||
this.props.autosave.enabled &&
|
||||
|
@ -1489,7 +1491,7 @@ class RCEWrapper extends React.Component {
|
|||
this.observer = new MutationObserver((mutationList, _observer) => {
|
||||
mutationList.forEach(mutation => {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
this.handleFocusEditor(new FocusEvent('focus', {target: mutation.target}))
|
||||
this.handleFocusEditor(new Event('focus', {target: mutation.target}))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This is a mock for the @tinymce/tinymce-react Editor component
|
||||
* and the inner tinymce editor object
|
||||
* jest.config.js moduleNameMapper has jest load this
|
||||
* file in response to
|
||||
* import {Editor} from '@tinymce/tinymce-react'
|
||||
* in RCEWrapper.js
|
||||
*/
|
||||
|
||||
import React, {useEffect, useRef} from 'react'
|
||||
|
||||
class FakeEditor {
|
||||
constructor(textareaId) {
|
||||
this.hidden = true
|
||||
this._textareaId = textareaId
|
||||
this.readonly = undefined
|
||||
this._eventHandlers = {}
|
||||
}
|
||||
|
||||
execCommand(_cmd) {}
|
||||
|
||||
focus() {
|
||||
this.getElement().focus()
|
||||
}
|
||||
|
||||
getContainer() {
|
||||
return this.getElement().parentElement
|
||||
}
|
||||
|
||||
getElement() {
|
||||
return document.getElementById(this._textareaId)
|
||||
}
|
||||
|
||||
isHidden() {
|
||||
return this.hidden
|
||||
}
|
||||
|
||||
on(event, handler) {
|
||||
this._eventHandlers[event] = handler
|
||||
}
|
||||
|
||||
getBody() {}
|
||||
|
||||
getContent() {
|
||||
return this.getElement().value
|
||||
}
|
||||
|
||||
mode = {
|
||||
set: mode => {
|
||||
this.readonly = mode === 'readonly'
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content) {
|
||||
this.getElement().value = content
|
||||
this._eventHandlers.change?.({
|
||||
type: 'change',
|
||||
target: this.getElement()
|
||||
})
|
||||
}
|
||||
|
||||
selection = {
|
||||
collapse: () => {},
|
||||
select: () => {}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.hidden = true
|
||||
}
|
||||
|
||||
show() {
|
||||
this.hidden = false
|
||||
}
|
||||
}
|
||||
|
||||
export function Editor(props) {
|
||||
const editorRef = useRef(null)
|
||||
const textareaRef = useRef(null)
|
||||
const tinymceEditor = useRef(new FakeEditor(props.id))
|
||||
|
||||
useEffect(() => {
|
||||
window.tinymce.editors[0] = tinymceEditor.current
|
||||
tinymceEditor.current.on('change', handleChange)
|
||||
props.onInit && props.onInit({}, tinymceEditor.current)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
function handleChange(event) {
|
||||
props.onEditorChange?.(event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={editorRef}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
id={props.id}
|
||||
name={props.textareaName}
|
||||
value={props.initialValue}
|
||||
onInput={handleChange}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -20,25 +20,6 @@ import React, {createRef} from 'react'
|
|||
import {render, waitFor} from '@testing-library/react'
|
||||
import CanvasRce from '../CanvasRce'
|
||||
import bridge from '../../bridge'
|
||||
// even though CanvasRce imports tinymce, it doesn't get
|
||||
// properly initialized. I'm thinking jsdom doesn't have
|
||||
// enough juice for that to happen.
|
||||
import FakeEditor from '../plugins/shared/__tests__/FakeEditor'
|
||||
|
||||
const fakeTinyMCE = {
|
||||
init: () => {},
|
||||
triggerSave: () => 'called',
|
||||
execCommand: () => 'command executed',
|
||||
// plugins
|
||||
create: () => {},
|
||||
PluginManager: {
|
||||
add: () => {}
|
||||
},
|
||||
plugins: {
|
||||
AccessibilityChecker: {}
|
||||
},
|
||||
editors: [new FakeEditor('textarea3')]
|
||||
}
|
||||
|
||||
describe('CanvasRce', () => {
|
||||
let target
|
||||
|
@ -50,7 +31,6 @@ describe('CanvasRce', () => {
|
|||
document.body.appendChild(div)
|
||||
|
||||
target = document.getElementById('target')
|
||||
global.tinymce = fakeTinyMCE
|
||||
})
|
||||
afterEach(() => {
|
||||
document.body.removeChild(document.getElementById('fixture'))
|
||||
|
@ -58,22 +38,13 @@ describe('CanvasRce', () => {
|
|||
})
|
||||
|
||||
it('bridges newly rendered editors', async () => {
|
||||
render(<CanvasRce textareaId="textarea3" tinymce={fakeTinyMCE.editors[0]} />, target)
|
||||
render(<CanvasRce textareaId="textarea3" />, target)
|
||||
await waitFor(() => expect(bridge.activeEditor().constructor.displayName).toEqual('RCEWrapper'))
|
||||
})
|
||||
|
||||
it('supports getCode() and setCode() on its ref', async () => {
|
||||
const rceRef = createRef(null)
|
||||
fakeTinyMCE.editors[0].$container.innerHTML = 'Hello RCE!' // because it won't happen organically
|
||||
render(
|
||||
<CanvasRce
|
||||
ref={rceRef}
|
||||
textareaId="textarea3"
|
||||
tinymce={fakeTinyMCE}
|
||||
defaultContent="Hello RCE!"
|
||||
/>,
|
||||
target
|
||||
)
|
||||
render(<CanvasRce ref={rceRef} textareaId="textarea3" defaultContent="Hello RCE!" />, target)
|
||||
|
||||
await waitFor(() => expect(rceRef.current).not.toBeNull())
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
import ReactDOM from 'react-dom'
|
||||
import {renderIntoDiv} from '../root'
|
||||
import Bridge from '../../bridge'
|
||||
import FakeEditor from '../plugins/shared/__tests__/FakeEditor'
|
||||
|
||||
describe('RceModule', () => {
|
||||
let target
|
||||
|
@ -34,11 +33,11 @@ describe('RceModule', () => {
|
|||
target = document.getElementById('target')
|
||||
|
||||
props = {
|
||||
tinymce: new FakeEditor(),
|
||||
liveRegion: () => document.getElementById('flash_screenreader_holder'),
|
||||
editorOptions: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
textareaId: 'textarea_id'
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ describe('Upload data actions', () => {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
Bridge.focusEditor(null)
|
||||
successSource.uploadFRD.resetHistory()
|
||||
successSource.uploadFRD.returns(Promise.resolve(results))
|
||||
successSource.setUsageRights.resetHistory()
|
||||
|
@ -343,7 +344,6 @@ describe('Upload data actions', () => {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
Bridge.focusEditor(null)
|
||||
const baseState = getBaseState()
|
||||
store = spiedStore(baseState)
|
||||
props = {}
|
||||
|
|
|
@ -103,8 +103,8 @@ const CanvasRce = forwardRef(function CanvasRce(props, rceRef) {
|
|||
onBlur={onBlur}
|
||||
onContentChange={onContentChange}
|
||||
onInit={onInit}
|
||||
use_rce_pretty_html_editor={!!window.ENV?.FEATURES.rce_pretty_html_editor}
|
||||
use_rce_buttons_and_icons={!!window.ENV?.FEATURES.rce_buttons_and_icons}
|
||||
use_rce_pretty_html_editor={!!window.ENV?.FEATURES?.rce_pretty_html_editor}
|
||||
use_rce_buttons_and_icons={!!window.ENV?.FEATURES?.rce_buttons_and_icons}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -73,7 +73,7 @@ export default class EditorConfig {
|
|||
...defaultTinymceConfig,
|
||||
|
||||
body_class:
|
||||
window.ENV.FEATURES.canvas_k6_theme ||
|
||||
window.ENV.FEATURES?.canvas_k6_theme ||
|
||||
window.ENV.K5_SUBJECT_COURSE ||
|
||||
window.ENV.K5_HOMEROOM_COURSE
|
||||
? 'elementary-theme'
|
||||
|
|
Loading…
Reference in New Issue