diff --git a/lib/controllers/file-patch-controller.js b/lib/controllers/file-patch-controller.js index 7c24870a47..f8ccbc7904 100644 --- a/lib/controllers/file-patch-controller.js +++ b/lib/controllers/file-patch-controller.js @@ -129,9 +129,18 @@ export default class FilePatchController extends React.Component { const staged = this.isStaged(); let filePatch = await this.getFilePatchForPath(this.props.filePath, staged); const isPartiallyStaged = await repository.isPartiallyStaged(this.props.filePath); + + const onFinish = () => { + this.props.switchboard.didFinishRepositoryRefresh(); + }; + if (filePatch) { this.resolveFilePatchLoadedPromise(); - if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); } + if (!this.destroyed) { + this.setState({filePatch, isPartiallyStaged}, onFinish); + } else { + onFinish(); + } } else { const oldFilePatch = this.state.filePatch; if (oldFilePatch) { @@ -140,7 +149,11 @@ export default class FilePatchController extends React.Component { newFile: oldFilePatch.newFile.clone({mode: null, symlink: null}), patch: oldFilePatch.getPatch().clone({hunks: []}), }); - if (!this.destroyed) { this.setState({filePatch, isPartiallyStaged}); } + if (!this.destroyed) { + this.setState({filePatch, isPartiallyStaged}, onFinish); + } else { + onFinish(); + } } } } @@ -262,6 +275,7 @@ export default class FilePatchController extends React.Component { await this.repositoryObserver.getActiveModel().applyPatchToIndex( this.state.filePatch.getStagePatchForHunk(hunk), ); + this.props.switchboard.didFinishStageOperation({stage: true, hunk: true}); } diff --git a/lib/models/ref-holder.js b/lib/models/ref-holder.js new file mode 100644 index 0000000000..a0c095cf19 --- /dev/null +++ b/lib/models/ref-holder.js @@ -0,0 +1,86 @@ +import {Emitter} from 'event-kit'; + +/* + * Allow child components to operate on refs captured by a parent component. + * + * React does not guarantee that refs are available until the component has finished mounting (before + * componentDidMount() is called), but a component does not finish mounting until all of its children are mounted. This + * causes problems when a child needs to consume a DOM node from its parent to interact with the Atom API, like we do in + * the `Tooltip` and `Commands` components. + * + * To pass a ref to a child, capture it in a RefHolder in the parent, and pass the RefHolder to the child: + * + * class Parent extends React.Component { + * constructor() { + * this.theRef = new RefHolder(); + * } + * + * render() { + * return ( + *
+ * + *
+ * ) + * } + * } + * + * In the child, use the `observe()` method to defer operations that need the DOM node to proceed: + * + * class Child extends React.Component { + * + * componentDidMount() { + * this.props.theRef.observe(domNode => this.register(domNode)) + * } + * + * render() { + * return null; + * } + * + * register(domNode) { + * console.log('Hey look I have a real DOM node', domNode); + * } + * } + */ +export default class RefHolder { + constructor() { + this.emitter = new Emitter(); + this.value = undefined; + } + + isEmpty() { + return this.value === undefined; + } + + get() { + if (this.isEmpty()) { + throw new Error('RefHolder is empty'); + } + return this.value; + } + + setter = value => { + if (value === null || value === undefined) { return; } + const oldValue = this.value; + this.value = value; + if (value !== oldValue) { + this.emitter.emit('did-update', value); + } + } + + observe(callback) { + if (!this.isEmpty()) { + callback(this.value); + } + return this.emitter.on('did-update', callback); + } + + static on(valueOrHolder) { + if (valueOrHolder instanceof this) { + return valueOrHolder; + } else { + const holder = new this(); + holder.setter(valueOrHolder); + return holder; + } + } +} diff --git a/lib/prop-types.js b/lib/prop-types.js index dd39e732a6..786dfd4042 100644 --- a/lib/prop-types.js +++ b/lib/prop-types.js @@ -57,3 +57,10 @@ export const RelayConnectionPropType = nodePropType => PropTypes.shape({ }), totalCount: PropTypes.number, }); + +export const RefHolderPropType = PropTypes.shape({ + isEmpty: PropTypes.func.isRequired, + get: PropTypes.func.isRequired, + setter: PropTypes.func.isRequired, + observe: PropTypes.func.isRequired, +}); diff --git a/lib/switchboard.js b/lib/switchboard.js index 45a66603b6..423b61d504 100644 --- a/lib/switchboard.js +++ b/lib/switchboard.js @@ -63,6 +63,7 @@ export default class Switchboard { 'FinishActiveContextUpdate', 'FinishRender', 'FinishContextChangeRender', + 'FinishRepositoryRefresh', ].forEach(eventName => { Switchboard.prototype[`did${eventName}`] = function(payload) { this.did(eventName, payload); diff --git a/lib/views/commands.js b/lib/views/commands.js index 9469592983..fdf8544dd0 100644 --- a/lib/views/commands.js +++ b/lib/views/commands.js @@ -1,7 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {Disposable} from 'event-kit'; -import {DOMNodePropType} from '../prop-types'; +import {DOMNodePropType, RefHolderPropType} from '../prop-types'; +import RefHolder from '../models/ref-holder'; export default class Commands extends React.Component { static propTypes = { @@ -9,6 +11,7 @@ export default class Commands extends React.Component { target: PropTypes.oneOfType([ PropTypes.string, DOMNodePropType, + RefHolderPropType, ]).isRequired, children: PropTypes.oneOfType([ PropTypes.element, @@ -34,30 +37,41 @@ export class Command extends React.Component { target: PropTypes.oneOfType([ PropTypes.string, DOMNodePropType, + RefHolderPropType, ]), command: PropTypes.string.isRequired, callback: PropTypes.func.isRequired, } + constructor(props, context) { + super(props, context); + this.subTarget = new Disposable(); + this.subCommand = new Disposable(); + } + componentDidMount() { - this.registerCommand(this.props); + this.observeTarget(this.props); } componentWillReceiveProps(newProps) { - for (const prop of ['registry', 'target', 'command', 'callback']) { - if (newProps[prop] !== this.props[prop]) { - this.disposable.dispose(); - this.registerCommand(newProps); - } + if (['registry', 'target', 'command', 'callback'].some(p => newProps[p] !== this.props[p])) { + this.observeTarget(newProps); } } componentWillUnmount() { - this.disposable.dispose(); + this.subTarget.dispose(); + this.subCommand.dispose(); + } + + observeTarget(props) { + this.subTarget.dispose(); + this.subTarget = RefHolder.on(props.target).observe(t => this.registerCommand(t, props)); } - registerCommand({registry, target, command, callback}) { - this.disposable = registry.add(target, command, callback); + registerCommand(target, {registry, command, callback}) { + this.subCommand.dispose(); + this.subCommand = registry.add(target, command, callback); } render() { diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index a43ff529c4..8da207b76d 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -1,5 +1,4 @@ import React from 'react'; -import ReactDom from 'react-dom'; import PropTypes from 'prop-types'; import {CompositeDisposable, Disposable} from 'event-kit'; @@ -11,6 +10,7 @@ import SimpleTooltip from './simple-tooltip'; import Commands, {Command} from './commands'; import FilePatchSelection from './file-patch-selection'; import Switchboard from '../switchboard'; +import RefHolder from '../models/ref-holder'; const executableText = { 100644: 'non executable 100644', @@ -62,18 +62,16 @@ export default class FilePatchView extends React.Component { this.mouseSelectionInProgress = false; this.disposables = new CompositeDisposable(); + this.refElement = new RefHolder(); + this.state = { selection: new FilePatchSelection(this.props.hunks), - domNode: null, }; } componentDidMount() { window.addEventListener('mouseup', this.mouseup); this.disposables.add(new Disposable(() => window.removeEventListener('mouseup', this.mouseup))); - this.setState({ - domNode: ReactDom.findDOMNode(this), - }); } componentWillReceiveProps(nextProps) { @@ -128,10 +126,6 @@ export default class FilePatchView extends React.Component { return true; } - if (this.state.domNode !== nextState.domNode) { - return true; - } - return false; } @@ -213,9 +207,9 @@ export default class FilePatchView extends React.Component { className={cx('github-FilePatchView', {'is-staged': !unstaged, 'is-unstaged': unstaged})} tabIndex="-1" onMouseUp={this.mouseup} - ref={e => { this.element = e; }}> + ref={this.refElement.setter}> - {this.state.domNode && this.registerCommands()} + {this.registerCommands()}
@@ -238,7 +232,7 @@ export default class FilePatchView extends React.Component { registerCommands() { return (
- + @@ -696,7 +690,7 @@ export default class FilePatchView extends React.Component { @autobind focus() { - this.element.focus(); + this.refElement.get().focus(); } @autobind diff --git a/package-lock.json b/package-lock.json index ec6077671c..9434263966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "github", - "version": "0.13.0", + "version": "0.13.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -10,6 +10,12 @@ "integrity": "sha1-nK+xca+CMpSQNTtIFvAzR6oVCjA=", "dev": true }, + "@types/node": { + "version": "9.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.4.tgz", + "integrity": "sha512-Awg4BcUYiZtNKoveGOu654JVPt11V/KIC77iBz8NweyoOAZpz5rUJfPDwwD+ajfTs2HndbTCEB8IuLfX9m/mmw==", + "dev": true + }, "acorn": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", @@ -1658,27 +1664,17 @@ } }, "cheerio": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", - "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", + "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", "dev": true, "requires": { "css-select": "1.2.0", "dom-serializer": "0.1.0", "entities": "1.1.1", "htmlparser2": "3.9.2", - "lodash.assignin": "4.2.0", - "lodash.bind": "4.2.1", - "lodash.defaults": "4.2.0", - "lodash.filter": "4.6.0", - "lodash.flatten": "4.4.0", - "lodash.foreach": "4.5.0", - "lodash.map": "4.6.0", - "lodash.merge": "4.6.0", - "lodash.pick": "4.4.0", - "lodash.reduce": "4.6.0", - "lodash.reject": "4.6.0", - "lodash.some": "4.6.0" + "lodash": "4.17.5", + "parse5": "3.0.3" } }, "chownr": { @@ -2074,6 +2070,12 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=" }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", + "dev": true + }, "doctrine": { "version": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", "integrity": "sha512-i5aQLQvEyAhw7XI4mbKxyrVdkqIc4OsCh9Z0XQof9X/ANftd0ZN1M4qz+TSU2VSokVwl23kXDvhnC4F4W+ip/g==", @@ -2185,21 +2187,51 @@ "dev": true }, "enzyme": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-2.9.1.tgz", - "integrity": "sha1-B9XOaRJBJA+4F78sSxjW5TAkDfY=", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.3.0.tgz", + "integrity": "sha512-l8csyPyLmtxskTz6pX9W8eDOyH1ckEtDttXk/vlFWCjv00SkjTjtoUrogqp4yEvMyneU9dUJoOLnqFoiHb8IHA==", "dev": true, "requires": { - "cheerio": "0.22.0", - "function.prototype.name": "1.0.3", + "cheerio": "1.0.0-rc.2", + "function.prototype.name": "1.1.0", + "has": "1.0.1", + "is-boolean-object": "1.0.0", + "is-callable": "1.1.3", + "is-number-object": "1.0.3", + "is-string": "1.0.4", "is-subset": "0.1.1", "lodash": "4.17.5", + "object-inspect": "1.5.0", "object-is": "1.0.1", - "object.assign": "4.0.4", + "object.assign": "4.1.0", "object.entries": "1.0.4", "object.values": "1.0.4", - "prop-types": "15.6.1", - "uuid": "3.1.0" + "raf": "3.4.0", + "rst-selector-parser": "2.2.3" + } + }, + "enzyme-adapter-react-15": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-15/-/enzyme-adapter-react-15-1.0.5.tgz", + "integrity": "sha512-GxQ+ZYbo6YFwwpaLc9LLyAwsx+F1au628/+hwTx3XV2OiuvHGyWgC/r1AAK1HlDRjujzfwwMNZTc/JxkjIuYVg==", + "dev": true, + "requires": { + "enzyme-adapter-utils": "1.3.0", + "lodash": "4.17.5", + "object.assign": "4.1.0", + "object.values": "1.0.4", + "prop-types": "15.6.1" + } + }, + "enzyme-adapter-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz", + "integrity": "sha512-vVXSt6uDv230DIv+ebCG66T1Pm36Kv+m74L1TrF4kaE7e1V7Q/LcxO0QRkajk5cA6R3uu9wJf5h13wOTezTbjA==", + "dev": true, + "requires": { + "lodash": "4.17.5", + "object.assign": "4.1.0", + "prop-types": "15.6.1" } }, "errno": { @@ -2231,14 +2263,14 @@ } }, "es-abstract": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", - "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.11.0.tgz", + "integrity": "sha512-ZnQrE/lXTTQ39ulXZ+J1DTFazV9qBy61x2bY071B+qGco8Z8q1QddsLdt/EF8Ai9hcWH72dWS0kFqXLxOxqslA==", "dev": true, "requires": { "es-to-primitive": "1.1.1", "function-bind": "1.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "has": "1.0.1", "is-callable": "1.1.3", "is-regex": "1.0.4" }, @@ -2462,7 +2494,7 @@ "doctrine": "1.5.0", "eslint-import-resolver-node": "0.3.1", "eslint-module-utils": "2.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "has": "1.0.1", "lodash.cond": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "read-pkg-up": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz" @@ -2565,7 +2597,7 @@ "dev": true, "requires": { "doctrine": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "has": "1.0.1", "jsx-ast-utils": "1.4.1" } }, @@ -2962,9 +2994,9 @@ "dev": true }, "function.prototype.name": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.0.3.tgz", - "integrity": "sha512-5EblxZUdioXi2JiMZ9FUbwYj40eQ9MFHyzFLBSPdlRl3SO8l7SLWuAnQ/at/1Wi4hjJwME/C5WpF2ZfAc8nGNw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", + "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", "dev": true, "requires": { "define-properties": "1.1.2", @@ -3135,7 +3167,8 @@ } }, "has": { - "version": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", "dev": true, "requires": { @@ -3149,6 +3182,12 @@ "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" } }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -3363,6 +3402,12 @@ "version": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, + "is-boolean-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", + "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", + "dev": true + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -3475,6 +3520,12 @@ } } }, + "is-number-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", + "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", + "dev": true + }, "is-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", @@ -3540,7 +3591,7 @@ "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "dev": true, "requires": { - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" + "has": "1.0.1" } }, "is-resolvable": { @@ -3553,6 +3604,12 @@ "version": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", + "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", + "dev": true + }, "is-subset": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", @@ -4043,18 +4100,6 @@ "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", "dev": true }, - "lodash.assignin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=", - "dev": true - }, - "lodash.bind": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", - "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=", - "dev": true - }, "lodash.cond": { "version": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", "integrity": "sha512-RWjUhzGbzG/KfDwk+onqdXvrsNv47G9UCMJgSKalPTSqJQyxZhQophG9jgqLf+15TIbZ5a/yG2YKOWsH3dVy9A==", @@ -4070,28 +4115,10 @@ "lodash._isiterateecall": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" } }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", - "dev": true - }, - "lodash.filter": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=", - "dev": true - }, - "lodash.flatten": { + "lodash.flattendeep": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true - }, - "lodash.foreach": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, "lodash.isarguments": { @@ -4119,47 +4146,11 @@ "lodash.isarray": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" } }, - "lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", - "dev": true - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" }, - "lodash.merge": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", - "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=", - "dev": true - }, - "lodash.pick": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", - "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", - "dev": true - }, - "lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=", - "dev": true - }, - "lodash.reject": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", - "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=", - "dev": true - }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", - "dev": true - }, "lolex": { "version": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", @@ -4605,6 +4596,18 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nearley": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.13.0.tgz", + "integrity": "sha512-ioYYogSaZhFlCpRizQgY3UT3G1qFXmHGY/5ozoFE3dMfiCRAeJfh+IPE3/eh9gCZvqLhPCWb4bLt7Bqzo+1mLQ==", + "dev": true, + "requires": { + "nomnom": "1.6.2", + "railroad-diagrams": "1.0.0", + "randexp": "0.4.6", + "semver": "5.4.1" + } + }, "next-tick": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", @@ -4625,6 +4628,16 @@ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" }, + "nomnom": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz", + "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=", + "dev": true, + "requires": { + "colors": "0.5.1", + "underscore": "1.4.4" + } + }, "normalize-package-data": { "version": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", @@ -4725,6 +4738,12 @@ } } }, + "object-inspect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.5.0.tgz", + "integrity": "sha512-UmOFbHbwvv+XHj7BerrhVq+knjceBdkvU5AriwLMvhv2qi+e7DJzxfBeFpILEjVzCp+xA+W/pIf06RGPWlZNfw==", + "dev": true + }, "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", @@ -4746,13 +4765,14 @@ } }, "object.assign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", - "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, "requires": { "define-properties": "1.1.2", "function-bind": "1.1.1", + "has-symbols": "1.0.0", "object-keys": "1.0.11" }, "dependencies": { @@ -4771,17 +4791,9 @@ "dev": true, "requires": { "define-properties": "1.1.2", - "es-abstract": "1.9.0", - "function-bind": "1.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" - }, - "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - } + "es-abstract": "1.11.0", + "function-bind": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", + "has": "1.0.1" } }, "object.pick": { @@ -4799,17 +4811,9 @@ "dev": true, "requires": { "define-properties": "1.1.2", - "es-abstract": "1.9.0", - "function-bind": "1.1.1", - "has": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" - }, - "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - } + "es-abstract": "1.11.0", + "function-bind": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", + "has": "1.0.1" } }, "once": { @@ -4881,7 +4885,7 @@ "p-limit": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "integrity": "sha1-DpK2vty1nwIsE9DxlJ3ILRWQnxw=", "requires": { "p-try": "1.0.0" } @@ -4906,6 +4910,15 @@ "error-ex": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz" } }, + "parse5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", + "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "dev": true, + "requires": { + "@types/node": "9.6.4" + } + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -5125,6 +5138,31 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" }, + "raf": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", + "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", + "dev": true, + "requires": { + "performance-now": "2.1.0" + } + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", + "dev": true + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "requires": { + "discontinuous-range": "1.0.0", + "ret": "0.1.15" + } + }, "react": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", @@ -5573,6 +5611,16 @@ "glob": "7.1.2" } }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "dev": true, + "requires": { + "lodash.flattendeep": "4.4.0", + "nearley": "2.13.0" + } + }, "run-async": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", @@ -6317,6 +6365,12 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.14.tgz", "integrity": "sha1-EQ1T+kw/MmwSEpK76skE0uAzh8o=" }, + "underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=", + "dev": true + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", diff --git a/package.json b/package.json index 69361825a3..779c20d772 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,8 @@ "chai-as-promised": "^5.3.0", "dedent-js": "^1.0.1", "electron-devtools-installer": "^2.2.3", - "enzyme": "^2.9.1", + "enzyme": "^3.3.0", + "enzyme-adapter-react-15": "^1.0.5", "eslint": "^3.19.0", "eslint-config-fbjs": "2.0.0-alpha.1", "eslint-config-standard": "^10.2.0", diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js index 228d696135..32a0e2ba8d 100644 --- a/test/controllers/commit-controller.test.js +++ b/test/controllers/commit-controller.test.js @@ -193,6 +193,7 @@ describe('CommitController', function() { wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('message in box'); await assert.async.equal(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath()); + wrapper.update(); assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox')); const editor = workspace.getActiveTextEditor(); @@ -208,7 +209,7 @@ describe('CommitController', function() { workspace.getActiveTextEditor().destroy(); assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox')); - await assert.async.strictEqual(wrapper.find('CommitView').prop('message'), 'message in editor'); + await assert.async.strictEqual(wrapper.update().find('CommitView').prop('message'), 'message in editor'); }); it('activates editor if already opened but in background', async function() { @@ -253,7 +254,7 @@ describe('CommitController', function() { wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')(); await assert.async.lengthOf(wrapper.instance().getCommitMessageEditors(), 0); assert.isTrue(atomEnvironment.applicationDelegate.confirm.called); - await assert.async.strictEqual(wrapper.find('CommitView').prop('message'), 'make some new changes'); + await assert.async.strictEqual(wrapper.update().find('CommitView').prop('message'), 'make some new changes'); }); }); diff --git a/test/controllers/editor-conflict-controller.test.js b/test/controllers/editor-conflict-controller.test.js index 0e34962b3a..61ef4b376c 100644 --- a/test/controllers/editor-conflict-controller.test.js +++ b/test/controllers/editor-conflict-controller.test.js @@ -284,6 +284,7 @@ describe('EditorConflictController', function() { editor.setCursorBufferPosition([16, 6]); // On "Your middle changes" commandRegistry.dispatch(editorView, 'github:dismiss-conflict'); + wrapper.update(); assert.equal(resolutionProgress.getRemaining(editor.getPath()), 2); assert.include(editor.getText(), 'Text in between 0 and 1.\n\n' + @@ -301,6 +302,8 @@ describe('EditorConflictController', function() { fs.writeFileSync(fixtureFile, onlyTwoMarkers); await assert.async.equal(wrapper.state('conflicts').size, 2); + wrapper.update(); + assert.lengthOf(wrapper.find(ConflictController), 2); assert.equal(resolutionProgress.getRemaining(fixtureFile), 2); }); diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 94d6915ca0..7143f1fb81 100644 --- a/test/controllers/file-patch-controller.test.js +++ b/test/controllers/file-patch-controller.test.js @@ -42,7 +42,6 @@ function createComponent(repository, filePath) { openFiles = sinon.spy(); getSelectedStagingViewItems = sinon.spy(); - getRepositoryForWorkdir = () => repository; resolutionProgress = new ResolutionProgress(); @@ -72,6 +71,16 @@ function createComponent(repository, filePath) { ); } +async function refreshRepository(wrapper) { + const workDir = wrapper.prop('workingDirectoryPath'); + const repository = wrapper.prop('getRepositoryForWorkdir')(workDir); + + const promise = wrapper.prop('switchboard').getFinishRepositoryRefreshPromise(); + repository.refresh(); + await promise; + wrapper.update(); +} + describe('FilePatchController', function() { afterEach(function() { atomEnv.destroy(); @@ -137,9 +146,9 @@ describe('FilePatchController', function() { const wrapper = mount(React.cloneElement(component, {largeDiffLineThreshold: 5})); - - await assert.async.isTrue(wrapper.find('.large-file-patch').exists()); + await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); wrapper.find('.large-file-patch').find('button').simulate('click'); + assert.isTrue(wrapper.find('HunkView').exists()); }); @@ -161,17 +170,17 @@ describe('FilePatchController', function() { filePath: filePatch1.getPath(), largeDiffLineThreshold: 5, })); - await assert.async.isTrue(wrapper.find('.large-file-patch').exists()); + await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); wrapper.find('.large-file-patch').find('button').simulate('click'); assert.isTrue(wrapper.find('HunkView').exists()); getFilePatchForPath.returns(filePatch2); wrapper.setProps({filePath: filePatch2.getPath()}); - await assert.async.isTrue(wrapper.find('.large-file-patch').exists()); + await assert.async.isTrue(wrapper.update().find('.large-file-patch').exists()); getFilePatchForPath.returns(filePatch1); wrapper.setProps({filePath: filePatch1.getPath()}); - assert.isTrue(wrapper.find('HunkView').exists()); + assert.isTrue(wrapper.update().find('HunkView').exists()); }); }); @@ -188,8 +197,7 @@ describe('FilePatchController', function() { assert.equal(wrapper.state('stagingStatus'), 'unstaged'); fs.writeFileSync(path.join(workdirPath, 'file.txt'), 'change\nand again!', 'utf8'); - repository.refresh(); - await wrapper.instance().onRepoRefresh(repository); + await refreshRepository(wrapper); assert.notEqual(originalFilePatch, wrapper.state('filePatch')); assert.equal(wrapper.state('stagingStatus'), 'unstaged'); @@ -210,7 +218,8 @@ describe('FilePatchController', function() { getFilePatchForPath.returns(filePatch); wrapper.instance().onRepoRefresh(repository); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); assert.isTrue(wrapper.find('HunkView').text().includes('@@ -0,1 +0,1 @@')); }); @@ -224,7 +233,7 @@ describe('FilePatchController', function() { let view0; await until(() => { - view0 = wrapper.find('FilePatchView').shallow(); + view0 = wrapper.update().find('FilePatchView').shallow(); return view0.find({hunk: hunk1}).exists(); }); assert.isTrue(view0.find({hunk: hunk2}).exists()); @@ -236,7 +245,7 @@ describe('FilePatchController', function() { wrapper.instance().onRepoRefresh(repository); let view1; await until(() => { - view1 = wrapper.find('FilePatchView').shallow(); + view1 = wrapper.update().find('FilePatchView').shallow(); return view1.find({hunk: hunk3}).exists(); }); assert.isTrue(view1.find({hunk: hunk1}).exists()); @@ -249,7 +258,7 @@ describe('FilePatchController', function() { const wrapper = mount(component); - await assert.async.isTrue(wrapper.find('FilePatchView').exists()); + await assert.async.isTrue(wrapper.update().find('Commands').exists()); commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-right'); assert.isTrue(didSurfaceFile.calledWith(filePath, 'unstaged')); }); @@ -274,11 +283,12 @@ describe('FilePatchController', function() { const wrapper = mount(React.cloneElement(component, {openFiles: openFilesStub})); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); wrapper.find('LineView').simulate('mousedown', {button: 0, detail: 1}); window.dispatchEvent(new MouseEvent('mouseup')); commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:open-file'); + wrapper.update(); await assert.async.isTrue(editorSpy.setCursorBufferPosition.called); @@ -328,7 +338,7 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); // Unstage a couple added lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); @@ -337,7 +347,8 @@ describe('FilePatchController', function() { hunkView0.find('button.github-HunkView-stageButton').simulate('click'); await opPromise0; - repository.refresh(); + await refreshRepository(wrapper); + // index shows symlink deletions still staged, only a couple of lines have been unstaged assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'qux\nbaz\nzoo\n'); @@ -359,7 +370,7 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\nbar\nbaz\n\n'); // stage a couple of lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); @@ -368,7 +379,8 @@ describe('FilePatchController', function() { hunkView0.find('button.github-HunkView-stageButton').simulate('click'); await opPromise0; - repository.refresh(); + await refreshRepository(wrapper); + // index shows symlink change has not been staged, a couple of lines have been deleted assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'foo\n\n'); @@ -392,7 +404,7 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '120000'); // Stage a couple added lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); @@ -401,7 +413,8 @@ describe('FilePatchController', function() { hunkView0.find('button.github-HunkView-stageButton').simulate('click'); await opPromise0; - repository.refresh(); + await refreshRepository(wrapper); + // index no longer shows file is symlink (symlink has been deleted), now a regular file with contents assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); assert.autocrlfEqual(await repository.readFileFromIndex(deletedSymlinkAddedFilePath), 'foo\nbar\n'); @@ -423,7 +436,7 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '120000'); // unstage a couple of lines, but not all - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); @@ -432,7 +445,8 @@ describe('FilePatchController', function() { hunkView0.find('button.github-HunkView-stageButton').simulate('click'); await opPromise0; - repository.refresh(); + await refreshRepository(wrapper); + // index no longer shows file is symlink (symlink creation has been unstaged), shows contents of file that existed prior to symlink assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); assert.autocrlfEqual(await repository.readFileFromIndex(deletedFileAddedSymlinkPath), 'bar\nbaz\n'); @@ -453,14 +467,15 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedFileAddedSymlinkPath)).mode, '100644'); // stage all deleted lines - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('.github-HunkView-title').simulate('click'); hunkView0.find('button.github-HunkView-stageButton').simulate('click'); await opPromise0; - repository.refresh(); + await refreshRepository(wrapper); + // File is not on index, file deletion has been staged assert.isNull(await indexModeAndOid(repository, deletedFileAddedSymlinkPath)); const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); @@ -485,14 +500,15 @@ describe('FilePatchController', function() { assert.equal((await indexModeAndOid(repository, deletedSymlinkAddedFilePath)).mode, '100644'); // unstage all added lines - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('.github-HunkView-title').simulate('click'); hunkView0.find('button.github-HunkView-stageButton').simulate('click'); await opPromise0; - repository.refresh(); + await refreshRepository(wrapper); + // File is not on index, file creation has been unstaged assert.isNull(await indexModeAndOid(repository, deletedSymlinkAddedFilePath)); const {stagedFiles, unstagedFiles} = await repository.getStatusesForChangedFiles(); @@ -524,11 +540,10 @@ describe('FilePatchController', function() { const wrapper = mount(component); - // selectNext() - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'core:move-down'); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const hunkView0 = wrapper.find('HunkView').at(0); assert.isFalse(hunkView0.prop('isSelected')); const opPromise0 = switchboard.getFinishStageOperationPromise(); @@ -558,7 +573,6 @@ describe('FilePatchController', function() { it('stages and unstages individual lines when the stage button is clicked on a hunk with selected lines', async function() { const absFilePath = path.join(workdirPath, filePath); - const originalLines = fs.readFileSync(absFilePath, 'utf8').split('\n'); // write some unstaged changes @@ -574,7 +588,7 @@ describe('FilePatchController', function() { // stage a subset of lines from first hunk const wrapper = mount(component); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise0 = switchboard.getFinishStageOperationPromise(); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('LineView').at(1).find('.github-HunkView-line').simulate('mousedown', {button: 0, detail: 1}); @@ -583,7 +597,8 @@ describe('FilePatchController', function() { hunkView0.find('button.github-HunkView-stageButton').simulate('click'); await opPromise0; - repository.refresh(); + await refreshRepository(wrapper); + const expectedLines0 = originalLines.slice(); expectedLines0.splice(1, 1, 'this is a modified line', @@ -592,16 +607,12 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines0.join('\n')); // stage remaining lines in hunk - const updatePromise1 = switchboard.getChangePatchPromise(); - const unstagedFilePatch1 = await repository.getFilePatchForPath('sample.js'); - wrapper.setState({filePatch: unstagedFilePatch1}); - await updatePromise1; - const opPromise1 = switchboard.getFinishStageOperationPromise(); wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); await opPromise1; - repository.refresh(); + await refreshRepository(wrapper); + const expectedLines1 = originalLines.slice(); expectedLines1.splice(1, 1, 'this is a modified line', @@ -611,13 +622,8 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines1.join('\n')); // unstage a subset of lines from the first hunk - const updatePromise2 = switchboard.getChangePatchPromise(); - const stagedFilePatch2 = await repository.getFilePatchForPath('sample.js', {staged: true}); - wrapper.setState({ - filePatch: stagedFilePatch2, - stagingStatus: 'staged', - }); - await updatePromise2; + wrapper.setState({stagingStatus: 'staged'}); + await refreshRepository(wrapper); const hunkView2 = wrapper.find('HunkView').at(0); hunkView2.find('LineView').at(1).find('.github-HunkView-line') @@ -631,7 +637,8 @@ describe('FilePatchController', function() { hunkView2.find('button.github-HunkView-stageButton').simulate('click'); await opPromise2; - repository.refresh(); + await refreshRepository(wrapper); + const expectedLines2 = originalLines.slice(); expectedLines2.splice(2, 0, 'this is a new line', @@ -640,13 +647,6 @@ describe('FilePatchController', function() { assert.autocrlfEqual(await repository.readFileFromIndex('sample.js'), expectedLines2.join('\n')); // unstage the rest of the hunk - const updatePromise3 = switchboard.getChangePatchPromise(); - const stagedFilePatch3 = await repository.getFilePatchForPath('sample.js', {staged: true}); - wrapper.setState({ - filePatch: stagedFilePatch3, - }); - await updatePromise3; - commandRegistry.dispatch(wrapper.find('FilePatchView').getDOMNode(), 'github:toggle-patch-selection-mode'); const opPromise3 = switchboard.getFinishStageOperationPromise(); @@ -669,7 +669,7 @@ describe('FilePatchController', function() { initialStagingStatus: 'staged', })); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const opPromise = switchboard.getFinishStageOperationPromise(); wrapper.find('HunkView').at(0).find('button.github-HunkView-stageButton').simulate('click'); @@ -690,7 +690,7 @@ describe('FilePatchController', function() { initialStagingStatus: 'staged', })); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const viewNode = wrapper.find('FilePatchView').getDOMNode(); commandRegistry.dispatch(viewNode, 'github:toggle-patch-selection-mode'); @@ -723,7 +723,7 @@ describe('FilePatchController', function() { const wrapper = mount(component); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); const hunkView0 = wrapper.find('HunkView').at(0); hunkView0.find('LineView').at(1).find('.github-HunkView-line') .simulate('mousedown', {button: 0, detail: 1}); @@ -739,7 +739,7 @@ describe('FilePatchController', function() { const changePatchPromise = switchboard.getChangePatchPromise(); // assert that only line 1 has been staged - repository.refresh(); // clear the cached file patches + await refreshRepository(wrapper); // clear the cached file patches let expectedLines = originalLines.slice(); expectedLines.splice(1, 0, 'this is a modified line', @@ -747,6 +747,7 @@ describe('FilePatchController', function() { let actualLines = await repository.readFileFromIndex(filePath); assert.autocrlfEqual(actualLines, expectedLines.join('\n')); await changePatchPromise; + wrapper.update(); const hunkView1 = wrapper.find('HunkView').at(0); hunkView1.find('LineView').at(2).find('.github-HunkView-line') @@ -782,7 +783,7 @@ describe('FilePatchController', function() { const wrapper = mount(component); - await assert.async.isTrue(wrapper.find('HunkView').exists()); + await assert.async.isTrue(wrapper.update().find('HunkView').exists()); // ensure staging the same hunk twice does not cause issues // second stage action is a no-op since the first staging operation is in flight @@ -792,7 +793,7 @@ describe('FilePatchController', function() { await hunk1StagingPromise; const patchPromise0 = switchboard.getChangePatchPromise(); - repository.refresh(); // clear the cached file patches + await refreshRepository(wrapper); // clear the cached file patches const modifiedFilePatch = await repository.getFilePatchForPath(filePath); wrapper.setState({filePatch: modifiedFilePatch}); await patchPromise0; diff --git a/test/controllers/git-tab-controller.test.js b/test/controllers/git-tab-controller.test.js index 51bf2a5b94..12c5ac81de 100644 --- a/test/controllers/git-tab-controller.test.js +++ b/test/controllers/git-tab-controller.test.js @@ -72,11 +72,11 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository}); const wrapper = mount(app); - assert.isTrue(wrapper.hasClass('is-loading')); + assert.isTrue(wrapper.find('.github-Panel').hasClass('is-loading')); assert.lengthOf(wrapper.find('EtchWrapper'), 1); assert.lengthOf(wrapper.find('CommitController'), 1); - await assert.async.isFalse(wrapper.hasClass('is-loading')); + await assert.async.isFalse(wrapper.update().find('.github-Panel').hasClass('is-loading')); assert.lengthOf(wrapper.find('EtchWrapper'), 1); assert.lengthOf(wrapper.find('CommitController'), 1); }); @@ -87,8 +87,8 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository}); const wrapper = mount(app); - assert.isTrue(wrapper.hasClass('is-empty')); - assert.lengthOf(wrapper.find('.no-repository'), 1); + assert.isTrue(wrapper.find('.is-empty').exists()); + assert.isTrue(wrapper.find('.no-repository').exists()); }); it('keeps the state of the GitTabView in sync with the assigned repository', async function() { @@ -111,17 +111,17 @@ describe('GitTabController', function() { // Fetches data when a new repository is assigned // Does not update repository instance variable until that data is fetched wrapper.setProps({repository: repository1}); - await assert.async.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository1.getUnstagedChanges()); + await assert.async.deepEqual(wrapper.update().find('GitTabView').prop('unstagedChanges'), await repository1.getUnstagedChanges()); wrapper.setProps({repository: repository2}); - await assert.async.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); + await assert.async.deepEqual(wrapper.update().find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); // Fetches data and updates child view when the repository is mutated fs.writeFileSync(path.join(workdirPath2, 'a.txt'), 'a change\n'); fs.unlinkSync(path.join(workdirPath2, 'b.txt')); repository2.refresh(); - await assert.async.deepEqual(wrapper.find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); + await assert.async.deepEqual(wrapper.update().find('GitTabView').prop('unstagedChanges'), await repository2.getUnstagedChanges()); }); it('fetches conflict marker counts for conflicting files', async function() { @@ -153,18 +153,17 @@ describe('GitTabController', function() { const confirm = sinon.stub(); app = React.cloneElement(app, {repository, confirm}); const wrapper = mount(app); - const view = wrapper.find('GitTabView'); - await assert.async.isTrue(view.prop('isMerging')); - assert.notEqual(view.prop('mergeConflicts').length, 0); - assert.isOk(view.prop('mergeMessage')); + await assert.async.isTrue(wrapper.update().find('GitTabView').prop('isMerging')); + assert.notEqual(wrapper.find('GitTabView').prop('mergeConflicts').length, 0); + assert.isOk(wrapper.find('GitTabView').prop('mergeMessage')); confirm.returns(0); await wrapper.instance().getWrappedComponentInstance().abortMerge(); - await assert.async.lengthOf(view.prop('mergeConflicts'), 0); - assert.isFalse(view.prop('isMerging')); - assert.isNull(view.prop('mergeMessage')); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('mergeConflicts'), 0); + assert.isFalse(wrapper.find('GitTabView').prop('isMerging')); + assert.isNull(wrapper.find('GitTabView').prop('mergeMessage')); }); }); @@ -243,7 +242,7 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository}); const wrapper = mount(app); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('unstagedChanges'), 3); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('unstagedChanges'), 3); const controller = wrapper.instance().getWrappedComponentInstance(); const gitTab = controller.refView; @@ -268,7 +267,7 @@ describe('GitTabController', function() { const wrapper = mount(app); const controller = wrapper.instance().getWrappedComponentInstance(); - assert.isTrue(wrapper.hasClass('is-empty')); + assert.isTrue(wrapper.find('.is-empty').exists()); assert.lengthOf(wrapper.find('.no-repository'), 1); controller.rememberLastFocus({target: null}); @@ -336,7 +335,7 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository}); wrapper = mount(app); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('unstagedChanges'), 3); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('unstagedChanges'), 3); extractReferences(); }); @@ -344,7 +343,7 @@ describe('GitTabController', function() { it('blurs on tool-panel:unfocus', function() { sinon.spy(workspace.getActivePane(), 'activate'); - commandRegistry.dispatch(wrapper.find('.github-Panel').getNode(), 'tool-panel:unfocus'); + commandRegistry.dispatch(wrapper.find('.github-Panel').getDOMNode(), 'tool-panel:unfocus'); assert.isTrue(workspace.getActivePane().activate.called); }); @@ -444,7 +443,7 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository, ensureGitTab}); const wrapper = mount(app); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('unstagedChanges'), 2); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('unstagedChanges'), 2); const gitTab = wrapper.instance().getWrappedComponentInstance().refView; const stagingView = gitTab.refStagingView.getWrappedComponent(); @@ -468,7 +467,7 @@ describe('GitTabController', function() { await assert.async.lengthOf(stagingView.props.unstagedChanges, 1); assert.lengthOf(stagingView.props.stagedChanges, 1); - commitView.find('atom-text-editor').getNode().getModel().setText('Make it so'); + commitView.find('atom-text-editor').instance().getModel().setText('Make it so'); commitView.find('.github-CommitView-commit').simulate('click'); await assert.async.equal((await repository.getLastCommit()).getMessageSubject(), 'Make it so'); @@ -484,7 +483,7 @@ describe('GitTabController', function() { app = React.cloneElement(app, {repository, confirm}); const wrapper = mount(app); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('mergeConflicts'), 5); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('mergeConflicts'), 5); const stagingView = wrapper.instance().getWrappedComponentInstance().refView.refStagingView.getWrappedComponent(); assert.equal(stagingView.props.mergeConflicts.length, 5); @@ -607,7 +606,7 @@ describe('GitTabController', function() { await repository.commit(commitMessage); getLastCommit = () => { - return wrapper.find('RecentCommitView').getNodes()[0].props.commit; + return wrapper.update().find('RecentCommitView').at(0).instance().props.commit; }; await assert.async.strictEqual(getLastCommit().getMessageSubject(), commitMessage); @@ -620,16 +619,16 @@ describe('GitTabController', function() { // stage some changes fs.writeFileSync(path.join(workdirPath, 'new-file.txt'), 'oh\nem\ngee\n'); await repository.stageFiles(['new-file.txt']); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('stagedChanges'), 1); // ensure that the commit editor is empty - assert.strictEqual(wrapper.find('CommitView').getNode().editor.getText(), ''); + assert.strictEqual(wrapper.find('CommitView').instance().editor.getText(), ''); commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); // amending should commit all unstaged changes - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('stagedChanges'), 0); // commit message from previous commit should be used const lastCommit = getLastCommit(); @@ -642,10 +641,10 @@ describe('GitTabController', function() { // new commit message const newMessage = 'such new very message'; const commitView = wrapper.find('CommitView'); - commitView.getNode().editor.setText(newMessage); + commitView.instance().editor.setText(newMessage); // no staged changes - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 0); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('stagedChanges'), 0); commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit'); await assert.async.deepEqual(repository.commit.args[0][1], {amend: true, coAuthors: []}); @@ -663,7 +662,7 @@ describe('GitTabController', function() { // add co author const author = {email: 'foo@bar.com', name: 'foo bar'}; - const commitView = wrapper.find('CommitView').getNode(); + const commitView = wrapper.find('CommitView').instance(); commitView.setState({showCoAuthorInput: true}); commitView.onSelectedCoAuthorsChanged([author]); @@ -682,7 +681,7 @@ describe('GitTabController', function() { // add co author const author = {email: 'foo@bar.com', name: 'foo bar'}; - const commitView = wrapper.find('CommitView').getNode(); + const commitView = wrapper.find('CommitView').instance(); commitView.setState({showCoAuthorInput: true}); commitView.onSelectedCoAuthorsChanged([author]); const newMessage = 'Star Wars: A New Message'; @@ -714,7 +713,7 @@ describe('GitTabController', function() { assert.strictEqual(getLastCommit().getMessageSubject(), message); // buh bye co author - const commitView = wrapper.find('CommitView').getNode(); + const commitView = wrapper.find('CommitView').instance(); assert.strictEqual(commitView.editor.getText(), ''); commitView.onSelectedCoAuthorsChanged([]); @@ -766,14 +765,14 @@ describe('GitTabController', function() { assert.deepEqual(wrapper.find('CommitView').prop('selectedCoAuthors'), []); - await assert.async.lengthOf(wrapper.find('.github-RecentCommit-undoButton'), 1); + await assert.async.lengthOf(wrapper.update().find('.github-RecentCommit-undoButton'), 1); wrapper.find('.github-RecentCommit-undoButton').simulate('click'); let commitMessages = wrapper.find('.github-RecentCommit-message').map(node => node.text()); // ensure that the co author trailer is stripped from commit message assert.deepEqual(commitMessages, [commitSubject, 'Initial commit']); - await assert.async.lengthOf(wrapper.find('GitTabView').prop('stagedChanges'), 1); + await assert.async.lengthOf(wrapper.update().find('GitTabView').prop('stagedChanges'), 1); assert.deepEqual(wrapper.find('GitTabView').prop('stagedChanges'), [{ filePath: 'new-file.txt', status: 'added', diff --git a/test/controllers/repository-conflict-controller.test.js b/test/controllers/repository-conflict-controller.test.js index 822a938081..af3963a05e 100644 --- a/test/controllers/repository-conflict-controller.test.js +++ b/test/controllers/repository-conflict-controller.test.js @@ -65,7 +65,7 @@ describe('RepositoryConflictController', () => { app = React.cloneElement(app, {repository}); const wrapper = mount(app); - await assert.async.equal(wrapper.find(EditorConflictController).length, 2); + await assert.async.equal(wrapper.update().find(EditorConflictController).length, 2); }); }); diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js index b8ac62c1c2..3ddc313c9b 100644 --- a/test/controllers/root-controller.test.js +++ b/test/controllers/root-controller.test.js @@ -316,6 +316,7 @@ describe('RootController', function() { const wrapper = shallow(app); wrapper.instance().initializeRepo(); + wrapper.update(); assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('InitDialog'), 1); }); @@ -325,6 +326,7 @@ describe('RootController', function() { const wrapper = shallow(app); wrapper.instance().initializeRepo(); + wrapper.update(); const dialog = wrapper.find('InitDialog'); dialog.prop('didAccept')('/a/path'); resolveInit(); @@ -337,9 +339,11 @@ describe('RootController', function() { const wrapper = shallow(app); wrapper.instance().initializeRepo(); + wrapper.update(); const dialog = wrapper.find('InitDialog'); dialog.prop('didCancel')(); + wrapper.update(); assert.isFalse(wrapper.find('InitDialog').exists()); }); @@ -350,6 +354,7 @@ describe('RootController', function() { const wrapper = shallow(app); wrapper.instance().initializeRepo(); + wrapper.update(); const dialog = wrapper.find('InitDialog'); const acceptPromise = dialog.prop('didAccept')('/a/path'); const err = new GitError('git init exited with status 1'); @@ -357,6 +362,7 @@ describe('RootController', function() { rejectInit(err); await acceptPromise; + wrapper.update(); assert.isFalse(wrapper.find('InitDialog').exists()); assert.isTrue(notificationManager.addError.calledWith( 'Unable to initialize git repository in /a/path', @@ -380,12 +386,14 @@ describe('RootController', function() { it('renders the modal clone panel', function() { wrapper.instance().openCloneDialog(); + wrapper.update(); assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('CloneDialog'), 1); }); it('triggers the clone callback on accept', function() { wrapper.instance().openCloneDialog(); + wrapper.update(); const dialog = wrapper.find('CloneDialog'); dialog.prop('didAccept')('git@github.com:atom/github.git', '/home/me/github'); @@ -396,17 +404,20 @@ describe('RootController', function() { it('marks the clone dialog as in progress during clone', async function() { wrapper.instance().openCloneDialog(); + wrapper.update(); const dialog = wrapper.find('CloneDialog'); assert.isFalse(dialog.prop('inProgress')); const acceptPromise = dialog.prop('didAccept')('git@github.com:atom/github.git', '/home/me/github'); + wrapper.update(); assert.isTrue(wrapper.find('CloneDialog').prop('inProgress')); resolveClone(); await acceptPromise; + wrapper.update(); assert.isFalse(wrapper.find('CloneDialog').exists()); }); @@ -414,6 +425,7 @@ describe('RootController', function() { sinon.stub(notificationManager, 'addError'); wrapper.instance().openCloneDialog(); + wrapper.update(); const dialog = wrapper.find('CloneDialog'); assert.isFalse(dialog.prop('inProgress')); @@ -424,6 +436,7 @@ describe('RootController', function() { rejectClone(err); await acceptPromise; + wrapper.update(); assert.isFalse(wrapper.find('CloneDialog').exists()); assert.isTrue(notificationManager.addError.calledWith( 'Unable to clone git@github.com:nope/nope.git', @@ -433,10 +446,12 @@ describe('RootController', function() { it('dismisses the clone panel on cancel', function() { wrapper.instance().openCloneDialog(); + wrapper.update(); const dialog = wrapper.find('CloneDialog'); dialog.prop('didCancel')(); + wrapper.update(); assert.lengthOf(wrapper.find('CloneDialog'), 0); assert.isFalse(cloneRepositoryForProjectPath.called); }); @@ -454,6 +469,7 @@ describe('RootController', function() { prompt: 'Password plz', includeUsername: true, }); + wrapper.update(); const dialog = wrapper.find('Panel').find({location: 'modal'}).find('CredentialDialog'); assert.isTrue(dialog.exists()); @@ -466,9 +482,12 @@ describe('RootController', function() { prompt: 'Speak "friend" and enter', includeUsername: false, }); + wrapper.update(); wrapper.find('CredentialDialog').prop('onSubmit')({password: 'friend'}); assert.deepEqual(await credentialPromise, {password: 'friend'}); + + wrapper.update(); assert.isFalse(wrapper.find('CredentialDialog').exists()); }); @@ -477,9 +496,12 @@ describe('RootController', function() { prompt: 'Enter the square root of 1244313452349528345', includeUsername: false, }); + wrapper.update(); wrapper.find('CredentialDialog').prop('onCancel')(); await assert.isRejected(credentialPromise); + + wrapper.update(); assert.isFalse(wrapper.find('CredentialDialog').exists()); }); }); diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js index 37883e2c72..951c07611d 100644 --- a/test/controllers/status-bar-tile-controller.test.js +++ b/test/controllers/status-bar-tile-controller.test.js @@ -44,7 +44,7 @@ describe('StatusBarTileController', function() { }); function getTooltipNode(wrapper, selector) { - const ts = tooltips.findTooltips(wrapper.find(selector).node.element); + const ts = tooltips.findTooltips(wrapper.find(selector).getDOMNode()); assert.lengthOf(ts, 1); ts[0].show(); return ts[0].getTooltipElement(); @@ -57,6 +57,7 @@ describe('StatusBarTileController', function() { const wrapper = mount(React.cloneElement(component, {repository})); await wrapper.instance().refreshModelData(); + wrapper.update(); assert.equal(wrapper.find(BranchView).prop('currentBranch').name, 'master'); assert.lengthOf(wrapper.find(BranchView).find('.github-branch-detached'), 0); @@ -69,6 +70,7 @@ describe('StatusBarTileController', function() { const wrapper = mount(React.cloneElement(component, {repository})); await wrapper.instance().refreshModelData(); + wrapper.update(); assert.equal(wrapper.find(BranchView).prop('currentBranch').name, 'master~2'); assert.lengthOf(wrapper.find(BranchView).find('.github-branch-detached'), 1); @@ -95,6 +97,7 @@ describe('StatusBarTileController', function() { const wrapper = mount(React.cloneElement(component, {repository})); await wrapper.instance().refreshModelData(); + wrapper.update(); const tip = getTooltipNode(wrapper, BranchView); const selectList = tip.querySelector('select'); @@ -281,6 +284,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); await statusBarTile.instance().refreshModelData(); + statusBarTile.update(); sinon.spy(repository, 'fetch'); sinon.spy(repository, 'push'); @@ -318,6 +322,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); await statusBarTile.instance().refreshModelData(); + statusBarTile.update(); sinon.spy(repository, 'fetch'); sinon.spy(repository, 'push'); @@ -356,6 +361,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); await statusBarTile.instance().refreshModelData(); + statusBarTile.update(); sinon.spy(repository, 'fetch'); sinon.spy(repository, 'push'); @@ -394,6 +400,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); await statusBarTile.instance().refreshModelData(); + statusBarTile.update(); sinon.spy(repository, 'fetch'); sinon.spy(repository, 'push'); @@ -433,6 +440,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); await statusBarTile.instance().refreshModelData(); + statusBarTile.update(); sinon.spy(repository, 'fetch'); sinon.spy(repository, 'push'); @@ -473,6 +481,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); await statusBarTile.instance().refreshModelData(); + statusBarTile.update(); sinon.spy(repository, 'fetch'); sinon.spy(repository, 'push'); @@ -503,6 +512,7 @@ describe('StatusBarTileController', function() { statusBarTile = mount(React.cloneElement(component, {repository})); await statusBarTile.instance().refreshModelData(); + statusBarTile.update(); sinon.spy(repository, 'fetch'); sinon.spy(repository, 'push'); diff --git a/test/decorators/observe-model.test.js b/test/decorators/observe-model.test.js index 314900655b..4bbb60171c 100644 --- a/test/decorators/observe-model.test.js +++ b/test/decorators/observe-model.test.js @@ -51,25 +51,25 @@ describe('ObserveModelDecorator', function() { assert.isTrue(wrapper.is('ObserveModelDecorator(TestComponent)')); - await assert.async.equal(wrapper.find('TestComponent').prop('one'), 1); - await assert.async.equal(wrapper.find('TestComponent').prop('two'), 2); - await assert.async.equal(wrapper.find('TestComponent').prop('testModel'), model); + await assert.async.equal(wrapper.update().find('TestComponent').prop('one'), 1); + await assert.async.equal(wrapper.update().find('TestComponent').prop('two'), 2); + await assert.async.equal(wrapper.update().find('TestComponent').prop('testModel'), model); model.update({one: 'one', two: 'two'}); - await assert.async.equal(wrapper.find('TestComponent').prop('one'), 'one'); - await assert.async.equal(wrapper.find('TestComponent').prop('two'), 'two'); - await assert.async.equal(wrapper.find('TestComponent').prop('testModel'), model); + await assert.async.equal(wrapper.update().find('TestComponent').prop('one'), 'one'); + await assert.async.equal(wrapper.update().find('TestComponent').prop('two'), 'two'); + await assert.async.equal(wrapper.update().find('TestComponent').prop('testModel'), model); wrapper.setProps({testModel: null}); - await assert.async.equal(wrapper.find('TestComponent').prop('one'), undefined); - await assert.async.equal(wrapper.find('TestComponent').prop('two'), undefined); - await assert.async.isNull(wrapper.find('TestComponent').prop('testModel')); + await assert.async.equal(wrapper.update().find('TestComponent').prop('one'), undefined); + await assert.async.equal(wrapper.update().find('TestComponent').prop('two'), undefined); + await assert.async.isNull(wrapper.update().find('TestComponent').prop('testModel')); const model2 = new TestModel({one: 1, two: 2}); wrapper.setProps({testModel: model2}); - await assert.async.equal(wrapper.find('TestComponent').prop('one'), 1); - await assert.async.equal(wrapper.find('TestComponent').prop('two'), 2); - await assert.async.equal(wrapper.find('TestComponent').prop('testModel'), model2); + await assert.async.equal(wrapper.update().find('TestComponent').prop('one'), 1); + await assert.async.equal(wrapper.update().find('TestComponent').prop('two'), 2); + await assert.async.equal(wrapper.update().find('TestComponent').prop('testModel'), model2); }); it('hosts static methods', function() { diff --git a/test/models/ref-holder.test.js b/test/models/ref-holder.test.js new file mode 100644 index 0000000000..97a62791fd --- /dev/null +++ b/test/models/ref-holder.test.js @@ -0,0 +1,68 @@ +import RefHolder from '../../lib/models/ref-holder'; + +describe('RefHolder', function() { + let sub; + + afterEach(function() { + if (sub) { sub.dispose(); } + }); + + it('begins empty', function() { + const h = new RefHolder(); + assert.isTrue(h.isEmpty()); + assert.throws(() => h.get(), /empty/); + }); + + it('does not become populated when assigned null', function() { + const h = new RefHolder(); + h.setter(null); + assert.isTrue(h.isEmpty()); + }); + + it('provides synchronous access to its current value', function() { + const h = new RefHolder(); + h.setter(1234); + assert.isFalse(h.isEmpty()); + assert.strictEqual(h.get(), 1234); + }); + + it('notifies subscribers when it becomes available', function() { + const h = new RefHolder(); + const callback = sinon.spy(); + sub = h.observe(callback); + + h.setter(1); + assert.isTrue(callback.calledWith(1)); + + h.setter(2); + assert.isTrue(callback.calledWith(2)); + + sub.dispose(); + + h.setter(3); + assert.isFalse(callback.calledWith(3)); + }); + + it('immediately notifies new subscribers if it is already available', function() { + const h = new RefHolder(); + h.setter(12); + + const callback = sinon.spy(); + sub = h.observe(callback); + assert.isTrue(callback.calledWith(12)); + }); + + describe('.on()', function() { + it('returns an existing RefHolder as-is', function() { + const original = new RefHolder(); + const wrapped = RefHolder.on(original); + assert.strictEqual(original, wrapped); + }); + + it('wraps a non-RefHolder value with a RefHolder set to it', function() { + const wrapped = RefHolder.on(9000); + assert.isFalse(wrapped.isEmpty()); + assert.strictEqual(wrapped.get(), 9000); + }); + }); +}); diff --git a/test/runner.js b/test/runner.js index 68b438a27f..7066d32479 100644 --- a/test/runner.js +++ b/test/runner.js @@ -16,6 +16,10 @@ module.exports = createRunner({ reporter: process.env.MOCHA_REPORTER || 'spec', overrideTestPaths: [/spec$/, /test/], }, mocha => { + const Enzyme = require('enzyme'); + const Adapter = require('enzyme-adapter-react-15'); + Enzyme.configure({adapter: new Adapter()}); + require('mocha-stress'); mocha.timeout(parseInt(process.env.MOCHA_TIMEOUT || '5000', 10)); diff --git a/test/views/clone-dialog.test.js b/test/views/clone-dialog.test.js index 200732a459..fd3c9a1f39 100644 --- a/test/views/clone-dialog.test.js +++ b/test/views/clone-dialog.test.js @@ -69,6 +69,7 @@ describe('CloneDialog', function() { it('disables the clone button with no remote URL', function() { setTextIn('.github-ProjectPath atom-text-editor', path.join('somewhere', 'else')); setTextIn('.github-CloneUrl atom-text-editor', ''); + wrapper.update(); assert.isTrue(wrapper.find('button.icon-repo-clone').prop('disabled')); }); @@ -76,6 +77,7 @@ describe('CloneDialog', function() { it('disables the clone button with no project path', function() { setTextIn('.github-ProjectPath atom-text-editor', ''); setTextIn('.github-CloneUrl atom-text-editor', 'git@github.com:atom/github.git'); + wrapper.update(); assert.isTrue(wrapper.find('button.icon-repo-clone').prop('disabled')); }); @@ -83,6 +85,7 @@ describe('CloneDialog', function() { it('enables the clone button when both text boxes are populated', function() { setTextIn('.github-ProjectPath atom-text-editor', path.join('somewhere', 'else')); setTextIn('.github-CloneUrl atom-text-editor', 'git@github.com:atom/github.git'); + wrapper.update(); assert.isFalse(wrapper.find('button.icon-repo-clone').prop('disabled')); }); diff --git a/test/views/co-author-form.test.js b/test/views/co-author-form.test.js index 53ab56136b..cc1167f82f 100644 --- a/test/views/co-author-form.test.js +++ b/test/views/co-author-form.test.js @@ -35,7 +35,7 @@ describe('CoAuthorForm', function() { const name = 'Original Name'; app = React.cloneElement(app, {name}); wrapper = mount(app); - assert.strictEqual(wrapper.find('.github-CoAuthorForm-name').node.value, name); + assert.strictEqual(wrapper.find('.github-CoAuthorForm-name').prop('value'), name); }); }); @@ -60,7 +60,7 @@ describe('CoAuthorForm', function() { wrapper = mount(app); const submitButton = wrapper.find('.btn-primary'); - assert.isTrue(submitButton.node.disabled); + assert.isTrue(submitButton.prop('disabled')); submitButton.simulate('click'); assert.isFalse(didSubmit.called); }); @@ -74,7 +74,7 @@ describe('CoAuthorForm', function() { setTextIn('.github-CoAuthorForm-email', email); const submitButton = wrapper.find('.btn-primary'); - assert.isTrue(submitButton.node.disabled); + assert.isTrue(submitButton.prop('disabled')); submitButton.simulate('click'); assert.isFalse(didSubmit.called); }); @@ -89,7 +89,7 @@ describe('CoAuthorForm', function() { it('calls cancel prop when `core:cancel` is triggered', function() { wrapper = mount(app); - atomEnv.commands.dispatch(wrapper.find('.github-CoAuthorForm').getNode(), 'core:cancel'); + atomEnv.commands.dispatch(wrapper.find('.github-CoAuthorForm').getDOMNode(), 'core:cancel'); assert.isTrue(didCancel.called); }); }); diff --git a/test/views/commands.test.js b/test/views/commands.test.js index 3882cd5633..7030296d2d 100644 --- a/test/views/commands.test.js +++ b/test/views/commands.test.js @@ -2,6 +2,7 @@ import React from 'react'; import {mount} from 'enzyme'; import Commands, {Command} from '../../lib/views/commands'; +import RefHolder from '../../lib/models/ref-holder'; describe('Commands', function() { let atomEnv, commandRegistry; @@ -89,4 +90,22 @@ describe('Commands', function() { assert.equal(callback1.callCount, 0); assert.equal(callback2.callCount, 1); }); + + it('allows the target prop to be a RefHolder to capture refs from parent components', function() { + const callback = sinon.spy(); + const holder = new RefHolder(); + mount( + + + , + ); + + const element = document.createElement('div'); + commandRegistry.dispatch(element, 'github:do-thing'); + assert.isFalse(callback.called); + + holder.setter(element); + commandRegistry.dispatch(element, 'github:do-thing'); + assert.isTrue(callback.called); + }); }); diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js index f0dc1efc00..61474a5ea3 100644 --- a/test/views/commit-view.test.js +++ b/test/views/commit-view.test.js @@ -62,39 +62,38 @@ describe('CommitView', function() { it('displays the remaining characters limit based on which line is being edited', function() { const wrapper = mount(app); - const remaining = wrapper.find('.github-CommitView-remaining-characters'); - assert.strictEqual(remaining.text(), '72'); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '72'); wrapper.setProps({message: 'abcde fghij'}); - assert.strictEqual(remaining.text(), '61'); - assert.isFalse(remaining.hasClass('is-error')); - assert.isFalse(remaining.hasClass('is-warning')); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '61'); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({message: '\nklmno'}); - assert.strictEqual(remaining.text(), '∞'); - assert.isFalse(remaining.hasClass('is-error')); - assert.isFalse(remaining.hasClass('is-warning')); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '∞'); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({message: 'abcde\npqrst'}); - assert.strictEqual(remaining.text(), '∞'); - assert.isFalse(remaining.hasClass('is-error')); - assert.isFalse(remaining.hasClass('is-warning')); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '∞'); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); - wrapper.find('atom-text-editor').getNode().getModel().setCursorBufferPosition([0, 3]); + wrapper.find('atom-text-editor').getDOMNode().getModel().setCursorBufferPosition([0, 3]); wrapper.update(); - assert.strictEqual(remaining.text(), '67'); - assert.isFalse(remaining.hasClass('is-error')); - assert.isFalse(remaining.hasClass('is-warning')); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '67'); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({stagedChangesExist: true, maximumCharacterLimit: 50}); - assert.strictEqual(remaining.text(), '45'); - assert.isFalse(remaining.hasClass('is-error')); - assert.isFalse(remaining.hasClass('is-warning')); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '45'); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({message: 'a'.repeat(41)}); - assert.strictEqual(remaining.text(), '9'); - assert.isFalse(remaining.hasClass('is-error')); - assert.isTrue(remaining.hasClass('is-warning')); + assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '9'); + assert.isFalse(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-error')); + assert.isTrue(wrapper.find('.github-CommitView-remaining-characters').hasClass('is-warning')); wrapper.setProps({message: 'a'.repeat(58)}); assert.strictEqual(wrapper.find('.github-CommitView-remaining-characters').text(), '-8'); @@ -103,7 +102,7 @@ describe('CommitView', function() { }); describe('the commit button', function() { - let wrapper, commitButton; + let wrapper; beforeEach(async function() { const workdirPath = await cloneRepository('three-files'); @@ -116,53 +115,51 @@ describe('CommitView', function() { message: 'something', }); wrapper = mount(app); - - commitButton = wrapper.find('.github-CommitView-commit'); }); it('is disabled when no changes are staged', function() { wrapper.setProps({stagedChangesExist: false}); - assert.isTrue(commitButton.prop('disabled')); + assert.isTrue(wrapper.find('.github-CommitView-commit').prop('disabled')); wrapper.setProps({stagedChangesExist: true}); - assert.isFalse(commitButton.prop('disabled')); + assert.isFalse(wrapper.find('.github-CommitView-commit').prop('disabled')); }); it('is disabled when there are merge conflicts', function() { wrapper.setProps({mergeConflictsExist: true}); - assert.isTrue(commitButton.prop('disabled')); + assert.isTrue(wrapper.find('.github-CommitView-commit').prop('disabled')); wrapper.setProps({mergeConflictsExist: false}); - assert.isFalse(commitButton.prop('disabled')); + assert.isFalse(wrapper.find('.github-CommitView-commit').prop('disabled')); }); it('is disabled when the commit message is empty', function() { wrapper.setProps({message: ''}); - assert.isTrue(commitButton.prop('disabled')); + assert.isTrue(wrapper.find('.github-CommitView-commit').prop('disabled')); wrapper.setProps({message: 'Not empty'}); - assert.isFalse(commitButton.prop('disabled')); + assert.isFalse(wrapper.find('.github-CommitView-commit').prop('disabled')); }); it('displays the current branch name', function() { const currentBranch = new Branch('aw-do-the-stuff'); wrapper.setProps({currentBranch}); - assert.strictEqual(commitButton.text(), 'Commit to aw-do-the-stuff'); + assert.strictEqual(wrapper.find('.github-CommitView-commit').text(), 'Commit to aw-do-the-stuff'); }); it('indicates when a commit will be detached', function() { const currentBranch = Branch.createDetached('master~3'); wrapper.setProps({currentBranch}); - assert.strictEqual(commitButton.text(), 'Create detached commit'); + assert.strictEqual(wrapper.find('.github-CommitView-commit').text(), 'Create detached commit'); }); it('displays a progress message while committing', function() { wrapper.setState({showWorking: true}); - assert.strictEqual(commitButton.text(), 'Working...'); + assert.strictEqual(wrapper.find('.github-CommitView-commit').text(), 'Working...'); }); it('falls back to "commit" with no current branch', function() { - assert.strictEqual(commitButton.text(), 'Commit'); + assert.strictEqual(wrapper.find('.github-CommitView-commit').text(), 'Commit'); }); }); @@ -177,16 +174,15 @@ describe('CommitView', function() { app = React.cloneElement(app, {stagedChangesExist: true, prepareToCommit, commit, message: 'Something'}); wrapper = mount(app); - editorElement = wrapper.find('atom-text-editor').getNode(); + editorElement = wrapper.find('atom-text-editor').getDOMNode(); sinon.spy(editorElement, 'focus'); editor = editorElement.getModel(); // Perform an extra render to ensure the editor text is reflected in the commit button enablement. // The controller accomplishes this by re-rendering on Repository update. - wrapper.update(); + wrapper.setProps({}); commitButton = wrapper.find('.github-CommitView-commit'); - workspaceElement = atomEnv.views.getView(atomEnv.workspace); }); @@ -196,6 +192,7 @@ describe('CommitView', function() { }); it('calls props.commit(message) when the commit button is clicked', async function() { + wrapper.update(); commitButton.simulate('click'); await assert.async.isTrue(commit.calledWith('Something')); diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 4292216d0e..59132587d3 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -82,6 +82,7 @@ describe('FilePatchView', function() { getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]); getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[1]); wrapper.instance().mouseup(); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); @@ -91,6 +92,7 @@ describe('FilePatchView', function() { // start a new selection, drag it across an existing selection getHunkView(1).prop('mousedownOnLine')({button: 0, detail: 1, metaKey: true}, hunks[1], hunks[1].lines[3]); getHunkView(0).prop('mousemoveOnLine')({}, hunks[0], hunks[0].lines[0]); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); @@ -102,6 +104,7 @@ describe('FilePatchView', function() { // drag back down without releasing mouse; the other selection remains intact getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[3]); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isFalse(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); @@ -114,6 +117,7 @@ describe('FilePatchView', function() { // drag back up so selections are adjacent, then release the mouse. selections should merge. getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[2]); wrapper.instance().mouseup(); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); @@ -124,12 +128,14 @@ describe('FilePatchView', function() { // we detect merged selections based on the head here wrapper.instance().selectToNext(); + wrapper.update(); assert.isFalse(getHunkView(0).prop('isSelected')); assert.isFalse(getHunkView(0).prop('selectedLines').has(hunks[0].lines[2])); // double-clicking clears the existing selection and starts hunk-wise selection getHunkView(0).prop('mousedownOnLine')({button: 0, detail: 2}, hunks[0], hunks[0].lines[2]); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); @@ -137,6 +143,7 @@ describe('FilePatchView', function() { assert.isFalse(getHunkView(1).prop('isSelected')); getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[1]); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); @@ -148,6 +155,7 @@ describe('FilePatchView', function() { // clicking the header clears the existing selection and starts hunk-wise selection getHunkView(0).prop('mousedownOnHeader')({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); @@ -155,6 +163,7 @@ describe('FilePatchView', function() { assert.isFalse(getHunkView(1).prop('isSelected')); getHunkView(1).prop('mousemoveOnLine')({}, hunks[1], hunks[1].lines[1]); + wrapper.update(); assert.isTrue(getHunkView(0).prop('isSelected')); assert.isTrue(getHunkView(0).prop('selectedLines').has(hunks[0].lines[1])); @@ -470,6 +479,7 @@ describe('FilePatchView', function() { assert.equal(wrapper.find('HunkView').prop('stageButtonLabel'), 'Unstage Hunk'); wrapper.instance().togglePatchSelectionMode(); + wrapper.update(); assert.equal(wrapper.find('HunkView').prop('stageButtonLabel'), 'Unstage Selection'); wrapper.setProps({stagingStatus: 'unstaged'}); diff --git a/test/views/init-dialog.test.js b/test/views/init-dialog.test.js index 3991558c49..974f7e5d8f 100644 --- a/test/views/init-dialog.test.js +++ b/test/views/init-dialog.test.js @@ -34,6 +34,7 @@ describe('InitDialog', function() { const setTextIn = function(selector, text) { wrapper.find(selector).getDOMNode().getModel().setText(text); + wrapper.update(); }; it('defaults to your project home path', function() {