From 163b1d21e09aa057855019275fb0443898bbd6ad Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 11 Nov 2020 21:41:43 +0000 Subject: [PATCH] Add basic selection tests --- .flowconfig | 1 + .watchmanconfig | 1 + babel.config.js | 5 + package.json | 5 +- .../src/__tests__/OutlineSelection-test.js | 219 ++++++++++++++++++ yarn.lock | 28 ++- 6 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 .watchmanconfig create mode 100644 babel.config.js create mode 100644 packages/outline/src/__tests__/OutlineSelection-test.js diff --git a/.flowconfig b/.flowconfig index 90509554688..7962986bf67 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,6 +1,7 @@ [ignore] .*/scripts/.* .*/build/.* +.*/__tests__/.* .*/dist/.* .*/.tempUserDataDir/.* .*/node_modules/.* diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000000..df08db5216f --- /dev/null +++ b/babel.config.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + presets: ['@babel/preset-env', '@babel/preset-react'], +}; diff --git a/package.json b/package.json index 8f746fc3adb..6b1d15eae62 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "build": "node scripts/build.js", "build-prod": "node scripts/build.js --prod", "watch": "node scripts/build.js --watch", - "flow": "node ./scripts/tasks/flow.js" + "flow": "node ./scripts/tasks/flow.js", + "test": "jest", + "debug-test": "node --inspect-brk node_modules/.bin/jest --runInBand" }, "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.12.1", @@ -38,6 +40,7 @@ "jest": "26.6.0", "minimist": "^1.2.5", "prettier": "^2.1.2", + "react-test-renderer": "^17.0.1", "rollup": "^2.33.1" } } diff --git a/packages/outline/src/__tests__/OutlineSelection-test.js b/packages/outline/src/__tests__/OutlineSelection-test.js new file mode 100644 index 00000000000..ada844a990f --- /dev/null +++ b/packages/outline/src/__tests__/OutlineSelection-test.js @@ -0,0 +1,219 @@ +let container = null; +let React; +let ReactDOM; +let ReactTestUtils; +let Outline; + +function sanitizeHTML(html) { + // Remove the special space characters + return html.replace(/\uFEFF/g, ''); +} + +function insertText(text) { + return { + type: 'insert_text', + text, + }; +} + +function deleteBackward() { + return { + type: 'delete_backward', + text: null, + }; +} + +function deleteForward() { + return { + type: 'delete_forward', + text: null, + }; +} + +function setNativeSelection( + anchorPath, + anchorOffset, + focusPath, + focusOffset, + isCollapsed = false, +) { + return { + type: 'set_native_selection', + anchorPath, + anchorOffset, + focusPath, + focusOffset, + isCollapsed, + }; +} + +function getNodeFromPath(path, editorElement) { + let node = editorElement; + for (let i = 0; i < path.length; i++) { + node = node.childNodes[path[i]]; + } + return node; +} + +function updateNativeSelection( + editorElement, + anchorPath, + anchorOffset, + focusPath, + focusOffset, + isCollapsed = false, +) { + const anchorNode = getNodeFromPath(anchorPath, editorElement); + const focusNode = getNodeFromPath(focusPath, editorElement); + const domSelection = window.getSelection(); + const range = document.createRange(); + range.collapse(isCollapsed); + range.setStart(anchorNode, anchorOffset); + range.setEnd(focusNode, focusOffset); + domSelection.removeAllRanges(); + domSelection.addRange(range); +} + +function applySelectionInputs(inputs, update, editor) { + const editorElement = editor.getEditorElement(); + inputs.forEach((input) => { + update((view) => { + const selection = view.getSelection(); + + switch (input.type) { + case 'insert_text': { + selection.insertText(input.text); + break; + } + case 'delete_backward': { + selection.deleteBackward(); + break; + } + case 'delete_forward': { + selection.deleteForward(); + break; + } + case 'set_native_selection': { + updateNativeSelection( + editorElement, + input.anchorPath, + input.anchorOffset, + input.focusPath, + input.focusOffset, + input.isCollapsed, + ); + break; + } + default: + console.log('TODO'); + } + }); + }); +} + +describe('OutlineSelection tests', () => { + beforeEach(() => { + React = require('react'); + ReactDOM = require('react-dom'); + ReactTestUtils = require('react-dom/test-utils'); + Outline = require('outline'); + + container = document.createElement('div'); + document.body.appendChild(container); + init(); + }); + + let editor = null; + + function init() { + const ref = React.createRef(); + + function TestBase() { + editor = Outline.useOutlineEditor(ref); + return
; + } + + ReactTestUtils.act(() => { + ReactDOM.render(, container); + }); + ref.current.focus(); + + // Insert initial block + update((view) => { + const paragraph = Outline.createParagraph(); + const text = Outline.createText(); + paragraph.append(text); + view.getBody().append(paragraph); + }); + + // Focus first element + updateNativeSelection(ref.current, [0, 0, 0], 0, [0, 0, 0], 0); + } + + function update(callback) { + const viewModel = editor.createViewModel(callback); + editor.update(viewModel, true); + } + + afterEach(() => { + document.body.removeChild(container); + container = null; + }); + + test('Expect initial output to be a block with some text', () => { + expect(sanitizeHTML(container.innerHTML)).toBe( + '


', + ); + }); + + const suite = [ + { + inputs: [ + insertText('H'), + insertText('e'), + insertText('l'), + insertText('l'), + insertText('o'), + ], + result: + '

Hello

', + }, + { + inputs: [ + insertText('1'), + insertText('2'), + insertText('3'), + deleteBackward(), + insertText('4'), + insertText('5'), + deleteBackward(), + insertText('6'), + deleteForward(), + ], + result: + '

1246

', + }, + { + inputs: [ + insertText('1'), + insertText('1'), + insertText('2'), + insertText('3'), + setNativeSelection([0, 0, 0], 0, [0, 0, 0], 0), + insertText('a'), + insertText('b'), + insertText('c'), + deleteForward(), + ], + result: + '

abc123

', + }, + ]; + + suite.forEach((testUnit, i) => { + test('Test case #' + (i + 1), () => { + applySelectionInputs(testUnit.inputs, update, editor); + expect(sanitizeHTML(container.innerHTML)).toBe(testUnit.result); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 398367553e0..aa70a8c0da2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9018,16 +9018,16 @@ react-error-overlay@^6.0.8: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de" integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw== +"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1: - version "17.0.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== - react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -9098,6 +9098,24 @@ react-scripts@4.0.0: optionalDependencies: fsevents "^2.1.3" +react-shallow-renderer@^16.13.1: + version "16.14.1" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124" + integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0" + +react-test-renderer@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3187e636c3063e6ae498aedf21ecf972721574c7" + integrity sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA== + dependencies: + object-assign "^4.1.1" + react-is "^17.0.1" + react-shallow-renderer "^16.13.1" + scheduler "^0.20.1" + react@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"