diff --git a/src/schema/data/actions.js b/src/schema/data/actions.js index e54d68a..001d39b 100644 --- a/src/schema/data/actions.js +++ b/src/schema/data/actions.js @@ -4,12 +4,8 @@ */ export const SET_SCHEMA_CURRENT_DEFINITION_NAME = 'SET_SCHEMA_CURRENT_DEFINITION_NAME'; -export const UPDATE_SCHEMA_STATE = 'UPDATE_SCHEMA_STATE'; export function setSchemaCurrentDefinitionName( currentSchemaDefinitionName ) { return { type: SET_SCHEMA_CURRENT_DEFINITION_NAME, currentSchemaDefinitionName }; } -export function updateSchemaState() { - return { type: UPDATE_SCHEMA_STATE }; -} diff --git a/src/schema/data/reducer.js b/src/schema/data/reducer.js index 9a7cf3d..0573992 100644 --- a/src/schema/data/reducer.js +++ b/src/schema/data/reducer.js @@ -4,8 +4,7 @@ */ import { - SET_SCHEMA_CURRENT_DEFINITION_NAME, - UPDATE_SCHEMA_STATE + SET_SCHEMA_CURRENT_DEFINITION_NAME } from './actions'; import { @@ -42,7 +41,6 @@ export default function schemaReducer( globalState, schemaState, action ) { // if we're back in the commands tab. // * UPDATE_MODEL_STATE – An action called by the editorEventObserver for the model document change. case SET_ACTIVE_INSPECTOR_TAB: - case UPDATE_SCHEMA_STATE: return { ...schemaState, diff --git a/src/schema/pane.js b/src/schema/pane.js index 44aa360..b67ce54 100644 --- a/src/schema/pane.js +++ b/src/schema/pane.js @@ -5,7 +5,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { updateSchemaState } from './data/actions'; import Pane from '../components/pane'; import Tabs from '../components/tabs'; @@ -37,4 +36,4 @@ const mapStateToProps = ( { currentEditorName } ) => { return { currentEditorName }; }; -export default connect( mapStateToProps, { updateSchemaState } )( SchemaPane ); +export default connect( mapStateToProps )( SchemaPane ); diff --git a/tests/inspector/schema/data/actions.js b/tests/inspector/schema/data/actions.js new file mode 100644 index 0000000..619905d --- /dev/null +++ b/tests/inspector/schema/data/actions.js @@ -0,0 +1,18 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { + setSchemaCurrentDefinitionName, + SET_SCHEMA_CURRENT_DEFINITION_NAME +} from '../../../../src/schema/data/actions'; + +describe( 'schema data store actions', () => { + it( 'should export setSchemaCurrentDefinitionName()', () => { + expect( setSchemaCurrentDefinitionName( 'foo' ) ).to.deep.equal( { + type: SET_SCHEMA_CURRENT_DEFINITION_NAME, + currentSchemaDefinitionName: 'foo' + } ); + } ); +} ); diff --git a/tests/inspector/schema/data/reducer.js b/tests/inspector/schema/data/reducer.js new file mode 100644 index 0000000..853aa56 --- /dev/null +++ b/tests/inspector/schema/data/reducer.js @@ -0,0 +1,180 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document, window */ + +import TestEditor from '../../../utils/testeditor'; +import schemaReducer from '../../../../src/schema/data/reducer'; + +import { + setSchemaCurrentDefinitionName +} from '../../../../src/schema/data/actions'; + +import { + setActiveTab, + setEditors, + setCurrentEditorName +} from '../../../../src/data/actions'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; + +describe( 'schema data store reducer', () => { + let editorA, editorB; + let elementA, elementB; + let globalState, schemaState; + + beforeEach( async () => { + window.localStorage.clear(); + + elementA = document.createElement( 'div' ); + elementB = document.createElement( 'div' ); + document.body.appendChild( elementA ); + document.body.appendChild( elementB ); + + editorA = await TestEditor.create( elementA, { + plugins: [ Paragraph ], + initialData: '

foo

' + } ); + + editorB = await TestEditor.create( elementB ); + + globalState = { + currentEditorName: 'a', + editors: new Map( [ + [ 'a', editorA ], + [ 'b', editorB ] + ] ), + ui: { + activeTab: 'Schema' + } + }; + + schemaState = schemaReducer( globalState, null, {} ); + } ); + + afterEach( async () => { + await editorA.destroy(); + await editorB.destroy(); + } ); + + it( 'should not create a state if ui#activeTab is different than "Schema"', () => { + globalState.ui.activeTab = 'Model'; + + schemaState = schemaReducer( globalState, null, {} ); + + expect( schemaState ).to.be.null; + } ); + + it( 'should create a default state if no schema state was passed to the reducer', () => { + schemaState = schemaReducer( globalState, null, {} ); + + expect( schemaState ).to.have.property( 'treeDefinition' ); + expect( schemaState ).to.have.property( 'currentSchemaDefinitionName' ); + expect( schemaState ).to.have.property( 'currentSchemaDefinition' ); + } ); + + it( 'should pass through when no action was passed to the reducer', () => { + schemaState = schemaReducer( globalState, { + treeDefinition: [ 'foo' ], + currentSchemaDefinitionName: 'bar', + currentSchemaDefinition: 'baz' + }, {} ); + + expect( schemaState.treeDefinition ).to.deep.equal( [ 'foo' ] ); + expect( schemaState.currentSchemaDefinitionName ).to.equal( 'bar' ); + expect( schemaState.currentSchemaDefinition ).to.equal( 'baz' ); + } ); + + describe( 'application state', () => { + describe( '#currentSchemaDefinitionName', () => { + it( 'should be reset on setEditors() action', () => { + schemaState.currentSchemaDefinitionName = 'paragraph'; + schemaState = schemaReducer( globalState, schemaState, setEditors( new Map( [ [ 'b', editorB ] ] ) ) ); + + expect( schemaState.currentSchemaDefinitionName ).to.be.null; + } ); + + it( 'should be reset on setCurrentEditorName() action', () => { + schemaState.currentSchemaDefinitionName = null; + schemaState = schemaReducer( globalState, schemaState, setCurrentEditorName( 'b' ) ); + + expect( schemaState.currentSchemaDefinitionName ).to.be.null; + } ); + + it( 'should be set on setSchemaCurrentDefinitionName() action', () => { + schemaState.currentSchemaDefinitionName = null; + schemaState = schemaReducer( globalState, schemaState, setSchemaCurrentDefinitionName( 'paragraph' ) ); + + expect( schemaState.currentSchemaDefinitionName ).to.equal( 'paragraph' ); + } ); + } ); + + describe( '#currentSchemaDefinition', () => { + it( 'should be reset on setEditors() action', () => { + schemaState.currentSchemaDefinition = null; + schemaState = schemaReducer( globalState, schemaState, setEditors( new Map( [ [ 'b', editorB ] ] ) ) ); + + expect( schemaState.currentSchemaDefinition ).to.be.null; + } ); + + it( 'should be reset on setCurrentEditorName() action', () => { + schemaState.currentSchemaDefinition = null; + schemaState = schemaReducer( globalState, schemaState, setCurrentEditorName( 'b' ) ); + + expect( schemaState.currentSchemaDefinition ).to.be.null; + } ); + + it( 'should be set on setSchemaCurrentDefinitionName() action', () => { + schemaState.currentSchemaDefinition = null; + schemaState = schemaReducer( globalState, schemaState, setSchemaCurrentDefinitionName( 'paragraph' ) ); + + expect( schemaState.currentSchemaDefinition ).to.be.an( 'object' ); + } ); + + it( 'should be set on setActiveTab() action', () => { + schemaState.currentSchemaDefinitionName = 'paragraph'; + schemaState.currentSchemaDefinition = null; + schemaState = schemaReducer( globalState, schemaState, setActiveTab( 'Schema' ) ); + + expect( schemaState.currentSchemaDefinition ).to.be.an( 'object' ); + } ); + } ); + + describe( '#treeDefinition', () => { + it( 'should be empty if there are no editors', () => { + schemaState.treeDefinition = null; + + globalState.editors = new Map(); + globalState.currentEditorName = null; + + schemaState = schemaReducer( globalState, schemaState, setEditors( new Map() ) ); + + expect( schemaState.treeDefinition ).to.be.an( 'array' ); + expect( schemaState.treeDefinition ).to.have.length( 0 ); + } ); + + it( 'should be set on setEditors() action', () => { + schemaState.treeDefinition = null; + schemaState = schemaReducer( globalState, schemaState, setEditors( new Map( [ [ 'b', editorB ] ] ) ) ); + + expect( schemaState.treeDefinition ).to.be.an( 'array' ); + } ); + + it( 'should be set on setCurrentEditorName() action', () => { + schemaState.treeDefinition = null; + schemaState = schemaReducer( globalState, schemaState, setCurrentEditorName( 'b' ) ); + + expect( schemaState.treeDefinition ).to.be.an( 'array' ); + } ); + + it( 'should be set on setActiveTab() action', () => { + schemaState.treeDefinition = null; + schemaState = schemaReducer( globalState, schemaState, setActiveTab( 'Commands' ) ); + + expect( schemaState.treeDefinition ).to.be.an( 'array' ); + } ); + } ); + } ); +} ); diff --git a/tests/inspector/schema/pane.js b/tests/inspector/schema/pane.js new file mode 100644 index 0000000..118b9c7 --- /dev/null +++ b/tests/inspector/schema/pane.js @@ -0,0 +1,81 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document, window */ + +import React from 'react'; +import TestEditor from '../../utils/testeditor'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; + +import SchemaPane from '../../../src/schema/pane'; +import SchemaTree from '../../../src/schema/tree'; +import SchemaDefinitionInspector from '../../../src/schema/schemadefinitioninspector'; + +describe( '', () => { + let editor, wrapper, element, store; + + beforeEach( () => { + window.localStorage.clear(); + + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return TestEditor.create( element ).then( newEditor => { + editor = newEditor; + + store = createStore( state => state, { + editors: new Map( [ [ 'test-editor', editor ] ] ), + currentEditorName: 'test-editor', + ui: { + activeTab: 'Schema' + }, + schema: { + } + } ); + + wrapper = mount( ); + } ); + } ); + + afterEach( () => { + wrapper.unmount(); + element.remove(); + + return editor.destroy(); + } ); + + describe( 'render()', () => { + it( 'renders a placeholder when no props#currentEditorName', () => { + store = createStore( state => state, { + currentEditorName: null, + model: { + ui: {} + } + } ); + + const wrapper = mount( ); + + expect( wrapper.text() ).to.match( /^Nothing to show/ ); + + wrapper.unmount(); + } ); + + it( 'should render ', () => { + const tabs = wrapper.find( 'Tabs' ); + + expect( tabs ).to.have.length( 1 ); + expect( tabs.props().activeTab ).to.equal( 'Inspect' ); + } ); + + it( 'should render a ', () => { + expect( wrapper.find( SchemaTree ) ).to.have.length( 1 ); + } ); + + it( 'should render a ', () => { + expect( wrapper.find( SchemaDefinitionInspector ) ).to.have.length( 1 ); + } ); + } ); +} ); diff --git a/tests/inspector/schema/schemadefinitioninspector.js b/tests/inspector/schema/schemadefinitioninspector.js new file mode 100644 index 0000000..7df9e32 --- /dev/null +++ b/tests/inspector/schema/schemadefinitioninspector.js @@ -0,0 +1,186 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document */ + +import React from 'react'; +import TestEditor from '../../utils/testeditor'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; + +import { getSchemaDefinition } from '../../../src/schema/data/utils'; + +import { reducer } from '../../../src/data/reducer'; +import ObjectInspector from '../../../src/components/objectinspector'; +import SchemaDefinitionInspector from '../../../src/schema/schemadefinitioninspector'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; + +describe( '', () => { + let editor, wrapper, element, store; + + beforeEach( async () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await TestEditor.create( element, { + plugins: [ + Paragraph, + function( editor ) { + this.afterInit = () => { + editor.model.schema.extend( 'paragraph', { + allowAttributes: [ 'foo' ] + } ); + + editor.model.schema.setAttributeProperties( 'foo', { + someProperty: 123 + } ); + }; + } + ] + } ); + + const editors = new Map( [ [ 'test-editor', editor ] ] ); + const currentEditorName = 'test-editor'; + + store = createStore( reducer, { + editors, + currentEditorName, + ui: { + activeTab: 'Schema' + }, + schema: { + currentSchemaDefinitionName: 'paragraph', + currentSchemaDefinition: getSchemaDefinition( { editors, currentEditorName }, 'paragraph' ), + treeDefinition: null + } + } ); + + wrapper = mount( ); + } ); + + afterEach( async () => { + wrapper.unmount(); + element.remove(); + sinon.restore(); + + await editor.destroy(); + } ); + + describe( 'render()', () => { + it( 'should render a placeholder when no props#currentSchemaDefinition', () => { + const store = createStore( state => state, { + editors: new Map( [ [ 'test-editor', editor ] ] ), + currentEditorName: 'test-editor', + schema: { + currentSchemaDefinition: null + } + } ); + + const wrapper = mount( ); + + expect( wrapper.childAt( 0 ).text() ).to.match( /^Select a schema definition/ ); + + wrapper.unmount(); + } ); + + it( 'should render an object inspector when there is props#currentSchemaDefinition', () => { + expect( wrapper.find( ObjectInspector ) ).to.have.length( 1 ); + } ); + + describe( 'scheme definition info', () => { + it( 'should render schema definition properties', () => { + wrapper.setProps( { currentSchemaDefinitionName: 'paragraph' } ); + + const inspector = wrapper.find( ObjectInspector ); + const lists = inspector.props().lists; + + expect( lists[ 0 ].name ).to.equal( 'Properties' ); + expect( lists[ 0 ].url ).to.match( /^https:\/\/ckeditor.com\/docs\// ); + expect( lists[ 0 ].itemDefinitions ).to.deep.equal( { + isBlock: { value: 'true' } + } ); + } ); + + it( 'should render schema definition allowed attributes', () => { + wrapper.setProps( { currentSchemaDefinitionName: 'paragraph' } ); + + const inspector = wrapper.find( ObjectInspector ); + const lists = inspector.props().lists; + + expect( lists[ 1 ].name ).to.equal( 'Allowed attributes' ); + expect( lists[ 1 ].url ).to.match( /^https:\/\/ckeditor.com\/docs\// ); + expect( lists[ 1 ].itemDefinitions ).to.deep.equal( { + foo: { + subProperties: { + someProperty: { + value: '123' + } + }, + value: 'true' + } + } ); + } ); + + it( 'should render schema definition allowed children', () => { + wrapper.setProps( { currentSchemaDefinitionName: 'paragraph' } ); + + const inspector = wrapper.find( ObjectInspector ); + const lists = inspector.props().lists; + + expect( lists[ 2 ].name ).to.equal( 'Allowed children' ); + expect( lists[ 2 ].url ).to.match( /^https:\/\/ckeditor.com\/docs\// ); + expect( lists[ 2 ].itemDefinitions ).to.deep.equal( { + $text: { value: 'true' } + } ); + } ); + + it( 'should render schema definition allowed in', () => { + wrapper.setProps( { currentSchemaDefinitionName: 'paragraph' } ); + + const inspector = wrapper.find( ObjectInspector ); + const lists = inspector.props().lists; + + expect( lists[ 3 ].name ).to.equal( 'Allowed in' ); + expect( lists[ 3 ].url ).to.match( /^https:\/\/ckeditor.com\/docs\// ); + expect( lists[ 3 ].itemDefinitions ).to.deep.equal( { + $clipboardHolder: { value: 'true' }, + $documentFragment: { value: 'true' }, + $root: { value: 'true' } + } ); + } ); + } ); + + it( 'should navigate to another definition upon clicking a name in "allowed children"', () => { + wrapper.setProps( { currentSchemaDefinitionName: 'paragraph' } ); + + const inspector = wrapper.find( ObjectInspector ); + const propertyTitleLabel = inspector + .find( 'PropertyTitle' ) + .filter( { name: '$text' } ) + .first() + .find( 'label' ); + + propertyTitleLabel.simulate( 'click' ); + + expect( store.getState().schema.currentSchemaDefinitionName ).to.equal( '$text' ); + } ); + + it( 'should navigate to another definition upon clicking a name in "allowed in"', () => { + wrapper.setProps( { currentSchemaDefinitionName: 'paragraph' } ); + + const inspector = wrapper.find( ObjectInspector ); + const propertyTitleLabel = inspector + .find( 'PropertyTitle' ) + .filter( { name: '$root' } ) + .first() + .find( 'label' ); + + propertyTitleLabel.simulate( 'click' ); + + expect( store.getState().schema.currentSchemaDefinitionName ).to.equal( '$root' ); + } ); + } ); +} ); diff --git a/tests/inspector/schema/tree.js b/tests/inspector/schema/tree.js new file mode 100644 index 0000000..e1241de --- /dev/null +++ b/tests/inspector/schema/tree.js @@ -0,0 +1,119 @@ +/** + * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document */ + +import React from 'react'; +import TestEditor from '../../utils/testeditor'; +import { createStore } from 'redux'; +import { Provider } from 'react-redux'; + +import { getSchemaTreeDefinition } from '../../../src/schema/data/utils'; + +import { reducer } from '../../../src/data/reducer'; +import Tree from '../../../src/components/tree/tree.js'; +import SchemaTree from '../../../src/schema/tree'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; + +describe( '', () => { + let editor, wrapper, element, store; + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return TestEditor.create( element, { + plugins: [ Paragraph ] + } ).then( newEditor => { + editor = newEditor; + + const editors = new Map( [ [ 'test-editor', editor ] ] ); + const currentEditorName = 'test-editor'; + + store = createStore( reducer, { + editors, + currentEditorName, + ui: { + activeTab: 'Schema' + }, + schema: { + currentSchemaDefinitionName: 'paragraph', + treeDefinition: getSchemaTreeDefinition( { editors, currentEditorName } ) + } + } ); + + wrapper = mount( ); + } ); + } ); + + afterEach( () => { + wrapper.unmount(); + element.remove(); + + return editor.destroy(); + } ); + + describe( 'render()', () => { + it( 'should use a component', () => { + const tree = wrapper.find( Tree ); + const schemaTree = wrapper.find( 'SchemaTree' ); + + expect( tree.props().definition ).to.equal( schemaTree.props().treeDefinition ); + expect( tree.props().onClick ).to.equal( schemaTree.instance().handleTreeClick ); + expect( tree.props().activeNode ).to.equal( 'paragraph' ); + } ); + + it( 'should render a with schema items in alphabetical order', () => { + const tree = wrapper.find( Tree ); + + // Note: Asserting just a few. There are plenty of them and they will change as the editor develops. + expect( tree.props().definition ).to.include.deep.members( [ + { + attributes: [], + children: [], + name: '$root', + node: '$root', + presentation: { + cssClass: 'ck-inspector-tree-node_tagless', + isEmpty: true + }, + type: 'element' + }, + { + attributes: [], + children: [], + name: '$text', + node: '$text', + presentation: { + cssClass: 'ck-inspector-tree-node_tagless', + isEmpty: true + }, + type: 'element' + }, + { + attributes: [], + children: [], + name: 'paragraph', + node: 'paragraph', + presentation: { + cssClass: 'ck-inspector-tree-node_tagless', + isEmpty: true + }, + type: 'element' + } + ] ); + } ); + + it( 'should start inspecting a schema definition when an item was clicked', () => { + const tree = wrapper.find( Tree ); + const element = tree.find( 'TreeElement' ).first(); + + element.simulate( 'click' ); + + expect( store.getState().schema.currentSchemaDefinitionName ).to.equal( '$block' ); + } ); + } ); +} );