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:
Ed Schiebel 2021-06-21 15:07:58 -04:00
parent 9bc1cd6353
commit c7af7b7e02
11 changed files with 149 additions and 45 deletions

View File

@ -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'],

View File

@ -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",

View File

@ -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'
}
}

View File

@ -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

View File

@ -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}))
}
})
})

View File

@ -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>
)
}

View File

@ -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())

View File

@ -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'
}
})

View File

@ -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 = {}

View File

@ -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}
/>
)

View File

@ -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'