/* * This file is part of ORY Editor. * * ORY Editor is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ORY Editor 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with ORY Editor. If not, see . * * @license LGPL-3.0 * @copyright 2016-2018 Aeneas Rekkas * @author Aeneas Rekkas * */ import { combineReducers, createStore, applyMiddleware } from 'redux'; import { rawEditableReducer } from '../index'; import * as actions from '../../../actions/cell/index'; import { decorate } from '../helper/tree'; import { cellOrder } from '../helper/order'; import thunk from 'redux-thunk'; const walker = ({ cells = [], rows = [], hover = null, ...other }) => { if (cells.length) { other.cells = cells.map(walker); } if (rows.length) { other.rows = rows.map(walker); } return { ...other, hover, }; }; const _cells = state => decorate(state).map(walker); const simulateDispatch = (currentState, action) => { const reducer = combineReducers({ editable: rawEditableReducer }); const store = createStore(reducer, currentState, applyMiddleware(thunk)); store.dispatch(action); return store.getState(); }; const runCase = (currentState, action, expectedState) => { const actualState = simulateDispatch(currentState, action); expect(actualState).toEqual({ editable: { ...expectedState.editable, cellOrder: cellOrder(expectedState.editable.cells), }, }); }; // tslint:disable-next-line:no-any export const createEditable = ( id: string, // tslint:disable-next-line:no-any cells?: any[] | { hover: any }[] ) => { // tslint:disable-next-line:no-any const editable: any = {}; if (id) { editable.id = id; } if (cells) { editable.cells = cells; } return { editable }; }; export const createCell = ( id: string, // tslint:disable-next-line:no-any rows: any[], // tslint:disable-next-line:no-any additional?: any ) => { // tslint:disable-next-line:no-any const cell: any = {}; if (id) { cell.id = id; } if (rows) { cell.rows = rows; } return { ...cell, ...additional, }; }; export const createLayoutCell = ( id: string, name: string, state: { foo: number; bar?: number }, // tslint:disable-next-line:no-any rows: any[], // tslint:disable-next-line:no-any additional?: any ) => { const cell = createCell(id, null, additional); // tslint:disable-next-line:no-any const layout: any = {}; if (name) { layout.plugin = {}; layout.plugin.name = name; } if (state) { layout.state = state; } if (rows) { cell.rows = rows; } return { ...cell, layout, }; }; export const createContentCell = ( id: string, name: string, state?: { foo: number; bar?: number }, additional?: { hover?: string | boolean; size?: number; inline?: string; focusSource?: string; focused?: boolean; } ) => { const cell = createCell(id, null, additional); // tslint:disable-next-line:no-any const content: any = {}; if (name) { content.plugin = {}; content.plugin.name = name; } if (state) { content.state = state; } return { ...cell, content, }; }; // tslint:disable-next-line:no-any export const createRow = (id: string, cells: any[], additional: any = {}) => { // tslint:disable-next-line:no-any const row: any = {}; if (id) { row.id = id; } if (cells) { row.cells = cells; } return { hasInlineChildren: false, ...row, ...additional, }; }; test('basic', () => { const currentState = createEditable('editable', undefined); const action = { type: 'foo' }; const expectedState = createEditable('editable', []); runCase(currentState, action, expectedState); }); test('cleanup does not remove layout nodes when having one child, nested', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createCell('000', [ createRow('0000', [ createLayoutCell('layout', 'layout', null, [ createRow('00000', [createContentCell('000000', 'foo')]), ]), ]), ]), ]), ]), ]); const action = { type: 'foo' }; const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [ createLayoutCell('layout', 'layout', null, [ createRow('00000', [createContentCell('000000', 'foo')]), ]), ]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cleanup does not remove layout nodes when having multiple cells in one row, nested', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createCell('000', [ createRow('0000', [ createLayoutCell('layout', 'layout', null, [ createRow('00000', [createContentCell('000000', 'foo', null)]), createRow('00001', [createContentCell('000010', 'bar', null)]), ]), ]), ]), ]), ]), ]); const action = { type: 'foo' }; const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [ createLayoutCell('layout', 'layout', null, [ createRow('00000', [createContentCell('000000', 'foo', null)]), createRow('00001', [createContentCell('000010', 'bar', null)]), ]), ]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell update content', () => { const currentState = createEditable('editable', [ createContentCell('0', 'foo', { foo: 1 }), ]); const action = actions.updateCellContent('0')({ bar: 1 }); const expectedState = createEditable( 'editable', _cells([createContentCell('0', 'foo', { bar: 1, foo: 1 })]) ); runCase(currentState, action, expectedState); }); test('cell update layout', () => { const currentState = createEditable('editable', [ createLayoutCell('0', 'foo', { foo: 1 }, [ createRow('2', [createContentCell('1', 'bar')]), ]), ]); const action = actions.updateCellLayout('0')({ bar: 1 }); const expectedState = createEditable( 'editable', _cells([ createLayoutCell('0', 'foo', { foo: 1, bar: 1 }, [ createRow('2', [createContentCell('1', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell remove', () => { const currentState = createEditable('editable', [ createContentCell('0', 'foo'), createContentCell('1', 'bar'), ]); const action = actions.removeCell('0'); const expectedState = createEditable( 'editable', _cells([createContentCell('1', 'bar')]) ); runCase(currentState, action, expectedState); }); test('last cell remove', () => { const currentState = createEditable('editable', [ createContentCell('0', 'foo'), ]); const action = actions.removeCell('0'); const actualState = simulateDispatch(currentState, action); expect(actualState.editable.cells.length).toEqual(0); }); test('cell cancel drag', () => { const currentState = createEditable('editable', [ createContentCell('0', 'foo', null, { hover: true }), ]); const action = actions.cancelCellDrag(); const expectedState = createEditable( 'editable', _cells([createContentCell('0', 'foo')]) ); runCase(currentState, action, expectedState); }); test('cell resize', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createContentCell('000', 'foo', null, { size: 6 }), createContentCell('001', 'bar', null, { size: 6 }), ]), ]), ]); const action = actions.resizeCell('000')(4); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [ createContentCell('000', 'foo', null, { size: 4 }), createContentCell('001', 'bar', null, { size: 8 }), ]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell resize inline cell (1)', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ]), ]), ]); const action = actions.resizeCell('000')(4); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow( '00', [ createContentCell('000', 'foo', null, { inline: 'left', size: 4 }), createContentCell('001', 'bar', null, { size: 12 }), ], { hasInlineChildren: true } ), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell hover real row', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.cellHoverLeftOf({ id: 'foo' }, { id: '00' }, 0); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [createContentCell('000', 'foo')], { hover: 'left-of', }), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell hover row', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.cellHoverLeftOf({ id: 'foo' }, { id: '000' }, 1); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [createContentCell('000', 'foo')], { hover: 'left-of', }), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell hover ancestor cell', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.cellHoverRightOf({ id: 'foo' }, { id: '000' }, 2); const expectedState = createEditable( 'editable', _cells([ createCell( '0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ], { hover: 'right-of' } ), ]) ); runCase(currentState, action, expectedState); }); test('insert cell right of, clean up tree afterwards', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createCell('000', [ createRow('0000', [ createCell('00000', [ createRow('000000', [ createCell('000000', [ createRow('0000000', [createContentCell('00000000', 'foo')]), createRow('0000001', [createContentCell('00000010', 'bar')]), ]), ]), ]), ]), ]), ]), ]), ]); const action = actions.insertCellRightOf( createContentCell('i', 'myPlugin'), { id: '00000000' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('0000000', [ createContentCell('id-others-1', 'foo'), createContentCell('id-item', 'myPlugin'), ]), createRow('0000001', [createContentCell('00000010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('anti-recursion test: cell insert below of two level', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createCell('000', [ createRow('0000', [ createCell('00000', [ createRow('000000', [ createCell('000000', [ createRow('0000000', [createContentCell('00000000', 'foo')]), createRow('0000001', [createContentCell('00000010', 'bar')]), ]), ]), ]), ]), ]), ]), ]), ]); const action = actions.insertCellBelow( createContentCell('i', 'myPlugin'), { id: '00000000' }, 2, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell( 'id-cell', [ createRow('0000000', [createContentCell('00000000', 'foo')]), createRow('0000001', [createContentCell('00000010', 'bar')]), createRow('id-others-3', [createContentCell('id-item', 'myPlugin')], { hasInlineChildren: false, }), ], { focusSource: '', focused: false, } ), ]) ); runCase(currentState, action, expectedState); }); test('cell insert right of cell', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.insertCellRightOf( createContentCell('i', 'myPlugin'), { id: '000' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [ createContentCell('id-others-1', 'foo'), createContentCell('id-item', 'myPlugin'), ]), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert below of cell - one level deep (row)', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.insertCellBelow( createContentCell('i', 'myPlugin'), { id: '000' }, 1, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('id-others-1', [createContentCell('000', 'foo')]), createRow('id-others-2', [createContentCell('id-item', 'myPlugin')], { hasInlineChildren: false, }), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert left of cell - one level deep (row)', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.insertCellLeftOf( createContentCell('i', 'myPlugin'), { id: '000' }, 1, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [ createContentCell('id-item', 'myPlugin'), createContentCell('000', 'foo'), ]), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert left of cell', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.insertCellLeftOf( createContentCell('i', 'myPlugin'), { id: '000' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [ createContentCell('id-item', 'myPlugin'), createContentCell('id-others-1', 'foo'), ]), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert above cell', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.insertCellAbove( createContentCell('i', 'myPlugin'), { id: '000' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('id-others-1', [createContentCell('id-item', 'myPlugin')], { hasInlineChildren: false, }), createRow('id-others-2', [createContentCell('id-others-3', 'foo')], { hasInlineChildren: false, }), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert below cell', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.insertCellBelow( createContentCell('i', 'myPlugin'), { id: '000' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('id-others-1', [createContentCell('id-others-2', 'foo')], { hasInlineChildren: false, }), createRow('id-others-3', [createContentCell('id-item', 'myPlugin')], { hasInlineChildren: false, }), createRow('01', [createContentCell('010', 'bar')]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell move below another cell', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [createContentCell('000', 'foo')]), createRow('01', [createContentCell('010', 'bar')]), ]), ]); const action = actions.insertCellBelow( createContentCell('000', 'foo'), { id: '010' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('id-others-1', [createContentCell('id-others-2', 'bar')], { hasInlineChildren: false, }), createRow('id-others-3', [createContentCell('id-item', 'foo')], { hasInlineChildren: false, }), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert inline cell left of', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createContentCell('000', 'foo'), createContentCell('001', 'bar'), ]), ]), ]); const action = actions.insertCellLeftInline( createContentCell('i', 'myPlugin'), { id: '000' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow('00', [ createCell( 'id-cell', [ createRow( 'id-others-1', [ createContentCell('id-item', 'myPlugin', null, { inline: 'left', }), createContentCell('id-others-2', 'foo', null, { inline: null, }), // FIXME: the row with id i00 has inline children! ], { hasInlineChildren: true } ), ], { focusSource: '', focused: false, } ), createContentCell('001', 'bar'), ]), ]), ]) ); runCase(currentState, action, expectedState); }); test('move inline cell from left to right', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ]), ]), ]); const action = actions.insertCellRightInline( createContentCell('000', 'foo', null, { inline: 'left' }), { id: '001' }, 0, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow( 'id-others-1', [ createContentCell('id-item', 'foo', null, { inline: 'right' }), createContentCell('id-others-2', 'bar', null, { inline: null }), ], { hasInlineChildren: true } ), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert cell left of inline row', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ]), ]), ]); const action = actions.insertCellLeftOf( createContentCell('i', 'myPlugin'), { id: '000' }, 2, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createContentCell('id-item', 'myPlugin', null, { size: 6 }), createCell('id-others-1', [ createRow( '00', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ], { hasInlineChildren: true } ), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert below inline row', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ]), ]), ]); const action = actions.insertCellBelow( createContentCell('i', 'myPlugin'), { id: '000' }, 1, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell('0', [ createRow( 'id-others-1', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ], { hasInlineChildren: true } ), createRow('id-others-2', [ createContentCell('id-item', 'myPlugin', null, { size: 6 }), ]), ]), ]) ); runCase(currentState, action, expectedState); }); test('cell insert below inline row - 2 level', () => { const currentState = createEditable('editable', [ createCell('0', [ createRow('00', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ]), ]), ]); const action = actions.insertCellBelow( createContentCell('i', 'myPlugin'), { id: '000' }, 2, { cell: 'id-cell', item: 'id-item', others: ['id-others-1', 'id-others-2', 'id-others-3'], } ); const expectedState = createEditable( 'editable', _cells([ createCell( 'id-cell', [ createRow( '00', [ createContentCell('000', 'foo', null, { inline: 'left' }), createContentCell('001', 'bar', null), ], { hasInlineChildren: true } ), createRow('id-others-3', [ createContentCell('id-item', 'myPlugin', null, { size: 6 }), ]), ], { focusSource: '', focused: false, } ), ]) ); runCase(currentState, action, expectedState); });