diff --git a/docs/focus-management.md b/docs/focus-management.md
index 2b24b5ed39..738a511aa6 100644
--- a/docs/focus-management.md
+++ b/docs/focus-management.md
@@ -29,7 +29,7 @@ We move focus around by registering Atom commands.
For example, in `GitTabView`:
```
- this.props.commandRegistry.add(this.refRoot, {
+ this.props.commands.add(this.refRoot, {
'tool-panel:unfocus': this.blur,
'core:focus-next': this.advanceFocus,
'core:focus-previous': this.retreatFocus,
diff --git a/lib/atom/atom-text-editor.js b/lib/atom/atom-text-editor.js
index 0cfea8bcee..6f1105dde1 100644
--- a/lib/atom/atom-text-editor.js
+++ b/lib/atom/atom-text-editor.js
@@ -36,6 +36,9 @@ export default class AtomTextEditor extends React.Component {
didDestroySelection: PropTypes.func,
hideEmptiness: PropTypes.bool,
+ preselect: PropTypes.bool,
+ className: PropTypes.string,
+ tabIndex: PropTypes.number,
refModel: RefHolderPropType,
@@ -49,6 +52,8 @@ export default class AtomTextEditor extends React.Component {
didDestroySelection: () => {},
hideEmptiness: false,
+ preselect: false,
+ tabIndex: 0,
}
constructor(props) {
@@ -77,6 +82,13 @@ export default class AtomTextEditor extends React.Component {
this.refParent.map(element => {
const editor = new TextEditor(modelProps);
+ editor.getElement().tabIndex = this.props.tabIndex;
+ if (this.props.className) {
+ editor.getElement().classList.add(this.props.className);
+ }
+ if (this.props.preselect) {
+ editor.selectAll();
+ }
element.appendChild(editor.getElement());
this.getRefModel().setter(editor);
this.refElement.setter(editor.getElement());
diff --git a/lib/atom/panel.js b/lib/atom/panel.js
index 9db1de1962..507744a0a5 100644
--- a/lib/atom/panel.js
+++ b/lib/atom/panel.js
@@ -22,14 +22,12 @@ export default class Panel extends React.Component {
children: PropTypes.element.isRequired,
options: PropTypes.object,
onDidClosePanel: PropTypes.func,
- visible: PropTypes.bool,
itemHolder: RefHolderPropType,
}
static defaultProps = {
options: {},
onDidClosePanel: panel => {},
- visible: true,
}
constructor(props) {
@@ -46,21 +44,6 @@ export default class Panel extends React.Component {
this.setupPanel();
}
- shouldComponentUpdate(newProps) {
- return this.props.visible !== newProps.visible;
- }
-
- componentDidUpdate() {
- if (this.didCloseItem) {
- // eslint-disable-next-line no-console
- console.error('Unexpected update in `Panel`: the contained panel has been destroyed');
- }
-
- if (this.panel) {
- this.panel[this.props.visible ? 'show' : 'hide']();
- }
- }
-
render() {
return ReactDOM.createPortal(
this.props.children,
@@ -76,7 +59,7 @@ export default class Panel extends React.Component {
const methodName = `add${location}Panel`;
const item = createItem(this.domNode, this.props.itemHolder);
- const options = {...this.props.options, visible: this.props.visible, item};
+ const options = {...this.props.options, item};
this.panel = this.props.workspace[methodName](options);
this.subscriptions.add(
this.panel.onDidDestroy(() => {
diff --git a/lib/autofocus.js b/lib/autofocus.js
new file mode 100644
index 0000000000..0f9ee9027e
--- /dev/null
+++ b/lib/autofocus.js
@@ -0,0 +1,75 @@
+/**
+ * When triggered, automatically focus the first element ref passed to this object.
+ *
+ * To unconditionally focus a single element:
+ *
+ * ```
+ * class SomeComponent extends React.Component {
+ * constructor(props) {
+ * super(props);
+ * this.autofocus = new Autofocus();
+ * }
+ *
+ * render() {
+ * return (
+ *
+ *
+ *
+ *
+ * );
+ * }
+ *
+ * componentDidMount() {
+ * this.autofocus.trigger();
+ * }
+ * }
+ * ```
+ *
+ * If multiple form elements are present, use `firstTarget` to create the ref instead. The rendered ref you assign the
+ * lowest numeric index will be focused on trigger:
+ *
+ * ```
+ * class SomeComponent extends React.Component {
+ * constructor(props) {
+ * super(props);
+ * this.autofocus = new Autofocus();
+ * }
+ *
+ * render() {
+ * return (
+ *
+ * {this.props.someProp && }
+ *
+ *
+ *
+ * );
+ * }
+ *
+ * componentDidMount() {
+ * this.autofocus.trigger();
+ * }
+ * }
+ * ```
+ *
+ */
+export default class AutoFocus {
+ constructor() {
+ this.index = Infinity;
+ this.captured = null;
+ }
+
+ target = element => this.firstTarget(0)(element);
+
+ firstTarget = index => element => {
+ if (index < this.index) {
+ this.index = index;
+ this.captured = element;
+ }
+ };
+
+ trigger() {
+ if (this.captured !== null) {
+ setTimeout(() => this.captured.focus(), 0);
+ }
+ }
+}
diff --git a/lib/controllers/commit-controller.js b/lib/controllers/commit-controller.js
index 7183eeb31a..f8d64d6bef 100644
--- a/lib/controllers/commit-controller.js
+++ b/lib/controllers/commit-controller.js
@@ -24,7 +24,7 @@ export default class CommitController extends React.Component {
static propTypes = {
workspace: PropTypes.object.isRequired,
grammars: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
tooltips: PropTypes.object.isRequired,
@@ -104,7 +104,7 @@ export default class CommitController extends React.Component {
prepareToCommit={this.props.prepareToCommit}
commit={this.commit}
abortMerge={this.props.abortMerge}
- commandRegistry={this.props.commandRegistry}
+ commands={this.props.commands}
maximumCharacterLimit={72}
messageBuffer={this.commitMessageBuffer}
isMerging={this.props.isMerging}
diff --git a/lib/controllers/dialogs-controller.js b/lib/controllers/dialogs-controller.js
new file mode 100644
index 0000000000..6c3a2f845f
--- /dev/null
+++ b/lib/controllers/dialogs-controller.js
@@ -0,0 +1,154 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import InitDialog from '../views/init-dialog';
+import CloneDialog from '../views/clone-dialog';
+import CredentialDialog from '../views/credential-dialog';
+import OpenIssueishDialog from '../views/open-issueish-dialog';
+import OpenCommitDialog from '../views/open-commit-dialog';
+
+const DIALOG_COMPONENTS = {
+ null: NullDialog,
+ init: InitDialog,
+ clone: CloneDialog,
+ credential: CredentialDialog,
+ issueish: OpenIssueishDialog,
+ commit: OpenCommitDialog,
+};
+
+export default class DialogsController extends React.Component {
+ static propTypes = {
+ // Model
+ request: PropTypes.shape({
+ identifier: PropTypes.string.isRequired,
+ isProgressing: PropTypes.bool.isRequired,
+ }).isRequired,
+
+ // Atom environment
+ workspace: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
+ config: PropTypes.object.isRequired,
+ };
+
+ state = {
+ requestInProgress: null,
+ requestError: [null, null],
+ }
+
+ render() {
+ const DialogComponent = DIALOG_COMPONENTS[this.props.request.identifier];
+ return ;
+ }
+
+ getCommonProps() {
+ const {request} = this.props;
+ const accept = request.isProgressing
+ ? async (...args) => {
+ this.setState({requestError: [null, null], requestInProgress: request});
+ try {
+ const result = await request.accept(...args);
+ this.setState({requestInProgress: null});
+ return result;
+ } catch (error) {
+ this.setState({requestError: [request, error], requestInProgress: null});
+ return undefined;
+ }
+ } : (...args) => {
+ this.setState({requestError: [null, null]});
+ try {
+ return request.accept(...args);
+ } catch (error) {
+ this.setState({requestError: [request, error]});
+ return undefined;
+ }
+ };
+ const wrapped = wrapDialogRequest(request, {accept});
+
+ return {
+ config: this.props.config,
+ commands: this.props.commands,
+ workspace: this.props.workspace,
+ inProgress: this.state.requestInProgress === request,
+ error: this.state.requestError[0] === request ? this.state.requestError[1] : null,
+ request: wrapped,
+ };
+ }
+}
+
+function NullDialog() {
+ return null;
+}
+
+class DialogRequest {
+ constructor(identifier, params = {}) {
+ this.identifier = identifier;
+ this.params = params;
+ this.isProgressing = false;
+ this.accept = () => {};
+ this.cancel = () => {};
+ }
+
+ onAccept(cb) {
+ this.accept = cb;
+ }
+
+ onProgressingAccept(cb) {
+ this.isProgressing = true;
+ this.onAccept(cb);
+ }
+
+ onCancel(cb) {
+ this.cancel = cb;
+ }
+
+ getParams() {
+ return this.params;
+ }
+}
+
+function wrapDialogRequest(original, {accept}) {
+ const dup = new DialogRequest(original.identifier, original.params);
+ dup.isProgressing = original.isProgressing;
+ dup.onAccept(accept);
+ dup.onCancel(original.cancel);
+ return dup;
+}
+
+export const dialogRequests = {
+ null: {
+ identifier: 'null',
+ isProgressing: false,
+ params: {},
+ accept: () => {},
+ cancel: () => {},
+ },
+
+ init({dirPath}) {
+ return new DialogRequest('init', {dirPath});
+ },
+
+ clone(opts) {
+ return new DialogRequest('clone', {
+ sourceURL: '',
+ destPath: '',
+ ...opts,
+ });
+ },
+
+ credential(opts) {
+ return new DialogRequest('credential', {
+ includeUsername: false,
+ includeRemember: false,
+ prompt: 'Please authenticate',
+ ...opts,
+ });
+ },
+
+ issueish() {
+ return new DialogRequest('issueish');
+ },
+
+ commit() {
+ return new DialogRequest('commit');
+ },
+};
diff --git a/lib/controllers/editor-conflict-controller.js b/lib/controllers/editor-conflict-controller.js
index 79f7309512..ad4c3c2848 100644
--- a/lib/controllers/editor-conflict-controller.js
+++ b/lib/controllers/editor-conflict-controller.js
@@ -15,7 +15,7 @@ import {autobind} from '../helpers';
export default class EditorConflictController extends React.Component {
static propTypes = {
editor: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
resolutionProgress: PropTypes.object.isRequired,
isRebase: PropTypes.bool.isRequired,
refreshResolutionProgress: PropTypes.func.isRequired,
@@ -56,7 +56,7 @@ export default class EditorConflictController extends React.Component {
return (
{this.state.conflicts.size > 0 && (
-
+
diff --git a/lib/controllers/git-tab-controller.js b/lib/controllers/git-tab-controller.js
index 647f8f9535..b75afe868d 100644
--- a/lib/controllers/git-tab-controller.js
+++ b/lib/controllers/git-tab-controller.js
@@ -35,7 +35,7 @@ export default class GitTabController extends React.Component {
fetchInProgress: PropTypes.bool.isRequired,
workspace: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
grammars: PropTypes.object.isRequired,
resolutionProgress: PropTypes.object.isRequired,
notificationManager: PropTypes.object.isRequired,
@@ -49,7 +49,7 @@ export default class GitTabController extends React.Component {
undoLastDiscard: PropTypes.func.isRequired,
discardWorkDirChangesForPaths: PropTypes.func.isRequired,
openFiles: PropTypes.func.isRequired,
- initializeRepo: PropTypes.func.isRequired,
+ openInitializeDialog: PropTypes.func.isRequired,
controllerRef: RefHolderPropType,
};
@@ -107,7 +107,7 @@ export default class GitTabController extends React.Component {
resolutionProgress={this.props.resolutionProgress}
workspace={this.props.workspace}
- commandRegistry={this.props.commandRegistry}
+ commands={this.props.commands}
grammars={this.props.grammars}
tooltips={this.props.tooltips}
notificationManager={this.props.notificationManager}
@@ -115,7 +115,7 @@ export default class GitTabController extends React.Component {
confirm={this.props.confirm}
config={this.props.config}
- initializeRepo={this.props.initializeRepo}
+ openInitializeDialog={this.props.openInitializeDialog}
openFiles={this.props.openFiles}
discardWorkDirChangesForPaths={this.props.discardWorkDirChangesForPaths}
undoLastDiscard={this.props.undoLastDiscard}
diff --git a/lib/controllers/recent-commits-controller.js b/lib/controllers/recent-commits-controller.js
index adfd83d2d2..fd64391098 100644
--- a/lib/controllers/recent-commits-controller.js
+++ b/lib/controllers/recent-commits-controller.js
@@ -15,7 +15,7 @@ export default class RecentCommitsController extends React.Component {
undoLastCommit: PropTypes.func.isRequired,
workspace: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
}
static focus = RecentCommitsView.focus
@@ -62,7 +62,7 @@ export default class RecentCommitsController extends React.Component {
selectNextCommit={this.selectNextCommit}
selectPreviousCommit={this.selectPreviousCommit}
selectedCommitSha={this.state.selectedCommitSha}
- commandRegistry={this.props.commandRegistry}
+ commands={this.props.commands}
/>
);
}
diff --git a/lib/controllers/repository-conflict-controller.js b/lib/controllers/repository-conflict-controller.js
index 68aabea1d6..e34b1a490a 100644
--- a/lib/controllers/repository-conflict-controller.js
+++ b/lib/controllers/repository-conflict-controller.js
@@ -19,7 +19,7 @@ const DEFAULT_REPO_DATA = {
export default class RepositoryConflictController extends React.Component {
static propTypes = {
workspace: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
resolutionProgress: PropTypes.object.isRequired,
repository: PropTypes.object.isRequired,
@@ -78,7 +78,7 @@ export default class RepositoryConflictController extends React.Component {
{conflictingEditors.map(editor => (
{
+ this.props.commands.onDidDispatch(event => {
if (event.type && event.type.startsWith('github:')
&& event.detail && event.detail[0] && event.detail[0].contextCommand) {
addEvent('context-menu-action', {
@@ -132,19 +131,20 @@ export default class RootController extends React.Component {
const devMode = global.atom && global.atom.inDevMode();
return (
-
+
{devMode && }
-
-
-
+ this.openInitializeDialog()} />
+ this.openCloneDialog()} />
+ this.openIssueishDialog()} />
+ this.openCommitDialog()} />
- {this.renderInitDialog()}
- {this.renderCloneDialog()}
- {this.renderCredentialDialog()}
- {this.renderOpenIssueishDialog()}
- {this.renderOpenCommitDialog()}
-
- );
- }
-
- renderInitDialog() {
- if (!this.state.initDialogActive) {
- return null;
- }
-
- return (
-
-
-
- );
- }
-
- renderCloneDialog() {
- if (!this.state.cloneDialogActive) {
- return null;
- }
-
- return (
-
-
-
- );
- }
-
- renderOpenIssueishDialog() {
- if (!this.state.openIssueishDialogActive) {
- return null;
- }
-
- return (
-
-
-
- );
- }
-
- renderOpenCommitDialog() {
- if (!this.state.openCommitDialogActive) {
- return null;
- }
-
- return (
-
-
-
- );
- }
-
- renderCredentialDialog() {
- if (this.state.credentialDialogQuery === null) {
- return null;
- }
-
- return (
-
-
-
+
);
}
@@ -286,7 +204,7 @@ export default class RootController extends React.Component {
return (
);
}
@@ -322,7 +240,7 @@ export default class RootController extends React.Component {
@@ -566,120 +484,110 @@ export default class RootController extends React.Component {
return this.props.loginModel.removeToken('https://api.github.com');
}
- initializeRepo(initDialogPath) {
- if (this.state.initDialogActive) {
- return null;
- }
+ closeDialog = () => new Promise(resolve => this.setState({dialogRequest: dialogRequests.null}, resolve));
- if (!initDialogPath) {
- initDialogPath = this.props.repository.getWorkingDirectoryPath();
+ openInitializeDialog = async dirPath => {
+ if (!dirPath) {
+ const activeEditor = this.props.workspace.getActiveTextEditor();
+ if (activeEditor) {
+ const [projectPath] = this.props.project.relativizePath(activeEditor.getPath());
+ if (projectPath) {
+ dirPath = projectPath;
+ }
+ }
}
- return new Promise(resolve => {
- this.setState({initDialogActive: true, initDialogPath, initDialogResolve: resolve});
- });
- }
+ if (!dirPath) {
+ const directories = this.props.project.getDirectories();
+ const withRepositories = await Promise.all(
+ directories.map(async d => [d, await this.props.project.repositoryForDirectory(d)]),
+ );
+ const firstUninitialized = withRepositories.find(([d, r]) => !r);
+ if (firstUninitialized && firstUninitialized[0]) {
+ dirPath = firstUninitialized[0].getPath();
+ }
+ }
- toggleCommitPreviewItem = () => {
- const workdir = this.props.repository.getWorkingDirectoryPath();
- return this.props.workspace.toggle(CommitPreviewItem.buildURI(workdir));
- }
+ if (!dirPath) {
+ dirPath = this.props.config.get('core.projectHome');
+ }
- showOpenIssueishDialog() {
- this.setState({openIssueishDialogActive: true});
- }
+ const dialogRequest = dialogRequests.init({dirPath});
+ dialogRequest.onProgressingAccept(async chosenPath => {
+ await this.props.initialize(chosenPath);
+ await this.closeDialog();
+ });
+ dialogRequest.onCancel(this.closeDialog);
- showOpenCommitDialog = () => {
- this.setState({openCommitDialogActive: true});
+ return new Promise(resolve => this.setState({dialogRequest}, resolve));
}
- showWaterfallDiagnostics() {
- this.props.workspace.open(GitTimingsView.buildURI());
- }
+ openCloneDialog = opts => {
+ const dialogRequest = dialogRequests.clone(opts);
+ dialogRequest.onProgressingAccept(async (url, chosenPath) => {
+ await this.props.clone(url, chosenPath);
+ await this.closeDialog();
+ });
+ dialogRequest.onCancel(this.closeDialog);
- showCacheDiagnostics() {
- this.props.workspace.open(GitCacheView.buildURI());
+ return new Promise(resolve => this.setState({dialogRequest}, resolve));
}
- async acceptClone(remoteUrl, projectPath) {
- this.setState({cloneDialogInProgress: true});
- try {
- await this.props.cloneRepositoryForProjectPath(remoteUrl, projectPath);
- addEvent('clone-repo', {package: 'github'});
- } catch (e) {
- this.props.notificationManager.addError(
- `Unable to clone ${remoteUrl}`,
- {detail: e.stdErr, dismissable: true},
- );
- } finally {
- this.setState({cloneDialogInProgress: false, cloneDialogActive: false});
- }
- }
+ openCredentialsDialog = query => {
+ return new Promise((resolve, reject) => {
+ const dialogRequest = dialogRequests.credential(query);
+ dialogRequest.onProgressingAccept(async result => {
+ resolve(result);
+ await this.closeDialog();
+ });
+ dialogRequest.onCancel(async () => {
+ reject();
+ await this.closeDialog();
+ });
- cancelClone() {
- this.setState({cloneDialogActive: false});
+ this.setState({dialogRequest});
+ });
}
- async acceptInit(projectPath) {
- try {
- await this.props.createRepositoryForProjectPath(projectPath);
- if (this.state.initDialogResolve) { this.state.initDialogResolve(projectPath); }
- } catch (e) {
- this.props.notificationManager.addError(
- `Unable to initialize git repository in ${projectPath}`,
- {detail: e.stdErr, dismissable: true},
- );
- } finally {
- this.setState({initDialogActive: false, initDialogPath: null, initDialogResolve: null});
- }
- }
+ openIssueishDialog = () => {
+ const dialogRequest = dialogRequests.issueish();
+ dialogRequest.onProgressingAccept(async url => {
+ await openIssueishItem(url, {
+ workspace: this.props.workspace,
+ workdir: this.props.repository.getWorkingDirectoryPath(),
+ });
+ await this.closeDialog();
+ });
+ dialogRequest.onCancel(this.closeDialog);
- cancelInit() {
- if (this.state.initDialogResolve) { this.state.initDialogResolve(false); }
- this.setState({initDialogActive: false, initDialogPath: null, initDialogResolve: null});
+ return new Promise(resolve => this.setState({dialogRequest}, resolve));
}
- acceptOpenIssueish({repoOwner, repoName, issueishNumber}) {
- const uri = IssueishDetailItem.buildURI({
- host: 'github.com',
- owner: repoOwner,
- repo: repoName,
- number: issueishNumber,
- });
- this.setState({openIssueishDialogActive: false});
- this.props.workspace.open(uri).then(() => {
- addEvent('open-issueish-in-pane', {package: 'github', from: 'dialog'});
+ openCommitDialog = () => {
+ const dialogRequest = dialogRequests.commit();
+ dialogRequest.onProgressingAccept(async ref => {
+ await openCommitDetailItem(ref, {
+ workspace: this.props.workspace,
+ repository: this.props.repository,
+ });
+ await this.closeDialog();
});
- }
+ dialogRequest.onCancel(this.closeDialog);
- cancelOpenIssueish() {
- this.setState({openIssueishDialogActive: false});
+ return new Promise(resolve => this.setState({dialogRequest}, resolve));
}
- isValidCommit = async ref => {
- try {
- await this.props.repository.getCommit(ref);
- return true;
- } catch (error) {
- if (error instanceof GitError && error.code === 128) {
- return false;
- } else {
- throw error;
- }
- }
+ toggleCommitPreviewItem = () => {
+ const workdir = this.props.repository.getWorkingDirectoryPath();
+ return this.props.workspace.toggle(CommitPreviewItem.buildURI(workdir));
}
- acceptOpenCommit = ({ref}) => {
- const workdir = this.props.repository.getWorkingDirectoryPath();
- const uri = CommitDetailItem.buildURI(workdir, ref);
- this.setState({openCommitDialogActive: false});
- this.props.workspace.open(uri).then(() => {
- addEvent('open-commit-in-pane', {package: 'github', from: OpenCommitDialog.name});
- });
+ showWaterfallDiagnostics() {
+ this.props.workspace.open(GitTimingsView.buildURI());
}
- cancelOpenCommit = () => {
- this.setState({openCommitDialogActive: false});
+ showCacheDiagnostics() {
+ this.props.workspace.open(GitCacheView.buildURI());
}
surfaceFromFileAtPath = (filePath, stagingStatus) => {
@@ -705,10 +613,6 @@ export default class RootController extends React.Component {
destroyEmptyFilePatchPaneItems(this.props.workspace);
}
- openCloneDialog() {
- this.setState({cloneDialogActive: true});
- }
-
quietlySelectItem(filePath, stagingStatus) {
const gitTab = this.gitTabTracker.getComponent();
return gitTab && gitTab.quietlySelectItem(filePath, stagingStatus);
@@ -959,22 +863,6 @@ export default class RootController extends React.Component {
});
});
}
-
- /*
- * Display the credential entry dialog. Return a Promise that will resolve with the provided credentials on accept
- * or reject on cancel.
- */
- promptForCredentials(query) {
- return new Promise((resolve, reject) => {
- this.setState({
- credentialDialogQuery: {
- ...query,
- onSubmit: response => this.setState({credentialDialogQuery: null}, () => resolve(response)),
- onCancel: () => this.setState({credentialDialogQuery: null}, reject),
- },
- });
- });
- }
}
class TabTracker {
diff --git a/lib/controllers/status-bar-tile-controller.js b/lib/controllers/status-bar-tile-controller.js
index 07842aa630..80c0ca3994 100644
--- a/lib/controllers/status-bar-tile-controller.js
+++ b/lib/controllers/status-bar-tile-controller.js
@@ -16,7 +16,7 @@ export default class StatusBarTileController extends React.Component {
static propTypes = {
workspace: PropTypes.object.isRequired,
notificationManager: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
tooltips: PropTypes.object.isRequired,
confirm: PropTypes.func.isRequired,
repository: PropTypes.object.isRequired,
@@ -111,7 +111,7 @@ export default class StatusBarTileController extends React.Component {
return (
-
+
this.controller.promptForCredentials(query),
+ promptCallback: query => this.controller.openCredentialsDialog(query),
pipelineManager: this.pipelineManager,
});
@@ -273,7 +273,7 @@ export default class GithubPackage {
ref={c => { this.controller = c; }}
workspace={this.workspace}
deserializers={this.deserializers}
- commandRegistry={this.commandRegistry}
+ commands={this.commands}
notificationManager={this.notificationManager}
tooltips={this.tooltips}
grammars={this.grammars}
@@ -286,8 +286,8 @@ export default class GithubPackage {
repository={this.getActiveRepository()}
resolutionProgress={this.getActiveResolutionProgress()}
statusBar={this.statusBar}
- createRepositoryForProjectPath={this.createRepositoryForProjectPath}
- cloneRepositoryForProjectPath={this.cloneRepositoryForProjectPath}
+ initialize={this.initialize}
+ clone={this.clone}
switchboard={this.switchboard}
startOpen={this.startOpen}
startRevealed={this.startRevealed}
@@ -425,7 +425,7 @@ export default class GithubPackage {
}
}
- async createRepositoryForProjectPath(projectPath) {
+ initialize = async projectPath => {
await fs.mkdirs(projectPath);
const repository = this.contextPool.add(projectPath).getRepository();
@@ -436,10 +436,11 @@ export default class GithubPackage {
this.project.addPath(projectPath);
}
+ await this.refreshAtomGitRepository(projectPath);
await this.scheduleActiveContextUpdate();
}
- async cloneRepositoryForProjectPath(remoteUrl, projectPath) {
+ clone = async (remoteUrl, projectPath) => {
const context = this.contextPool.getContext(projectPath);
let repository;
if (context.isPresent()) {
@@ -585,11 +586,16 @@ export default class GithubPackage {
this.setActiveContext(nextActiveContext);
}
- refreshAtomGitRepository(workdir) {
- const atomGitRepo = this.project.getRepositories().find(repo => {
- return repo && path.normalize(repo.getWorkingDirectory()) === workdir;
- });
- return atomGitRepo ? atomGitRepo.refreshStatus() : Promise.resolve();
+ async refreshAtomGitRepository(workdir) {
+ const directory = this.project.getDirectoryForProjectPath(workdir);
+ if (!directory) {
+ return;
+ }
+
+ const atomGitRepo = await this.project.repositoryForDirectory(directory);
+ if (atomGitRepo) {
+ await atomGitRepo.refreshStatus();
+ }
}
}
diff --git a/lib/index.js b/lib/index.js
index accea10ea8..814bca9773 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -6,7 +6,7 @@ const entry = {
pack = new GithubPackage({
workspace: atom.workspace,
project: atom.project,
- commandRegistry: atom.commands,
+ commands: atom.commands,
notificationManager: atom.notifications,
tooltips: atom.tooltips,
styles: atom.styles,
diff --git a/lib/views/branch-menu-view.js b/lib/views/branch-menu-view.js
index e81adf7254..85b0edcb0e 100644
--- a/lib/views/branch-menu-view.js
+++ b/lib/views/branch-menu-view.js
@@ -10,7 +10,7 @@ import {autobind} from '../helpers';
export default class BranchMenuView extends React.Component {
static propTypes = {
workspace: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
notificationManager: PropTypes.object.isRequired,
repository: PropTypes.object,
branches: BranchSetPropType.isRequired,
@@ -85,7 +85,7 @@ export default class BranchMenuView extends React.Component {
return (
-
+
diff --git a/lib/views/clone-dialog.js b/lib/views/clone-dialog.js
index 68d41854be..32bb97d8d1 100644
--- a/lib/views/clone-dialog.js
+++ b/lib/views/clone-dialog.js
@@ -1,172 +1,129 @@
import React from 'react';
import PropTypes from 'prop-types';
import {CompositeDisposable} from 'event-kit';
+import {TextBuffer} from 'atom';
import url from 'url';
import path from 'path';
-import Commands, {Command} from '../atom/commands';
-import {autobind} from '../helpers';
+import AtomTextEditor from '../atom/atom-text-editor';
+import AutoFocus from '../autofocus';
+import DialogView from './dialog-view';
export default class CloneDialog extends React.Component {
static propTypes = {
- config: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ // Model
+ request: PropTypes.shape({
+ getParams: PropTypes.func.isRequired,
+ accept: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+ }).isRequired,
inProgress: PropTypes.bool,
- didAccept: PropTypes.func,
- didCancel: PropTypes.func,
- }
+ error: PropTypes.instanceOf(Error),
- static defaultProps = {
- inProgress: false,
- didAccept: () => {},
- didCancel: () => {},
+ // Atom environment
+ workspace: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
+ config: PropTypes.object.isRequired,
}
- constructor(props, context) {
- super(props, context);
- autobind(this, 'clone', 'cancel', 'didChangeRemoteUrl', 'didChangeProjectPath', 'editorRefs');
+ constructor(props) {
+ super(props);
+
+ const params = this.props.request.getParams();
+ this.sourceURL = new TextBuffer({text: params.sourceURL});
+ this.destinationPath = new TextBuffer({
+ text: params.destPath || this.props.config.get('core.projectHome'),
+ });
+ this.destinationPathModified = false;
this.state = {
- cloneDisabled: false,
+ acceptEnabled: false,
};
- this.projectHome = this.props.config.get('core.projectHome');
- this.subs = new CompositeDisposable();
- }
-
- componentDidMount() {
- if (this.projectPathEditor) {
- this.projectPathEditor.setText(this.props.config.get('core.projectHome'));
- this.projectPathModified = false;
- }
+ this.subs = new CompositeDisposable(
+ this.sourceURL.onDidChange(this.didChangeSourceUrl),
+ this.destinationPath.onDidChange(this.didChangeDestinationPath),
+ );
- if (this.remoteUrlElement) {
- setTimeout(() => this.remoteUrlElement.focus());
- }
+ this.autofocus = new AutoFocus();
}
render() {
- if (!this.props.inProgress) {
- return this.renderDialog();
- } else {
- return this.renderSpinner();
- }
- }
-
- renderDialog() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
);
}
- renderSpinner() {
- return (
-
-
-
-
- Cloning {this.getRemoteUrl()}
-
-
-
- );
+ componentDidMount() {
+ this.autofocus.trigger();
}
- clone() {
- if (this.getRemoteUrl().length === 0 || this.getProjectPath().length === 0) {
- return;
+ accept = () => {
+ const sourceURL = this.sourceURL.getText();
+ const destinationPath = this.destinationPath.getText();
+ if (sourceURL === '' || destinationPath === '') {
+ return Promise.resolve();
}
- this.props.didAccept(this.getRemoteUrl(), this.getProjectPath());
+ return this.props.request.accept(sourceURL, destinationPath);
}
- cancel() {
- this.props.didCancel();
- }
-
- didChangeRemoteUrl() {
- if (!this.projectPathModified) {
- const name = path.basename(url.parse(this.getRemoteUrl()).pathname, '.git') || '';
+ didChangeSourceUrl = () => {
+ if (!this.destinationPathModified) {
+ const name = path.basename(url.parse(this.sourceURL.getText()).pathname, '.git') || '';
if (name.length > 0) {
- const proposedPath = path.join(this.projectHome, name);
- this.projectPathEditor.setText(proposedPath);
- this.projectPathModified = false;
+ const proposedPath = path.join(this.props.config.get('core.projectHome'), name);
+ this.destinationPath.setText(proposedPath);
+ this.destinationPathModified = false;
}
}
- this.setCloneEnablement();
- }
-
- didChangeProjectPath() {
- this.projectPathModified = true;
- this.setCloneEnablement();
- }
-
- editorRefs(baseName) {
- const elementName = `${baseName}Element`;
- const modelName = `${baseName}Editor`;
- const subName = `${baseName}Subs`;
- const changeMethodName = `didChange${baseName[0].toUpperCase()}${baseName.substring(1)}`;
-
- return element => {
- if (!element) {
- return;
- }
-
- this[elementName] = element;
- const editor = element.getModel();
- if (this[modelName] !== editor) {
- this[modelName] = editor;
-
- if (this[subName]) {
- this[subName].dispose();
- this.subs.remove(this[subName]);
- }
-
- this[subName] = editor.onDidChange(this[changeMethodName]);
- this.subs.add(this[subName]);
- }
- };
- }
-
- getProjectPath() {
- return this.projectPathEditor ? this.projectPathEditor.getText() : '';
+ this.setAcceptEnablement();
}
- getRemoteUrl() {
- return this.remoteUrlEditor ? this.remoteUrlEditor.getText() : '';
+ didChangeDestinationPath = () => {
+ this.destinationPathModified = true;
+ this.setAcceptEnablement();
}
- setCloneEnablement() {
- const disabled = this.getRemoteUrl().length === 0 || this.getProjectPath().length === 0;
- this.setState({cloneDisabled: disabled});
+ setAcceptEnablement = () => {
+ const enabled = !this.sourceURL.isEmpty() && !this.destinationPath.isEmpty();
+ if (enabled !== this.state.acceptEnabled) {
+ this.setState({acceptEnabled: enabled});
+ }
}
}
diff --git a/lib/views/co-author-form.js b/lib/views/co-author-form.js
index 23d5f9f1ba..92efdd695c 100644
--- a/lib/views/co-author-form.js
+++ b/lib/views/co-author-form.js
@@ -7,7 +7,7 @@ import {autobind} from '../helpers';
export default class CoAuthorForm extends React.Component {
static propTypes = {
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
onSubmit: PropTypes.func,
onCancel: PropTypes.func,
name: PropTypes.string,
@@ -36,7 +36,7 @@ export default class CoAuthorForm extends React.Component {
render() {
return (
-
+
diff --git a/lib/views/commit-view.js b/lib/views/commit-view.js
index a12b50f0c9..0572af2f3a 100644
--- a/lib/views/commit-view.js
+++ b/lib/views/commit-view.js
@@ -40,7 +40,7 @@ export default class CommitView extends React.Component {
workspace: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
tooltips: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
lastCommit: PropTypes.object.isRequired,
currentBranch: PropTypes.object.isRequired,
@@ -147,14 +147,14 @@ export default class CommitView extends React.Component {
return (
-
+
-
+
@@ -168,7 +168,7 @@ export default class CommitView extends React.Component {
-
+
@@ -358,7 +358,7 @@ export default class CommitView extends React.Component {
return (
{},
- onCancel: () => {},
- }
+ constructor(props) {
+ super(props);
- constructor(props, context) {
- super(props, context);
- autobind(this, 'confirm', 'cancel', 'onUsernameChange', 'onPasswordChange', 'onRememberChange',
- 'focusFirstInput', 'toggleShowPassword');
+ this.autofocus = new AutoFocus();
this.state = {
username: '',
@@ -34,101 +33,108 @@ export default class CredentialDialog extends React.Component {
};
}
- componentDidMount() {
- setTimeout(this.focusFirstInput);
- }
-
render() {
+ const request = this.props.request;
+ const params = request.getParams();
+
return (
-
-
-
-
-
-
-
- {this.props.includeUsername ? (
-
- ) : null}
-
-
-
+ )}
+
+ Password:
+
+
+
+ {params.includeRemember && (
+
+
+ Remember
+
+ )}
+
+
);
}
- confirm() {
+ componentDidMount() {
+ this.autofocus.trigger();
+ }
+
+ recaptureFocus = () => this.autofocus.trigger();
+
+ accept = () => {
+ if (!this.canSignIn()) {
+ return Promise.resolve();
+ }
+
+ const request = this.props.request;
+ const params = request.getParams();
+
const payload = {password: this.state.password};
- if (this.props.includeUsername) {
+ if (params.includeUsername) {
payload.username = this.state.username;
}
- if (this.props.includeRemember) {
+ if (params.includeRemember) {
payload.remember = this.state.remember;
}
- this.props.onSubmit(payload);
+ return request.accept(payload);
}
- cancel() {
- this.props.onCancel();
- }
+ didChangeUsername = e => this.setState({username: e.target.value});
- onUsernameChange(e) {
- this.setState({username: e.target.value});
- }
+ didChangePassword = e => this.setState({password: e.target.value});
- onPasswordChange(e) {
- this.setState({password: e.target.value});
- }
+ didChangeRemember = e => this.setState({remember: e.target.checked});
- onRememberChange(e) {
- this.setState({remember: e.target.checked});
- }
-
- focusFirstInput() {
- (this.usernameInput || this.passwordInput).focus();
- }
+ toggleShowPassword = () => this.setState({showPassword: !this.state.showPassword});
- toggleShowPassword() {
- this.setState({showPassword: !this.state.showPassword});
+ canSignIn() {
+ return !this.props.request.getParams().includeUsername || this.state.username.length > 0;
}
}
diff --git a/lib/views/dialog-view.js b/lib/views/dialog-view.js
new file mode 100644
index 0000000000..5c6fe497e9
--- /dev/null
+++ b/lib/views/dialog-view.js
@@ -0,0 +1,93 @@
+import React, {Fragment} from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+
+import Commands, {Command} from '../atom/commands';
+import Panel from '../atom/panel';
+
+export default class DialogView extends React.Component {
+ static propTypes = {
+ // Customization
+ prompt: PropTypes.string,
+ progressMessage: PropTypes.string,
+ acceptEnabled: PropTypes.bool,
+ acceptTabIndex: PropTypes.number,
+ acceptClassName: PropTypes.string,
+ acceptText: PropTypes.string,
+ cancelTabIndex: PropTypes.number,
+
+ // Callbacks
+ accept: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+
+ // State
+ autofocus: PropTypes.shape({
+ trigger: PropTypes.func.isRequired,
+ }),
+ inProgress: PropTypes.bool.isRequired,
+ error: PropTypes.instanceOf(Error),
+
+ // Atom environment
+ workspace: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
+
+ // Form content
+ children: PropTypes.node.isRequired,
+ }
+
+ static defaultProps = {
+ acceptTabIndex: 0,
+ acceptEnabled: true,
+ acceptText: 'Accept',
+ cancelTabIndex: 0,
+ }
+
+ render() {
+ return (
+
+ this.props.autofocus.trigger()}>
+
+
+
+
+ {this.props.prompt && (
+
+ )}
+
+ {this.props.children}
+
+
+
+
+ );
+ }
+}
diff --git a/lib/views/git-tab-view.js b/lib/views/git-tab-view.js
index 20e54b9164..53c01408ce 100644
--- a/lib/views/git-tab-view.js
+++ b/lib/views/git-tab-view.js
@@ -40,7 +40,7 @@ export default class GitTabView extends React.Component {
updateSelectedCoAuthors: PropTypes.func.isRequired,
workspace: PropTypes.object.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
grammars: PropTypes.object.isRequired,
resolutionProgress: PropTypes.object.isRequired,
notificationManager: PropTypes.object.isRequired,
@@ -48,7 +48,7 @@ export default class GitTabView extends React.Component {
project: PropTypes.object.isRequired,
tooltips: PropTypes.object.isRequired,
- initializeRepo: PropTypes.func.isRequired,
+ openInitializeDialog: PropTypes.func.isRequired,
abortMerge: PropTypes.func.isRequired,
commit: PropTypes.func.isRequired,
undoLastCommit: PropTypes.func.isRequired,
@@ -75,7 +75,7 @@ export default class GitTabView extends React.Component {
componentDidMount() {
this.props.refRoot.map(root => {
return this.subscriptions.add(
- this.props.commandRegistry.add(root, {
+ this.props.commands.add(root, {
'tool-panel:unfocus': this.blur,
'core:focus-next': this.advanceFocus,
'core:focus-previous': this.retreatFocus,
@@ -143,7 +143,7 @@ export default class GitTabView extends React.Component {
ref={this.props.refRoot.setter}>
{},
- didCancel: () => {},
- }
+ constructor(props) {
+ super(props);
- constructor(props, context) {
- super(props, context);
- autobind(this, 'init', 'cancel', 'editorRef', 'setInitEnablement');
+ this.autofocus = new AutoFocus();
- this.state = {
- initDisabled: false,
- };
-
- this.subs = new CompositeDisposable();
- }
+ this.destinationPath = new TextBuffer({
+ text: this.props.request.getParams().dirPath,
+ });
- componentDidMount() {
- if (this.projectPathEditor) {
- this.projectPathEditor.setText(this.props.initPath || this.props.config.get('core.projectHome'));
- this.projectPathModified = false;
- }
+ this.sub = this.destinationPath.onDidChange(this.setAcceptEnablement);
- if (this.projectPathElement) {
- setTimeout(() => this.projectPathElement.focus());
- }
+ this.state = {
+ acceptEnabled: !this.destinationPath.isEmpty(),
+ };
}
render() {
return (
-
-
-
-
-
-
-
- Initialize git repository in directory
-
-
-
-
-
-
-
-
+
+
+
+ Initialize git repository in directory
+
+
+
+
);
}
- init() {
- if (this.getProjectPath().length === 0) {
- return;
- }
-
- this.props.didAccept(this.getProjectPath());
+ componentDidMount() {
+ this.autofocus.trigger();
}
- cancel() {
- this.props.didCancel();
+ componentWillUnmount() {
+ this.sub.dispose();
}
- editorRef() {
- return element => {
- if (!element) {
- return;
- }
-
- this.projectPathElement = element;
- const editor = element.getModel();
- if (this.projectPathEditor !== editor) {
- this.projectPathEditor = editor;
-
- if (this.projectPathSubs) {
- this.projectPathSubs.dispose();
- this.subs.remove(this.projectPathSubs);
- }
-
- this.projectPathSubs = editor.onDidChange(this.setInitEnablement);
- this.subs.add(this.projectPathSubs);
- }
- };
- }
-
- getProjectPath() {
- return this.projectPathEditor ? this.projectPathEditor.getText() : '';
- }
+ accept = () => {
+ const destPath = this.destinationPath.getText();
+ if (destPath.length === 0) {
+ return Promise.resolve();
+ }
- getRemoteUrl() {
- return this.remoteUrlEditor ? this.remoteUrlEditor.getText() : '';
+ return this.props.request.accept(destPath);
}
- setInitEnablement() {
- this.setState({initDisabled: this.getProjectPath().length === 0});
+ setAcceptEnablement = () => {
+ const enablement = !this.destinationPath.isEmpty();
+ if (enablement !== this.state.acceptEnabled) {
+ this.setState({acceptEnabled: enablement});
+ }
}
}
diff --git a/lib/views/open-commit-dialog.js b/lib/views/open-commit-dialog.js
index 6878e8bcae..e95106bd03 100644
--- a/lib/views/open-commit-dialog.js
+++ b/lib/views/open-commit-dialog.js
@@ -1,113 +1,106 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {CompositeDisposable} from 'event-kit';
+import {TextBuffer} from 'atom';
-import Commands, {Command} from '../atom/commands';
+import AtomTextEditor from '../atom/atom-text-editor';
+import CommitDetailItem from '../items/commit-detail-item';
+import {GitError} from '../git-shell-out-strategy';
+import DialogView from './dialog-view';
+import AutoFocus from '../autofocus';
+import {addEvent} from '../reporter-proxy';
export default class OpenCommitDialog extends React.Component {
static propTypes = {
- commandRegistry: PropTypes.object.isRequired,
- didAccept: PropTypes.func.isRequired,
- didCancel: PropTypes.func.isRequired,
- isValidEntry: PropTypes.func.isRequired,
+ // Model
+ request: PropTypes.shape({
+ getParams: PropTypes.func.isRequired,
+ accept: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+ }).isRequired,
+ inProgress: PropTypes.bool,
+ error: PropTypes.instanceOf(Error),
+
+ // Atom environment
+ workspace: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
}
- constructor(props, context) {
- super(props, context);
+ constructor(props) {
+ super(props);
+
+ this.ref = new TextBuffer();
+ this.sub = this.ref.onDidChange(this.didChangeRef);
this.state = {
- error: null,
+ acceptEnabled: false,
};
- this.subs = new CompositeDisposable();
- }
- componentDidMount() {
- setTimeout(() => this.commitRefElement.focus());
+ this.autofocus = new AutoFocus();
}
- componentWillUnmount() {
- this.subs.dispose();
+ render() {
+ return (
+
+
+
+ Commit sha or ref:
+
+
+
+
+ );
}
- render() {
- return this.renderDialog();
+ componentDidMount() {
+ this.autofocus.trigger();
}
- renderDialog() {
- return (
-
-
-
-
-
-
-
- Commit sha or Git ref:
-
-
- {this.state.error && {this.state.error}}
-
-
-
-
-
-
- );
+ componentWillUnmount() {
+ this.sub.dispose();
}
- accept = async () => {
- const ref = this.getCommitRef();
- const valid = await this.props.isValidEntry(ref);
- if (valid === true) {
- this.props.didAccept({ref});
- } else {
- this.setState({error: `There is no commit associated with "${ref}" in this repository`});
+ accept = () => {
+ const ref = this.ref.getText();
+ if (ref.length === 0) {
+ return Promise.resolve();
}
+
+ return this.props.request.accept(ref);
}
- cancel = () => this.props.didCancel()
-
- editorRefs = baseName => {
- const elementName = `${baseName}Element`;
- const modelName = `${baseName}Editor`;
- const subName = `${baseName}Subs`;
- const changeMethodName = `didChange${baseName[0].toUpperCase()}${baseName.substring(1)}`;
-
- return element => {
- if (!element) {
- return;
- }
-
- this[elementName] = element;
- const editor = element.getModel();
- if (this[modelName] !== editor) {
- this[modelName] = editor;
-
- /* istanbul ignore if */
- if (this[subName]) {
- this[subName].dispose();
- this.subs.remove(this[subName]);
- }
-
- this[subName] = editor.onDidChange(this[changeMethodName]);
- this.subs.add(this[subName]);
- }
- };
+ didChangeRef = () => {
+ const enabled = !this.ref.isEmpty();
+ if (this.state.acceptEnabled !== enabled) {
+ this.setState({acceptEnabled: enabled});
+ }
}
+}
- didChangeCommitRef = () => new Promise(resolve => {
- this.setState({error: null}, resolve);
- })
+export async function openCommitDetailItem(ref, {workspace, repository}) {
+ try {
+ await repository.getCommit(ref);
+ } catch (error) {
+ if (error instanceof GitError && error.code === 128) {
+ error.userMessage = 'There is no commit associated with that reference.';
+ }
- getCommitRef() {
- return this.commitRefEditor ? this.commitRefEditor.getText() : '';
+ throw error;
}
+
+ const item = await workspace.open(
+ CommitDetailItem.buildURI(repository.getWorkingDirectoryPath(), ref),
+ {searchAllPanes: true},
+ );
+ addEvent('open-commit-in-pane', {package: 'github', from: OpenCommitDialog.name});
+ return item;
}
diff --git a/lib/views/open-issueish-dialog.js b/lib/views/open-issueish-dialog.js
index dbd21ff04d..39d6ac95c8 100644
--- a/lib/views/open-issueish-dialog.js
+++ b/lib/views/open-issueish-dialog.js
@@ -1,125 +1,88 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {CompositeDisposable} from 'event-kit';
+import {TextBuffer} from 'atom';
-import Commands, {Command} from '../atom/commands';
-import {autobind} from '../helpers';
+import AtomTextEditor from '../atom/atom-text-editor';
+import IssueishDetailItem from '../items/issueish-detail-item';
+import AutoFocus from '../autofocus';
+import DialogView from './dialog-view';
+import {addEvent} from '../reporter-proxy';
-const ISSUEISH_URL_REGEX = /^(?:https?:\/\/)?github.com\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/;
+const ISSUEISH_URL_REGEX = /^(?:https?:\/\/)?(github.com)\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)/;
export default class OpenIssueishDialog extends React.Component {
static propTypes = {
- commandRegistry: PropTypes.object.isRequired,
- didAccept: PropTypes.func,
- didCancel: PropTypes.func,
+ // Model
+ request: PropTypes.shape({
+ getParams: PropTypes.func.isRequired,
+ accept: PropTypes.func.isRequired,
+ cancel: PropTypes.func.isRequired,
+ }).isRequired,
+ inProgress: PropTypes.bool,
+ error: PropTypes.instanceOf(Error),
+
+ // Atom environment
+ workspace: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
}
- static defaultProps = {
- didAccept: () => {},
- didCancel: () => {},
- }
+ constructor(props) {
+ super(props);
- constructor(props, context) {
- super(props, context);
- autobind(this, 'accept', 'cancel', 'editorRefs', 'didChangeIssueishUrl');
+ this.url = new TextBuffer();
this.state = {
- cloneDisabled: false,
+ acceptEnabled: false,
};
- this.subs = new CompositeDisposable();
- }
+ this.sub = this.url.onDidChange(this.didChangeURL);
- componentDidMount() {
- if (this.issueishUrlElement) {
- setTimeout(() => this.issueishUrlElement.focus());
- }
+ this.autofocus = new AutoFocus();
}
render() {
- return this.renderDialog();
- }
-
- renderDialog() {
return (
-
-
-
-
-
-
-
- Issue or pull request URL:
-
-
- {this.state.error && {this.state.error}}
-
-
-
-
-
-
+
+
+
+ Issue or pull request URL:
+
+
+
+
);
}
- accept() {
- if (this.getIssueishUrl().length === 0) {
- return;
- }
-
- const parsed = this.parseUrl();
- if (!parsed) {
- this.setState({
- error: 'That is not a valid issue or pull request URL.',
- });
- return;
- }
- const {repoOwner, repoName, issueishNumber} = parsed;
-
- this.props.didAccept({repoOwner, repoName, issueishNumber});
+ componentDidMount() {
+ this.autofocus.trigger();
}
- cancel() {
- this.props.didCancel();
+ componentWillUnmount() {
+ this.sub.dispose();
}
- editorRefs(baseName) {
- const elementName = `${baseName}Element`;
- const modelName = `${baseName}Editor`;
- const subName = `${baseName}Subs`;
- const changeMethodName = `didChange${baseName[0].toUpperCase()}${baseName.substring(1)}`;
-
- return element => {
- if (!element) {
- return;
- }
-
- this[elementName] = element;
- const editor = element.getModel();
- if (this[modelName] !== editor) {
- this[modelName] = editor;
-
- if (this[subName]) {
- this[subName].dispose();
- this.subs.remove(this[subName]);
- }
-
- this[subName] = editor.onDidChange(this[changeMethodName]);
- this.subs.add(this[subName]);
- }
- };
- }
+ accept = () => {
+ const issueishURL = this.url.getText();
+ if (issueishURL.length === 0) {
+ return Promise.resolve();
+ }
- didChangeIssueishUrl() {
- this.setState({error: null});
+ return this.props.request.accept(issueishURL);
}
parseUrl() {
@@ -132,7 +95,22 @@ export default class OpenIssueishDialog extends React.Component {
return {repoOwner, repoName, issueishNumber};
}
- getIssueishUrl() {
- return this.issueishUrlEditor ? this.issueishUrlEditor.getText() : '';
+ didChangeURL = () => {
+ const enabled = !this.url.isEmpty();
+ if (this.state.acceptEnabled !== enabled) {
+ this.setState({acceptEnabled: enabled});
+ }
+ }
+}
+
+export async function openIssueishItem(issueishURL, {workspace, workdir}) {
+ const matches = ISSUEISH_URL_REGEX.exec(issueishURL);
+ if (!matches) {
+ throw new Error('Not a valid issue or pull request URL');
}
+ const [, host, owner, repo, number] = matches;
+ const uri = IssueishDetailItem.buildURI({host, owner, repo, number, workdir});
+ const item = await workspace.open(uri, {searchAllPanes: true});
+ addEvent('open-issueish-in-pane', {package: 'github', from: 'dialog'});
+ return item;
}
diff --git a/lib/views/recent-commits-view.js b/lib/views/recent-commits-view.js
index aee9718f2c..0624bfb0db 100644
--- a/lib/views/recent-commits-view.js
+++ b/lib/views/recent-commits-view.js
@@ -118,7 +118,7 @@ export default class RecentCommitsView extends React.Component {
selectedCommitSha: PropTypes.string.isRequired,
// Atom environment
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
// Action methods
undoLastCommit: PropTypes.func.isRequired,
@@ -160,7 +160,7 @@ export default class RecentCommitsView extends React.Component {
render() {
return (
-
+
diff --git a/lib/views/staging-view.js b/lib/views/staging-view.js
index 474db1aaa0..fa73b425fc 100644
--- a/lib/views/staging-view.js
+++ b/lib/views/staging-view.js
@@ -56,7 +56,7 @@ export default class StagingView extends React.Component {
workingDirectoryPath: PropTypes.string,
resolutionProgress: PropTypes.object,
hasUndoHistory: PropTypes.bool.isRequired,
- commandRegistry: PropTypes.object.isRequired,
+ commands: PropTypes.object.isRequired,
notificationManager: PropTypes.object.isRequired,
workspace: PropTypes.object.isRequired,
openFiles: PropTypes.func.isRequired,
@@ -267,7 +267,7 @@ export default class StagingView extends React.Component {
renderCommands() {
return (
-
+
this.selectPrevious()} />
this.selectNext()} />
@@ -288,7 +288,7 @@ export default class StagingView extends React.Component {
-
+
diff --git a/styles/dialog.less b/styles/dialog.less
index ef9df6efbe..17c58524f8 100644
--- a/styles/dialog.less
+++ b/styles/dialog.less
@@ -3,14 +3,14 @@
@github-dialog-spacing: @component-padding / 2;
.github-Dialog {
-
&Prompt {
margin: @component-padding;
text-align: center;
font-size: 1.2em;
+ user-select: none;
}
- &Inputs {
+ &Form {
display: flex;
flex-direction: column;
padding: @github-dialog-spacing;
@@ -22,22 +22,48 @@
position: relative;
line-height: 2;
- &Button {
- position: absolute;
- background: transparent;
- right: .3em;
- bottom: 0;
- border: none;
- color: @text-color-subtle;
- cursor: pointer;
+ &--horizontal {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+
+ input {
+ margin: 0 @component-padding;
+ }
- &:hover {
- color: @text-color-highlight;
+ input[type="text"] , input[type="password"] {
+ width: 85%;
}
}
+
+ .github-AtomTextEditor-container {
+ margin-top: @github-dialog-spacing;
+ }
+ }
+
+ &Info {
+ display: inline-block;
+
+ li {
+ display: inline-block;
+ }
+ }
+
+ &Footer {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &Info {
+ flex-grow: 1;
+ margin: @github-dialog-spacing;
+ padding: @github-dialog-spacing;
}
&Buttons {
+ flex-grow: 0;
display: flex;
align-items: center;
justify-content: flex-end;
@@ -60,6 +86,26 @@
}
}
+ &--insetButton {
+ position: absolute;
+ background: transparent;
+ right: 1em;
+ top: 0;
+ bottom: 0;
+ border: none;
+ color: @text-color-subtle;
+ cursor: pointer;
+
+ &:hover {
+ color: @text-color-highlight;
+ }
+
+ &:focus {
+ border: solid 1px @button-background-color-selected;
+ border-radius: 4px;
+ }
+ }
+
&Spinner {
display: flex;
align-items: center;
@@ -73,5 +119,17 @@
atom-text-editor[mini].editor {
margin: 0;
+
+ &[readOnly] {
+ color: @text-color-subtle;
+ }
+ }
+
+ // A trick to trap keyboard focus within this DOM element.
+ // In JavaScript, attach an event handler to the onTransitionEnd event and reassign focus to the initial element
+ // within the dialog.
+ &:not(:focus-within) {
+ padding-bottom: 1px;
+ transition: padding-bottom 0.01s;
}
}
diff --git a/test/atom/atom-text-editor.test.js b/test/atom/atom-text-editor.test.js
index ce2fc700cd..297260cb9b 100644
--- a/test/atom/atom-text-editor.test.js
+++ b/test/atom/atom-text-editor.test.js
@@ -85,6 +85,25 @@ describe('AtomTextEditor', function() {
assert.strictEqual(editor.getText(), 'changed');
});
+ it('mount with all text preselected on request', function() {
+ const buffer = new TextBuffer();
+ buffer.setText('precreated\ntwo lines\n');
+
+ mount(
+ ,
+ );
+
+ const editor = refModel.get();
+
+ assert.strictEqual(editor.getText(), 'precreated\ntwo lines\n');
+ assert.strictEqual(editor.getSelectedText(), 'precreated\ntwo lines\n');
+ });
+
it('updates changed attributes on re-render', function() {
const app = mount(
+
);
const wrapper = mount(app);
- commandRegistry.dispatch(element, 'github:do-thing1');
+ commands.dispatch(element, 'github:do-thing1');
assert.equal(callback1.callCount, 1);
- commandRegistry.dispatch(element, 'github:do-thing2');
+ commands.dispatch(element, 'github:do-thing2');
assert.equal(callback2.callCount, 1);
await new Promise(resolve => {
@@ -39,18 +39,18 @@ describe('Commands', function() {
callback1.reset();
callback2.reset();
- commandRegistry.dispatch(element, 'github:do-thing1');
+ commands.dispatch(element, 'github:do-thing1');
assert.equal(callback1.callCount, 1);
- commandRegistry.dispatch(element, 'github:do-thing2');
+ commands.dispatch(element, 'github:do-thing2');
assert.equal(callback2.callCount, 0);
wrapper.unmount();
callback1.reset();
callback2.reset();
- commandRegistry.dispatch(element, 'github:do-thing1');
+ commands.dispatch(element, 'github:do-thing1');
assert.equal(callback1.callCount, 0);
- commandRegistry.dispatch(element, 'github:do-thing2');
+ commands.dispatch(element, 'github:do-thing2');
assert.equal(callback2.callCount, 0);
});
@@ -63,7 +63,7 @@ describe('Commands', function() {
render() {
return (
;
const wrapper = mount(app);
- commandRegistry.dispatch(element, 'user:command1');
+ commands.dispatch(element, 'user:command1');
assert.equal(callback1.callCount, 1);
await new Promise(resolve => {
@@ -83,10 +83,10 @@ describe('Commands', function() {
});
callback1.reset();
- commandRegistry.dispatch(element, 'user:command1');
+ commands.dispatch(element, 'user:command1');
assert.equal(callback1.callCount, 0);
assert.equal(callback2.callCount, 0);
- commandRegistry.dispatch(element, 'user:command2');
+ commands.dispatch(element, 'user:command2');
assert.equal(callback1.callCount, 0);
assert.equal(callback2.callCount, 1);
});
@@ -95,17 +95,17 @@ describe('Commands', function() {
const callback = sinon.spy();
const holder = new RefHolder();
mount(
-
+
,
);
const element = document.createElement('div');
- commandRegistry.dispatch(element, 'github:do-thing');
+ commands.dispatch(element, 'github:do-thing');
assert.isFalse(callback.called);
holder.setter(element);
- commandRegistry.dispatch(element, 'github:do-thing');
+ commands.dispatch(element, 'github:do-thing');
assert.isTrue(callback.called);
});
});
diff --git a/test/atom/panel.test.js b/test/atom/panel.test.js
index e8474f9e8a..afe0a8446c 100644
--- a/test/atom/panel.test.js
+++ b/test/atom/panel.test.js
@@ -51,7 +51,6 @@ describe('Panel', function() {
assert.strictEqual(workspace.addLeftPanel.callCount, 1);
const options = workspace.addLeftPanel.args[0][0];
assert.strictEqual(options.some, 'option');
- assert.isTrue(options.visible);
assert.isDefined(options.item.getElement());
const panel = wrapper.instance().getPanel();
@@ -69,22 +68,4 @@ describe('Panel', function() {
wrapper.instance().getPanel().destroy();
assert.strictEqual(onDidClosePanel.callCount, 1);
});
-
- describe('when updating the visible prop', function() {
- it('shows or hides the panel', function() {
- const wrapper = mount(
-
-
- ,
- );
-
- const panel = wrapper.instance().getPanel();
-
- wrapper.setProps({visible: false});
- assert.strictEqual(panel.hide.callCount, 1);
-
- wrapper.setProps({visible: true});
- assert.strictEqual(panel.show.callCount, 1);
- });
- });
});
diff --git a/test/autofocus.test.js b/test/autofocus.test.js
new file mode 100644
index 0000000000..0a6626b20c
--- /dev/null
+++ b/test/autofocus.test.js
@@ -0,0 +1,61 @@
+import AutoFocus from '../lib/autofocus';
+
+describe('AutoFocus', function() {
+ let clock;
+
+ beforeEach(function() {
+ clock = sinon.useFakeTimers();
+ });
+
+ afterEach(function() {
+ clock.restore();
+ });
+
+ it('captures an element and focuses it on trigger', function() {
+ const element = new MockElement();
+ const autofocus = new AutoFocus();
+
+ autofocus.target(element);
+ autofocus.trigger();
+ clock.next();
+
+ assert.isTrue(element.wasFocused());
+ });
+
+ it('captures multiple elements by index and focuses the first on trigger', function() {
+ const element0 = new MockElement();
+ const element1 = new MockElement();
+ const element2 = new MockElement();
+
+ const autofocus = new AutoFocus();
+ autofocus.firstTarget(0)(element0);
+ autofocus.firstTarget(1)(element1);
+ autofocus.firstTarget(2)(element2);
+
+ autofocus.trigger();
+ clock.next();
+
+ assert.isTrue(element0.wasFocused());
+ assert.isFalse(element1.wasFocused());
+ assert.isFalse(element2.wasFocused());
+ });
+
+ it('does nothing on trigger when nothing is captured', function() {
+ const autofocus = new AutoFocus();
+ autofocus.trigger();
+ });
+});
+
+class MockElement {
+ constructor() {
+ this.focused = false;
+ }
+
+ focus() {
+ this.focused = true;
+ }
+
+ wasFocused() {
+ return this.focused;
+ }
+}
diff --git a/test/controllers/commit-controller.test.js b/test/controllers/commit-controller.test.js
index 2b7ba9f32a..e1121a27f6 100644
--- a/test/controllers/commit-controller.test.js
+++ b/test/controllers/commit-controller.test.js
@@ -13,13 +13,13 @@ import {cloneRepository, buildRepository, buildRepositoryWithPipeline, registerG
import * as reporterProxy from '../../lib/reporter-proxy';
describe('CommitController', function() {
- let atomEnvironment, workspace, commandRegistry, notificationManager, lastCommit, config, confirm, tooltips;
+ let atomEnvironment, workspace, commands, notificationManager, lastCommit, config, confirm, tooltips;
let app;
beforeEach(function() {
atomEnvironment = global.buildAtomEnvironment();
workspace = atomEnvironment.workspace;
- commandRegistry = atomEnvironment.commands;
+ commands = atomEnvironment.commands;
notificationManager = atomEnvironment.notifications;
config = atomEnvironment.config;
tooltips = atomEnvironment.tooltips;
@@ -35,7 +35,7 @@ describe('CommitController', function() {
+ );
+ }
+
+ it('renders nothing when a nullDialogRequest is provided', function() {
+ const wrapper = shallow(buildApp({
+ request: dialogRequests.null,
+ }));
+ assert.isTrue(wrapper.exists('NullDialog'));
+ });
+
+ it('renders a chosen dialog when the appropriate DialogRequest is provided', function() {
+ const wrapper = shallow(buildApp({
+ request: dialogRequests.init({dirPath: __dirname}),
+ }));
+ assert.isTrue(wrapper.exists('InitDialog'));
+ });
+
+ it('passes inProgress to the dialog when the accept callback is asynchronous', async function() {
+ let completeWork = () => {};
+ const workPromise = new Promise(resolve => {
+ completeWork = resolve;
+ });
+ const accept = sinon.stub().returns(workPromise);
+
+ const request = dialogRequests.init({dirPath: '/not/home'});
+ request.onProgressingAccept(accept);
+
+ const wrapper = shallow(buildApp({request}));
+ assert.isFalse(wrapper.find('InitDialog').prop('inProgress'));
+ assert.isFalse(accept.called);
+
+ const acceptPromise = wrapper.find('InitDialog').prop('request').accept('an-argument');
+ assert.isTrue(wrapper.find('InitDialog').prop('inProgress'));
+ assert.isTrue(accept.calledWith('an-argument'));
+
+ completeWork('some-result');
+ assert.strictEqual(await acceptPromise, 'some-result');
+
+ wrapper.update();
+ assert.isFalse(wrapper.find('InitDialog').prop('inProgress'));
+ });
+
+ describe('error handling', function() {
+ it('passes a raised error to the dialog when raised during a synchronous accept callback', function() {
+ const e = new Error('wtf');
+ const request = dialogRequests.init({dirPath: __dirname});
+ request.onAccept(() => { throw e; });
+
+ const wrapper = shallow(buildApp({request}));
+ wrapper.find('InitDialog').prop('request').accept();
+ assert.strictEqual(wrapper.find('InitDialog').prop('error'), e);
+ });
+
+ it('passes a raised error to the dialog when raised during an asynchronous accept callback', async function() {
+ let breakWork = () => {};
+ const workPromise = new Promise((_, reject) => {
+ breakWork = reject;
+ });
+ const accept = sinon.stub().returns(workPromise);
+
+ const request = dialogRequests.init({dirPath: '/not/home'});
+ request.onProgressingAccept(accept);
+
+ const wrapper = shallow(buildApp({request}));
+ const acceptPromise = wrapper.find('InitDialog').prop('request').accept('an-argument');
+ assert.isTrue(wrapper.find('InitDialog').prop('inProgress'));
+ assert.isTrue(accept.calledWith('an-argument'));
+
+ const e = new Error('ouch');
+ breakWork(e);
+ await acceptPromise;
+
+ wrapper.update();
+ assert.strictEqual(wrapper.find('InitDialog').prop('error'), e);
+ });
+ });
+
+ describe('specific dialogs', function() {
+ it('passes appropriate props to InitDialog', function() {
+ const accept = sinon.spy();
+ const cancel = sinon.spy();
+ const request = dialogRequests.init({dirPath: '/some/path'});
+ request.onAccept(accept);
+ request.onCancel(cancel);
+
+ const wrapper = shallow(buildApp({request}));
+ const dialog = wrapper.find('InitDialog');
+ assert.strictEqual(dialog.prop('commands'), atomEnv.commands);
+
+ const req = dialog.prop('request');
+
+ req.accept();
+ assert.isTrue(accept.called);
+
+ req.cancel();
+ assert.isTrue(cancel.called);
+
+ assert.strictEqual(req.getParams().dirPath, '/some/path');
+ });
+
+ it('passes appropriate props to CloneDialog', function() {
+ const accept = sinon.spy();
+ const cancel = sinon.spy();
+ const request = dialogRequests.clone({sourceURL: 'git@github.com:atom/github.git', destPath: '/some/path'});
+ request.onAccept(accept);
+ request.onCancel(cancel);
+
+ const wrapper = shallow(buildApp({request}));
+ const dialog = wrapper.find('CloneDialog');
+ assert.strictEqual(dialog.prop('config'), atomEnv.config);
+ assert.strictEqual(dialog.prop('commands'), atomEnv.commands);
+
+ const req = dialog.prop('request');
+
+ req.accept();
+ assert.isTrue(accept.called);
+
+ req.cancel();
+ assert.isTrue(cancel.called);
+
+ assert.strictEqual(req.getParams().sourceURL, 'git@github.com:atom/github.git');
+ assert.strictEqual(req.getParams().destPath, '/some/path');
+ });
+
+ it('passes appropriate props to CredentialDialog', function() {
+ const accept = sinon.spy();
+ const cancel = sinon.spy();
+ const request = dialogRequests.credential({
+ prompt: 'who the hell are you',
+ includeUsername: true,
+ includeRemember: true,
+ });
+ request.onAccept(accept);
+ request.onCancel(cancel);
+
+ const wrapper = shallow(buildApp({request}));
+ const dialog = wrapper.find('CredentialDialog');
+ assert.strictEqual(dialog.prop('commands'), atomEnv.commands);
+
+ const req = dialog.prop('request');
+
+ req.accept({username: 'me', password: 'whatever'});
+ assert.isTrue(accept.calledWith({username: 'me', password: 'whatever'}));
+
+ req.cancel();
+ assert.isTrue(cancel.called);
+
+ assert.strictEqual(req.getParams().prompt, 'who the hell are you');
+ assert.isTrue(req.getParams().includeUsername);
+ assert.isTrue(req.getParams().includeRemember);
+ });
+
+ it('passes appropriate props to OpenIssueishDialog', function() {
+ const accept = sinon.spy();
+ const cancel = sinon.spy();
+ const request = dialogRequests.issueish();
+ request.onAccept(accept);
+ request.onCancel(cancel);
+
+ const wrapper = shallow(buildApp({request}));
+ const dialog = wrapper.find('OpenIssueishDialog');
+ assert.strictEqual(dialog.prop('commands'), atomEnv.commands);
+
+ const req = dialog.prop('request');
+
+ req.accept('https://github.com/atom/github/issue/123');
+ assert.isTrue(accept.calledWith('https://github.com/atom/github/issue/123'));
+
+ req.cancel();
+ assert.isTrue(cancel.called);
+ });
+
+ it('passes appropriate props to OpenCommitDialog', function() {
+ const accept = sinon.spy();
+ const cancel = sinon.spy();
+ const request = dialogRequests.commit();
+ request.onAccept(accept);
+ request.onCancel(cancel);
+
+ const wrapper = shallow(buildApp({request}));
+ const dialog = wrapper.find('OpenCommitDialog');
+ assert.strictEqual(dialog.prop('commands'), atomEnv.commands);
+
+ const req = dialog.prop('request');
+
+ req.accept('abcd1234');
+ assert.isTrue(accept.calledWith('abcd1234'));
+
+ req.cancel();
+ assert.isTrue(cancel.called);
+ });
+ });
+});
diff --git a/test/controllers/editor-conflict-controller.test.js b/test/controllers/editor-conflict-controller.test.js
index 872acb70f2..4de8ac326e 100644
--- a/test/controllers/editor-conflict-controller.test.js
+++ b/test/controllers/editor-conflict-controller.test.js
@@ -31,14 +31,14 @@ More of your changes
Stuff at the very end.`;
describe('EditorConflictController', function() {
- let atomEnv, workspace, commandRegistry, app, wrapper, editor, editorView;
+ let atomEnv, workspace, commands, app, wrapper, editor, editorView;
let resolutionProgress, refreshResolutionProgress;
let fixtureFile;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
workspace = atomEnv.workspace;
- commandRegistry = atomEnv.commands;
+ commands = atomEnv.commands;
refreshResolutionProgress = sinon.spy();
resolutionProgress = new ResolutionProgress();
@@ -61,7 +61,7 @@ describe('EditorConflictController', function() {
app = (
e.setText(newMessage));
- commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit');
+ commands.dispatch(workspaceElement, 'github:amend-last-commit');
// verify that coAuthor was passed
await assert.async.deepEqual(
@@ -600,7 +600,7 @@ describe('GitTabController', function() {
commitView.onSelectedCoAuthorsChanged([]);
// amend again
- commandRegistry.dispatch(workspaceElement, 'github:amend-last-commit');
+ commands.dispatch(workspaceElement, 'github:amend-last-commit');
// verify that NO coAuthor was passed
await assert.async.deepEqual(
repository.commit.args[0][1],
diff --git a/test/controllers/recent-commits-controller.test.js b/test/controllers/recent-commits-controller.test.js
index 875c2fb3fa..949bf58ea8 100644
--- a/test/controllers/recent-commits-controller.test.js
+++ b/test/controllers/recent-commits-controller.test.js
@@ -22,7 +22,7 @@ describe('RecentCommitsController', function() {
isLoading={false}
undoLastCommit={() => { }}
workspace={atomEnv.workspace}
- commandRegistry={atomEnv.commands}
+ commands={atomEnv.commands}
repository={repository}
/>
);
diff --git a/test/controllers/repository-conflict-controller.test.js b/test/controllers/repository-conflict-controller.test.js
index af3963a05e..b1d7e84faa 100644
--- a/test/controllers/repository-conflict-controller.test.js
+++ b/test/controllers/repository-conflict-controller.test.js
@@ -14,9 +14,9 @@ describe('RepositoryConflictController', () => {
atomEnv = global.buildAtomEnvironment();
atomEnv.config.set('github.graphicalConflictResolution', true);
workspace = atomEnv.workspace;
- const commandRegistry = atomEnv.commands;
+ const commands = atomEnv.commands;
- app = ;
+ app = ;
});
afterEach(() => atomEnv.destroy());
diff --git a/test/controllers/root-controller.test.js b/test/controllers/root-controller.test.js
index 91295710de..ee56e91dc0 100644
--- a/test/controllers/root-controller.test.js
+++ b/test/controllers/root-controller.test.js
@@ -4,34 +4,35 @@ import fs from 'fs-extra';
import React from 'react';
import {shallow, mount} from 'enzyme';
import dedent from 'dedent-js';
+import temp from 'temp';
import {cloneRepository, buildRepository} from '../helpers';
import {multiFilePatchBuilder} from '../builder/patch';
-import {GitError} from '../../lib/git-shell-out-strategy';
import Repository from '../../lib/models/repository';
import WorkdirContextPool from '../../lib/models/workdir-context-pool';
+import ResolutionProgress from '../../lib/models/conflicts/resolution-progress';
+import RefHolder from '../../lib/models/ref-holder';
import GithubLoginModel from '../../lib/models/github-login-model';
import {InMemoryStrategy} from '../../lib/shared/keytar-strategy';
+import {dialogRequests} from '../../lib/controllers/dialogs-controller';
import GitTabItem from '../../lib/items/git-tab-item';
import GitHubTabItem from '../../lib/items/github-tab-item';
-import ResolutionProgress from '../../lib/models/conflicts/resolution-progress';
import IssueishDetailItem from '../../lib/items/issueish-detail-item';
import CommitPreviewItem from '../../lib/items/commit-preview-item';
import CommitDetailItem from '../../lib/items/commit-detail-item';
import * as reporterProxy from '../../lib/reporter-proxy';
import RootController from '../../lib/controllers/root-controller';
-import OpenCommitDialog from '../../lib/views/open-commit-dialog';
describe('RootController', function() {
let atomEnv, app;
- let workspace, commandRegistry, notificationManager, tooltips, config, confirm, deserializers, grammars, project;
+ let workspace, commands, notificationManager, tooltips, config, confirm, deserializers, grammars, project;
let workdirContextPool;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
workspace = atomEnv.workspace;
- commandRegistry = atomEnv.commands;
+ commands = atomEnv.commands;
deserializers = atomEnv.deserializers;
grammars = atomEnv.grammars;
notificationManager = atomEnv.notifications;
@@ -49,7 +50,7 @@ describe('RootController', function() {
app = (
{}}
+ clone={() => {}}
+
startOpen={false}
startRevealed={false}
- workdirContextPool={workdirContextPool}
/>
);
});
@@ -255,342 +261,297 @@ describe('RootController', function() {
});
});
- describe('initializeRepo', function() {
- let createRepositoryForProjectPath, resolveInit, rejectInit;
+ describe('initialize', function() {
+ let initialize;
beforeEach(function() {
- createRepositoryForProjectPath = sinon.stub().returns(new Promise((resolve, reject) => {
- resolveInit = resolve;
- rejectInit = reject;
- }));
+ initialize = sinon.stub().resolves();
+ app = React.cloneElement(app, {initialize});
});
- it('renders the modal init panel', function() {
- app = React.cloneElement(app, {createRepositoryForProjectPath});
- const wrapper = shallow(app);
+ it('requests the init dialog with a command', async function() {
+ sinon.stub(config, 'get').returns(path.join('/home/me/src'));
- wrapper.instance().initializeRepo();
- wrapper.update();
+ const wrapper = shallow(app);
- assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('InitDialog'), 1);
+ await wrapper.find('Command[command="github:initialize"]').prop('callback')();
+ const req = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req.identifier, 'init');
+ assert.strictEqual(req.getParams().dirPath, path.join('/home/me/src'));
});
- it('triggers the init callback on accept', function() {
- app = React.cloneElement(app, {createRepositoryForProjectPath});
- const wrapper = shallow(app);
+ it('defaults to the project directory containing the open file if there is one', async function() {
+ const noRepo0 = await new Promise((resolve, reject) => temp.mkdir({}, (err, p) => (err ? reject(err) : resolve(p))));
+ const noRepo1 = await new Promise((resolve, reject) => temp.mkdir({}, (err, p) => (err ? reject(err) : resolve(p))));
+ const filePath = path.join(noRepo1, 'file.txt');
+ await fs.writeFile(filePath, 'stuff\n', {encoding: 'utf8'});
- wrapper.instance().initializeRepo();
- wrapper.update();
- const dialog = wrapper.find('InitDialog');
- dialog.prop('didAccept')('/a/path');
- resolveInit();
+ project.setPaths([noRepo0, noRepo1]);
+ await workspace.open(filePath);
- assert.isTrue(createRepositoryForProjectPath.calledWith('/a/path'));
+ const wrapper = shallow(app);
+ await wrapper.find('Command[command="github:initialize"]').prop('callback')();
+ const req = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req.identifier, 'init');
+ assert.strictEqual(req.getParams().dirPath, noRepo1);
});
- it('dismisses the init callback on cancel', function() {
- app = React.cloneElement(app, {createRepositoryForProjectPath});
- const wrapper = shallow(app);
+ it('defaults to the first project directory with no repository if one is present', async function() {
+ const withRepo = await cloneRepository();
+ const noRepo0 = await new Promise((resolve, reject) => temp.mkdir({}, (err, p) => (err ? reject(err) : resolve(p))));
+ const noRepo1 = await new Promise((resolve, reject) => temp.mkdir({}, (err, p) => (err ? reject(err) : resolve(p))));
- wrapper.instance().initializeRepo();
- wrapper.update();
- const dialog = wrapper.find('InitDialog');
- dialog.prop('didCancel')();
+ project.setPaths([withRepo, noRepo0, noRepo1]);
- wrapper.update();
- assert.isFalse(wrapper.find('InitDialog').exists());
+ const wrapper = shallow(app);
+ await wrapper.find('Command[command="github:initialize"]').prop('callback')();
+ const req = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req.identifier, 'init');
+ assert.strictEqual(req.getParams().dirPath, noRepo0);
});
- it('creates a notification if the init fails', async function() {
- sinon.stub(notificationManager, 'addError');
-
- app = React.cloneElement(app, {createRepositoryForProjectPath});
+ it('requests the init dialog from the git tab', async function() {
const wrapper = shallow(app);
+ const gitTabWrapper = wrapper
+ .find('PaneItem[className="github-Git-root"]')
+ .renderProp('children')({itemHolder: new RefHolder()});
- 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');
- err.stdErr = 'this is stderr';
- rejectInit(err);
- await acceptPromise;
+ await gitTabWrapper.find('GitTabItem').prop('openInitializeDialog')(path.join('/some/workdir'));
- wrapper.update();
- assert.isFalse(wrapper.find('InitDialog').exists());
- assert.isTrue(notificationManager.addError.calledWith(
- 'Unable to initialize git repository in /a/path',
- sinon.match({detail: sinon.match(/this is stderr/)}),
- ));
+ const req = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req.identifier, 'init');
+ assert.strictEqual(req.getParams().dirPath, path.join('/some/workdir'));
});
- });
-
- describe('github:open-commit', function() {
- let workdirPath, wrapper, openCommitDetails, resolveOpenCommit, repository;
- beforeEach(async function() {
- openCommitDetails = sinon.stub(atomEnv.workspace, 'open').returns(new Promise(resolve => {
- resolveOpenCommit = resolve;
- }));
+ it('triggers the initialize callback on accept', async function() {
+ const wrapper = shallow(app);
+ await wrapper.find('Command[command="github:initialize"]').prop('callback')();
- workdirPath = await cloneRepository('multiple-commits');
- repository = await buildRepository(workdirPath);
+ const req0 = wrapper.find('DialogsController').prop('request');
+ await req0.accept(path.join('/home/me/src'));
+ assert.isTrue(initialize.calledWith(path.join('/home/me/src')));
- app = React.cloneElement(app, {repository});
- wrapper = shallow(app);
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
- it('renders the modal open-commit panel', function() {
- wrapper.instance().showOpenCommitDialog();
- wrapper.update();
+ it('dismisses the dialog with its cancel callback', async function() {
+ const wrapper = shallow(app);
+ await wrapper.find('Command[command="github:initialize"]').prop('callback')();
- assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('OpenCommitDialog'), 1);
- });
+ const req0 = wrapper.find('DialogsController').prop('request');
+ assert.notStrictEqual(req0, dialogRequests.null);
+ req0.cancel();
- it('triggers the open callback on accept and fires `open-commit-in-pane` event', async function() {
- sinon.stub(reporterProxy, 'addEvent');
- wrapper.instance().showOpenCommitDialog();
- wrapper.update();
+ const req1 = wrapper.update().find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
+ });
+ });
- const dialog = wrapper.find('OpenCommitDialog');
- const ref = 'asdf1234';
+ describe('openCloneDialog()', function() {
+ let clone;
- const promise = dialog.prop('didAccept')({ref});
- resolveOpenCommit();
- await promise;
+ beforeEach(function() {
+ clone = sinon.stub().resolves();
+ app = React.cloneElement(app, {clone});
+ });
- const uri = CommitDetailItem.buildURI(workdirPath, ref);
+ it('requests the clone dialog with a command', function() {
+ sinon.stub(config, 'get').returns(path.join('/home/me/src'));
- assert.isTrue(openCommitDetails.calledWith(uri));
+ const wrapper = shallow(app);
- await assert.isTrue(reporterProxy.addEvent.calledWith('open-commit-in-pane', {package: 'github', from: OpenCommitDialog.name}));
+ wrapper.find('Command[command="github:clone"]').prop('callback')();
+ const req = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req.identifier, 'clone');
+ assert.strictEqual(req.getParams().sourceURL, '');
+ assert.strictEqual(req.getParams().destPath, '');
});
- it('dismisses the open-commit panel on cancel', function() {
- wrapper.instance().showOpenCommitDialog();
- wrapper.update();
+ it('triggers the clone callback on accept', async function() {
+ const wrapper = shallow(app);
+ wrapper.find('Command[command="github:clone"]').prop('callback')();
- const dialog = wrapper.find('OpenCommitDialog');
- dialog.prop('didCancel')();
+ const req0 = wrapper.find('DialogsController').prop('request');
+ await req0.accept('git@github.com:atom/atom.git', path.join('/home/me/src'));
+ assert.isTrue(clone.calledWith('git@github.com:atom/atom.git', path.join('/home/me/src')));
- wrapper.update();
- assert.lengthOf(wrapper.find('OpenCommitDialog'), 0);
- assert.isFalse(openCommitDetails.called);
- assert.isFalse(wrapper.state('openCommitDialogActive'));
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
- describe('isValidCommit', function() {
- it('returns true if commit exists in repo, false if not', async function() {
- assert.isTrue(await wrapper.instance().isValidCommit('HEAD'));
- assert.isFalse(await wrapper.instance().isValidCommit('invalidCommitRef'));
- });
+ it('dismisses the dialog with its cancel callback', function() {
+ const wrapper = shallow(app);
+ wrapper.find('Command[command="github:clone"]').prop('callback')();
- it('re-throws exceptions encountered during validation check', async function() {
- sinon.stub(repository, 'getCommit').throws(new Error('Oh shit'));
- await assert.isRejected(wrapper.instance().isValidCommit('HEAD'), 'Oh shit');
- });
+ const req0 = wrapper.find('DialogsController').prop('request');
+ assert.notStrictEqual(req0, dialogRequests.null);
+ req0.cancel();
+
+ const req1 = wrapper.update().find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
});
- describe('github:open-issue-or-pull-request', function() {
- let workdirPath, wrapper, openIssueishDetails, resolveOpenIssueish;
+ describe('openIssueishDialog()', function() {
+ let repository, workdir;
beforeEach(async function() {
- openIssueishDetails = sinon.stub(atomEnv.workspace, 'open').returns(new Promise(resolve => {
- resolveOpenIssueish = resolve;
- }));
-
- workdirPath = await cloneRepository('multiple-commits');
- const repository = await buildRepository(workdirPath);
-
- app = React.cloneElement(app, {repository});
- wrapper = shallow(app);
+ workdir = await cloneRepository('multiple-commits');
+ repository = await buildRepository(workdir);
});
- it('renders the modal open-commit panel', function() {
- wrapper.instance().showOpenIssueishDialog();
+ it('renders the OpenIssueish dialog', function() {
+ const wrapper = shallow(app);
+ wrapper.find('Command[command="github:open-issue-or-pull-request"]').prop('callback')();
wrapper.update();
- assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('OpenIssueishDialog'), 1);
+ assert.strictEqual(wrapper.find('DialogsController').prop('request').identifier, 'issueish');
});
it('triggers the open callback on accept and fires `open-commit-in-pane` event', async function() {
sinon.stub(reporterProxy, 'addEvent');
- wrapper.instance().showOpenIssueishDialog();
- wrapper.update();
-
- const dialog = wrapper.find('OpenIssueishDialog');
- const repoOwner = 'owner';
- const repoName = 'name';
- const issueishNumber = 1234;
-
- const promise = dialog.prop('didAccept')({repoOwner, repoName, issueishNumber});
- resolveOpenIssueish();
- await promise;
-
- const uri = IssueishDetailItem.buildURI({
- host: 'github.com',
- owner: repoOwner,
- repo: repoName,
- number: issueishNumber,
- });
-
- assert.isTrue(openIssueishDetails.calledWith(uri));
+ sinon.stub(workspace, 'open').resolves();
+
+ const wrapper = shallow(React.cloneElement(app, {repository}));
+ wrapper.find('Command[command="github:open-issue-or-pull-request"]').prop('callback')();
+
+ const req0 = wrapper.find('DialogsController').prop('request');
+ await req0.accept('https://github.com/atom/github/pull/123');
+
+ assert.isTrue(workspace.open.calledWith(
+ IssueishDetailItem.buildURI({
+ host: 'github.com',
+ owner: 'atom',
+ repo: 'github',
+ number: 123,
+ workdir,
+ }),
+ {searchAllPanes: true},
+ ));
+ assert.isTrue(reporterProxy.addEvent.calledWith(
+ 'open-issueish-in-pane', {package: 'github', from: 'dialog'}),
+ );
- await assert.isTrue(reporterProxy.addEvent.calledWith('open-issueish-in-pane', {package: 'github', from: 'dialog'}));
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
- it('dismisses the open-commit panel on cancel', function() {
- wrapper.instance().showOpenIssueishDialog();
+ it('dismisses the OpenIssueish dialog on cancel', function() {
+ const wrapper = shallow(app);
+ wrapper.find('Command[command="github:open-issue-or-pull-request"]').prop('callback')();
wrapper.update();
- const dialog = wrapper.find('OpenIssueishDialog');
- dialog.prop('didCancel')();
+ const req0 = wrapper.find('DialogsController').prop('request');
+ req0.cancel();
wrapper.update();
- assert.lengthOf(wrapper.find('OpenIssueishDialog'), 0);
- assert.isFalse(openIssueishDetails.called);
- assert.isFalse(wrapper.state('openIssueishDialogActive'));
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
});
- describe('github:clone', function() {
- let wrapper, cloneRepositoryForProjectPath, resolveClone, rejectClone;
-
- beforeEach(function() {
- cloneRepositoryForProjectPath = sinon.stub().returns(new Promise((resolve, reject) => {
- resolveClone = resolve;
- rejectClone = reject;
- }));
-
- app = React.cloneElement(app, {cloneRepositoryForProjectPath});
- wrapper = shallow(app);
- });
-
- it('renders the modal clone panel', function() {
- wrapper.instance().openCloneDialog();
- wrapper.update();
-
- assert.lengthOf(wrapper.find('Panel').find({location: 'modal'}).find('CloneDialog'), 1);
- });
+ describe('openCommitDialog()', function() {
+ let workdirPath, repository;
- it('triggers the clone callback on accept and fires `clone-repo` event', async function() {
+ beforeEach(async function() {
sinon.stub(reporterProxy, 'addEvent');
- wrapper.instance().openCloneDialog();
- wrapper.update();
+ sinon.stub(atomEnv.workspace, 'open').resolves('item');
- const dialog = wrapper.find('CloneDialog');
- const promise = dialog.prop('didAccept')('git@github.com:atom/github.git', '/home/me/github');
- resolveClone();
- await promise;
+ workdirPath = await cloneRepository('multiple-commits');
+ repository = await buildRepository(workdirPath);
+ sinon.stub(repository, 'getCommit').callsFake(ref => {
+ return ref === 'abcd1234' ? Promise.resolve('ok') : Promise.reject(new Error('nah'));
+ });
- assert.isTrue(cloneRepositoryForProjectPath.calledWith('git@github.com:atom/github.git', '/home/me/github'));
- await assert.isTrue(reporterProxy.addEvent.calledWith('clone-repo', {package: 'github'}));
+ app = React.cloneElement(app, {repository});
});
- 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;
+ it('renders the OpenCommitDialog', function() {
+ const wrapper = shallow(app);
- wrapper.update();
- assert.isFalse(wrapper.find('CloneDialog').exists());
+ wrapper.find('Command[command="github:open-commit"]').prop('callback')();
+ assert.strictEqual(wrapper.find('DialogsController').prop('request').identifier, 'commit');
});
- it('creates a notification if the clone fails and does not fire `clone-repo` event', async function() {
- sinon.stub(notificationManager, 'addError');
- sinon.stub(reporterProxy, 'addEvent');
-
- wrapper.instance().openCloneDialog();
- wrapper.update();
-
- const dialog = wrapper.find('CloneDialog');
- assert.isFalse(dialog.prop('inProgress'));
+ it('triggers the open callback on accept', async function() {
+ const wrapper = shallow(app);
+ wrapper.find('Command[command="github:open-commit"]').prop('callback')();
- const acceptPromise = dialog.prop('didAccept')('git@github.com:nope/nope.git', '/home/me/github');
- const err = new GitError('git clone exited with status 1');
- err.stdErr = 'this is stderr';
- rejectClone(err);
- await acceptPromise;
+ const req0 = wrapper.find('DialogsController').prop('request');
+ await req0.accept('abcd1234');
- wrapper.update();
- assert.isFalse(wrapper.find('CloneDialog').exists());
- assert.isTrue(notificationManager.addError.calledWith(
- 'Unable to clone git@github.com:nope/nope.git',
- sinon.match({detail: sinon.match(/this is stderr/)}),
+ assert.isTrue(workspace.open.calledWith(
+ CommitDetailItem.buildURI(repository.getWorkingDirectoryPath(), 'abcd1234'),
+ {searchAllPanes: true},
));
- assert.isFalse(reporterProxy.addEvent.called);
+ assert.isTrue(reporterProxy.addEvent.called);
+
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
- it('dismisses the clone panel on cancel', function() {
- wrapper.instance().openCloneDialog();
- wrapper.update();
+ it('dismisses the OpenCommitDialog on cancel', function() {
+ const wrapper = shallow(app);
+ wrapper.find('Command[command="github:open-commit"]').prop('callback')();
- const dialog = wrapper.find('CloneDialog');
- dialog.prop('didCancel')();
+ const req0 = wrapper.find('DialogsController').prop('request');
+ req0.cancel();
wrapper.update();
- assert.lengthOf(wrapper.find('CloneDialog'), 0);
- assert.isFalse(cloneRepositoryForProjectPath.called);
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
});
- describe('promptForCredentials()', function() {
- let wrapper;
-
- beforeEach(function() {
- wrapper = shallow(app);
- });
-
+ describe('openCredentialsDialog()', function() {
it('renders the modal credentials dialog', function() {
- wrapper.instance().promptForCredentials({
+ const wrapper = shallow(app);
+
+ wrapper.instance().openCredentialsDialog({
prompt: 'Password plz',
includeUsername: true,
});
wrapper.update();
- const dialog = wrapper.find('Panel').find({location: 'modal'}).find('CredentialDialog');
- assert.isTrue(dialog.exists());
- assert.equal(dialog.prop('prompt'), 'Password plz');
- assert.isTrue(dialog.prop('includeUsername'));
+ const req = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req.identifier, 'credential');
+ assert.deepEqual(req.getParams(), {
+ prompt: 'Password plz',
+ includeUsername: true,
+ includeRemember: false,
+ });
});
it('resolves the promise with credentials on accept', async function() {
- const credentialPromise = wrapper.instance().promptForCredentials({
+ const wrapper = shallow(app);
+ const credentialPromise = wrapper.instance().openCredentialsDialog({
prompt: 'Speak "friend" and enter',
includeUsername: false,
});
- wrapper.update();
- wrapper.find('CredentialDialog').prop('onSubmit')({password: 'friend'});
+ const req0 = wrapper.find('DialogsController').prop('request');
+ await req0.accept({password: 'friend'});
assert.deepEqual(await credentialPromise, {password: 'friend'});
- wrapper.update();
- assert.isFalse(wrapper.find('CredentialDialog').exists());
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
it('rejects the promise on cancel', async function() {
- const credentialPromise = wrapper.instance().promptForCredentials({
+ const wrapper = shallow(app);
+ const credentialPromise = wrapper.instance().openCredentialsDialog({
prompt: 'Enter the square root of 1244313452349528345',
includeUsername: false,
});
wrapper.update();
- wrapper.find('CredentialDialog').prop('onCancel')();
+ const req0 = wrapper.find('DialogsController').prop('request');
+ await req0.cancel(new Error('cancelled'));
await assert.isRejected(credentialPromise);
- wrapper.update();
- assert.isFalse(wrapper.find('CredentialDialog').exists());
+ const req1 = wrapper.find('DialogsController').prop('request');
+ assert.strictEqual(req1, dialogRequests.null);
});
});
@@ -1245,16 +1206,6 @@ describe('RootController', function() {
assert.strictEqual(item.getTitle(), 'owner/repo#123');
assert.lengthOf(wrapper.update().find('IssueishDetailItem'), 1);
});
-
- describe('acceptOpenIssueish', function() {
- it('records an event', async function() {
- const wrapper = mount(app);
- sinon.stub(reporterProxy, 'addEvent');
- sinon.stub(workspace, 'open').returns(Promise.resolve());
- await wrapper.instance().acceptOpenIssueish({repoOwner: 'owner', repoName: 'repo', issueishNumber: 123});
- assert.isTrue(reporterProxy.addEvent.calledWith('open-issueish-in-pane', {package: 'github', from: 'dialog'}));
- });
- });
});
describe('opening a CommitPreviewItem', function() {
@@ -1297,7 +1248,7 @@ describe('RootController', function() {
});
it('sends an event when a command is triggered via a context menu', function() {
- commandRegistry.dispatch(
+ commands.dispatch(
wrapper.find('CommitView').getDOMNode(),
'github:toggle-expanded-commit-message-editor',
[{contextCommand: true}],
@@ -1310,7 +1261,7 @@ describe('RootController', function() {
});
it('does not send an event when a command is triggered in other ways', function() {
- commandRegistry.dispatch(
+ commands.dispatch(
wrapper.find('CommitView').getDOMNode(),
'github:toggle-expanded-commit-message-editor',
);
@@ -1318,7 +1269,7 @@ describe('RootController', function() {
});
it('does not send an event when a command not starting with github: is triggered via a context menu', function() {
- commandRegistry.dispatch(
+ commands.dispatch(
wrapper.find('CommitView').getDOMNode(),
'core:copy',
[{contextCommand: true}],
diff --git a/test/controllers/status-bar-tile-controller.test.js b/test/controllers/status-bar-tile-controller.test.js
index f5d80c8daf..71c2694e4a 100644
--- a/test/controllers/status-bar-tile-controller.test.js
+++ b/test/controllers/status-bar-tile-controller.test.js
@@ -15,12 +15,12 @@ import GithubTileView from '../../lib/views/github-tile-view';
describe('StatusBarTileController', function() {
let atomEnvironment;
- let workspace, workspaceElement, commandRegistry, notificationManager, tooltips, confirm;
+ let workspace, workspaceElement, commands, notificationManager, tooltips, confirm;
beforeEach(function() {
atomEnvironment = global.buildAtomEnvironment();
workspace = atomEnvironment.workspace;
- commandRegistry = atomEnvironment.commands;
+ commands = atomEnvironment.commands;
notificationManager = atomEnvironment.notifications;
tooltips = atomEnvironment.tooltips;
confirm = sinon.stub(atomEnvironment, 'confirm');
@@ -36,7 +36,7 @@ describe('StatusBarTileController', function() {
return (
{},
+ openInitializeDialog: () => {},
abortMerge: () => {},
commit: () => {},
undoLastCommit: () => {},
diff --git a/test/github-package.test.js b/test/github-package.test.js
index e7a97dc467..6e037704fa 100644
--- a/test/github-package.test.js
+++ b/test/github-package.test.js
@@ -8,7 +8,7 @@ import {fileExists, getTempDir} from '../lib/helpers';
import GithubPackage from '../lib/github-package';
describe('GithubPackage', function() {
- let atomEnv, workspace, project, commandRegistry, notificationManager, grammars, config, keymaps;
+ let atomEnv, workspace, project, commands, notificationManager, grammars, config, keymaps;
let confirm, tooltips, styles;
let getLoadSettings, configDirPath, deserializers;
let githubPackage, contextPool;
@@ -19,7 +19,7 @@ describe('GithubPackage', function() {
workspace = atomEnv.workspace;
project = atomEnv.project;
- commandRegistry = atomEnv.commands;
+ commands = atomEnv.commands;
deserializers = atomEnv.deserializers;
notificationManager = atomEnv.notifications;
tooltips = atomEnv.tooltips;
@@ -32,7 +32,7 @@ describe('GithubPackage', function() {
configDirPath = path.join(__dirname, 'fixtures', 'atomenv-config');
githubPackage = new GithubPackage({
- workspace, project, commandRegistry, notificationManager, tooltips, styles, grammars,
+ workspace, project, commands, notificationManager, tooltips, styles, grammars,
keymaps, config, deserializers,
confirm, getLoadSettings,
configDirPath,
@@ -76,7 +76,7 @@ describe('GithubPackage', function() {
const getLoadSettings1 = () => ({initialPaths});
githubPackage1 = new GithubPackage({
- workspace, project, commandRegistry, notificationManager, tooltips, styles, grammars, keymaps,
+ workspace, project, commands, notificationManager, tooltips, styles, grammars, keymaps,
config, deserializers, confirm, getLoadSettings: getLoadSettings1,
configDirPath,
});
@@ -501,7 +501,7 @@ describe('GithubPackage', function() {
project.setPaths([workdir1]);
await workspace.open(path.join(workdir0, 'a.txt'));
- commandRegistry.dispatch(atomEnv.views.getView(workspace), 'tree-view:toggle-focus');
+ commands.dispatch(atomEnv.views.getView(workspace), 'tree-view:toggle-focus');
workspace.getLeftDock().activate();
await githubPackage.scheduleActiveContextUpdate();
@@ -682,7 +682,7 @@ describe('GithubPackage', function() {
});
});
- describe('createRepositoryForProjectPath()', function() {
+ describe('initialize', function() {
it('creates and sets a repository for the given project path', async function() {
const nonRepositoryPath = await getTempDir();
project.setPaths([nonRepositoryPath]);
@@ -693,7 +693,7 @@ describe('GithubPackage', function() {
assert.isTrue(githubPackage.getActiveRepository().isEmpty());
assert.isFalse(githubPackage.getActiveRepository().isAbsent());
- await githubPackage.createRepositoryForProjectPath(nonRepositoryPath);
+ await githubPackage.initialize(nonRepositoryPath);
assert.isTrue(githubPackage.getActiveRepository().isPresent());
assert.strictEqual(
diff --git a/test/integration/helpers.js b/test/integration/helpers.js
index 616d66f705..4bbce8fa8b 100644
--- a/test/integration/helpers.js
+++ b/test/integration/helpers.js
@@ -79,7 +79,7 @@ export async function setup(options = {}) {
const githubPackage = new GithubPackage({
workspace: atomEnv.workspace,
project: atomEnv.project,
- commandRegistry: atomEnv.commands,
+ commands: atomEnv.commands,
notificationManager: atomEnv.notifications,
tooltips: atomEnv.tooltips,
styles: atomEnv.styles,
diff --git a/test/views/clone-dialog.test.js b/test/views/clone-dialog.test.js
index fd3c9a1f39..42fe583154 100644
--- a/test/views/clone-dialog.test.js
+++ b/test/views/clone-dialog.test.js
@@ -1,123 +1,124 @@
import React from 'react';
-import {mount} from 'enzyme';
+import {shallow} from 'enzyme';
import path from 'path';
import CloneDialog from '../../lib/views/clone-dialog';
+import {dialogRequests} from '../../lib/controllers/dialogs-controller';
describe('CloneDialog', function() {
- let atomEnv, config, commandRegistry;
- let app, wrapper, didAccept, didCancel;
+ let atomEnv;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
- config = atomEnv.config;
- commandRegistry = atomEnv.commands;
- sinon.stub(config, 'get').returns(path.join('home', 'me', 'codes'));
-
- didAccept = sinon.stub();
- didCancel = sinon.stub();
-
- app = (
-
- );
- wrapper = mount(app);
});
afterEach(function() {
atomEnv.destroy();
});
- const setTextIn = function(selector, text) {
- wrapper.find(selector).getDOMNode().getModel().setText(text);
- };
+ function buildApp(overrides = {}) {
+ return (
+
+ );
+ }
describe('entering a remote URL', function() {
it("updates the project path automatically if it hasn't been modified", function() {
- setTextIn('.github-CloneUrl atom-text-editor', 'git@github.com:atom/github.git');
-
- assert.equal(wrapper.instance().getProjectPath(), path.join('home', 'me', 'codes', 'github'));
- });
+ sinon.stub(atomEnv.config, 'get').returns(path.join('/home/me/src'));
+ const wrapper = shallow(buildApp());
- it('updates the project path for https URLs', function() {
- setTextIn('.github-CloneUrl atom-text-editor', 'https://github.com/smashwilson/slack-emojinator.git');
+ wrapper.find('.github-Clone-sourceURL').prop('buffer').setText('git@github.com:atom/github.git');
+ wrapper.update();
+ assert.strictEqual(
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').getText(),
+ path.join('/home/me/src/github'),
+ );
- assert.equal(wrapper.instance().getProjectPath(), path.join('home', 'me', 'codes', 'slack-emojinator'));
+ wrapper.find('.github-Clone-sourceURL').prop('buffer')
+ .setText('https://github.com/smashwilson/slack-emojinator.git');
+ wrapper.update();
+ assert.strictEqual(
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').getText(),
+ path.join('/home/me/src/slack-emojinator'),
+ );
});
it("doesn't update the project path if it has been modified", function() {
- setTextIn('.github-ProjectPath atom-text-editor', path.join('somewhere', 'else'));
- setTextIn('.github-CloneUrl atom-text-editor', 'git@github.com:atom/github.git');
-
- assert.equal(wrapper.instance().getProjectPath(), path.join('somewhere', 'else'));
- });
-
- it('does update the project path if it was modified automatically', function() {
- setTextIn('.github-CloneUrl atom-text-editor', 'git@github.com:atom/atom1.git');
- assert.equal(wrapper.instance().getProjectPath(), path.join('home', 'me', 'codes', 'atom1'));
-
- setTextIn('.github-CloneUrl atom-text-editor', 'git@github.com:atom/atom2.git');
- assert.equal(wrapper.instance().getProjectPath(), path.join('home', 'me', 'codes', 'atom2'));
+ const wrapper = shallow(buildApp());
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').setText(path.join('/somewhere/else'));
+ wrapper.find('.github-Clone-sourceURL').prop('buffer').setText('git@github.com:atom/github.git');
+ assert.strictEqual(
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').getText(),
+ path.join('/somewhere/else'),
+ );
});
});
describe('clone button enablement', 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', '');
+ const wrapper = shallow(buildApp());
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').setText(path.join('/some/where'));
+ wrapper.find('.github-Clone-sourceURL').prop('buffer').setText('');
wrapper.update();
- assert.isTrue(wrapper.find('button.icon-repo-clone').prop('disabled'));
+ assert.isFalse(wrapper.find('DialogView').prop('acceptEnabled'));
});
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');
+ const wrapper = shallow(buildApp());
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').setText('');
+ wrapper.find('.github-Clone-sourceURL').prop('buffer').setText('git@github.com:atom/github.git');
wrapper.update();
- assert.isTrue(wrapper.find('button.icon-repo-clone').prop('disabled'));
+ assert.isFalse(wrapper.find('DialogView').prop('acceptEnabled'));
});
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');
+ const wrapper = shallow(buildApp());
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').setText(path.join('/some/where'));
+ wrapper.find('.github-Clone-sourceURL').prop('buffer').setText('git@github.com:atom/github.git');
wrapper.update();
- assert.isFalse(wrapper.find('button.icon-repo-clone').prop('disabled'));
+ assert.isTrue(wrapper.find('DialogView').prop('acceptEnabled'));
});
});
it('calls the acceptance callback', function() {
- setTextIn('.github-ProjectPath atom-text-editor', '/somewhere/directory/');
- setTextIn('.github-CloneUrl atom-text-editor', 'git@github.com:atom/atom.git');
+ const accept = sinon.spy();
+ const request = dialogRequests.clone();
+ request.onAccept(accept);
+ const wrapper = shallow(buildApp({request}));
- wrapper.find('button.icon-repo-clone').simulate('click');
+ wrapper.find('.github-Clone-destinationPath').prop('buffer').setText(path.join('/some/where'));
+ wrapper.find('.github-Clone-sourceURL').prop('buffer').setText('git@github.com:atom/github.git');
- assert.isTrue(didAccept.calledWith('git@github.com:atom/atom.git', '/somewhere/directory/'));
+ wrapper.find('DialogView').prop('accept')();
+ assert.isTrue(accept.calledWith('git@github.com:atom/github.git', path.join('/some/where')));
});
it('calls the cancellation callback', function() {
- wrapper.find('button.github-CancelButton').simulate('click');
- assert.isTrue(didCancel.called);
+ const cancel = sinon.spy();
+ const request = dialogRequests.clone();
+ request.onCancel(cancel);
+ const wrapper = shallow(buildApp({request}));
+
+ wrapper.find('DialogView').prop('cancel')();
+ assert.isTrue(cancel.called);
});
describe('in progress', function() {
- beforeEach(function() {
- app = React.cloneElement(app, {inProgress: true});
- wrapper = mount(app);
- });
-
- it('conceals the text editors and buttons', function() {
- assert.lengthOf(wrapper.find('atom-text-editor'), 0);
- assert.lengthOf(wrapper.find('.btn'), 0);
- });
+ it('disables the text editors and buttons', function() {
+ const wrapper = shallow(buildApp({inProgress: true}));
- it('displays the progress spinner', function() {
- assert.lengthOf(wrapper.find('.loading'), 1);
+ assert.isTrue(wrapper.find('.github-Clone-sourceURL').prop('readOnly'));
+ assert.isTrue(wrapper.find('.github-Clone-destinationPath').prop('readOnly'));
});
});
});
diff --git a/test/views/co-author-form.test.js b/test/views/co-author-form.test.js
index b2980682ce..cf67e10990 100644
--- a/test/views/co-author-form.test.js
+++ b/test/views/co-author-form.test.js
@@ -16,7 +16,7 @@ describe('CoAuthorForm', function() {
app = (
diff --git a/test/views/commit-view.test.js b/test/views/commit-view.test.js
index fe239fceb0..fe3776b5f4 100644
--- a/test/views/commit-view.test.js
+++ b/test/views/commit-view.test.js
@@ -15,13 +15,13 @@ import StagingView from '../../lib/views/staging-view';
import * as reporterProxy from '../../lib/reporter-proxy';
describe('CommitView', function() {
- let atomEnv, commandRegistry, tooltips, config, lastCommit;
+ let atomEnv, commands, tooltips, config, lastCommit;
let messageBuffer;
let app;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
- commandRegistry = atomEnv.commands;
+ commands = atomEnv.commands;
tooltips = atomEnv.tooltips;
config = atomEnv.config;
@@ -35,7 +35,7 @@ describe('CommitView', function() {
app = (
- );
});
afterEach(function() {
atomEnv.destroy();
});
- const setTextIn = function(selector, text) {
- wrapper.find(selector).simulate('change', {target: {value: text}});
- };
+ function buildApp(overrides = {}) {
+ return (
+
+ );
+ }
- describe('confirm', function() {
+ describe('accept', function() {
it('reports the current username and password', function() {
- wrapper = mount(app);
-
- setTextIn('.github-CredentialDialog-Username', 'someone');
- setTextIn('.github-CredentialDialog-Password', 'letmein');
+ const accept = sinon.spy();
+ const request = dialogRequests.credential({includeUsername: true});
+ request.onAccept(accept);
+ const wrapper = shallow(buildApp({request}));
- wrapper.find('.btn-primary').simulate('click');
+ wrapper.find('.github-Credential-username').simulate('change', {target: {value: 'someone'}});
+ wrapper.find('.github-Credential-password').simulate('change', {target: {value: 'letmein'}});
+ wrapper.find('DialogView').prop('accept')();
- assert.deepEqual(didSubmit.firstCall.args[0], {
+ assert.isTrue(accept.calledWith({
username: 'someone',
password: 'letmein',
- });
+ }));
});
it('omits the username if includeUsername is false', function() {
- wrapper = mount(React.cloneElement(app, {includeUsername: false}));
+ const accept = sinon.spy();
+ const request = dialogRequests.credential({includeUsername: false});
+ request.onAccept(accept);
+ const wrapper = shallow(buildApp({request}));
- assert.isFalse(wrapper.find('.github-CredentialDialog-Username').exists());
- setTextIn('.github-CredentialDialog-Password', 'twowordsuppercase');
+ assert.isFalse(wrapper.find('.github-Credential-username').exists());
+ wrapper.find('.github-Credential-password').simulate('change', {target: {value: 'twowordsuppercase'}});
+ wrapper.find('DialogView').prop('accept')();
- wrapper.find('.btn-primary').simulate('click');
-
- assert.deepEqual(didSubmit.firstCall.args[0], {
+ assert.isTrue(accept.calledWith({
password: 'twowordsuppercase',
- });
+ }));
});
it('includes a "remember me" checkbox', function() {
- wrapper = mount(React.cloneElement(app, {includeRemember: true}));
+ const accept = sinon.spy();
+ const request = dialogRequests.credential({includeUsername: true, includeRemember: true});
+ request.onAccept(accept);
+ const wrapper = shallow(buildApp({request}));
- const rememberBox = wrapper.find('.github-CredentialDialog-remember');
+ const rememberBox = wrapper.find('.github-Credential-remember');
assert.isTrue(rememberBox.exists());
rememberBox.simulate('change', {target: {checked: true}});
- setTextIn('.github-CredentialDialog-Username', 'someone');
- setTextIn('.github-CredentialDialog-Password', 'letmein');
-
- wrapper.find('.btn-primary').simulate('click');
+ wrapper.find('.github-Credential-username').simulate('change', {target: {value: 'someone'}});
+ wrapper.find('.github-Credential-password').simulate('change', {target: {value: 'letmein'}});
+ wrapper.find('DialogView').prop('accept')();
- assert.deepEqual(didSubmit.firstCall.args[0], {
+ assert.isTrue(accept.calledWith({
username: 'someone',
password: 'letmein',
remember: true,
- });
+ }));
});
it('omits the "remember me" checkbox', function() {
- wrapper = mount(app);
- assert.isFalse(wrapper.find('.github-CredentialDialog-remember').exists());
+ const request = dialogRequests.credential({includeRemember: false});
+ const wrapper = shallow(buildApp({request}));
+ assert.isFalse(wrapper.exists('.github-Credential-remember'));
});
});
it('calls the cancel callback', function() {
- wrapper = mount(app);
- wrapper.find('.github-CancelButton').simulate('click');
- assert.isTrue(didCancel.called);
+ const cancel = sinon.spy();
+ const request = dialogRequests.credential();
+ request.onCancel(cancel);
+ const wrapper = shallow(buildApp({request}));
+
+ wrapper.find('DialogView').prop('cancel')();
+ assert.isTrue(cancel.called);
});
describe('show password', function() {
it('sets the passwords input type to "text" on the first click', function() {
- wrapper = mount(app);
-
- wrapper.find('.github-DialogLabelButton').simulate('click');
+ const wrapper = shallow(buildApp());
+ wrapper.find('.github-Credential-visibility').simulate('click');
- const passwordInput = wrapper.find('.github-CredentialDialog-Password');
- assert.equal(passwordInput.prop('type'), 'text');
+ const passwordInput = wrapper.find('.github-Credential-password');
+ assert.strictEqual(passwordInput.prop('type'), 'text');
});
it('sets the passwords input type back to "password" on the second click', function() {
- wrapper = mount(app);
+ const wrapper = shallow(buildApp());
+ wrapper.find('.github-Credential-visibility').simulate('click').simulate('click');
+
+ const passwordInput = wrapper.find('.github-Credential-password');
+ assert.strictEqual(passwordInput.prop('type'), 'password');
+ });
+ });
+
+ describe('sign in button enablement', function() {
+ it('is always enabled when includeUsername is false', function() {
+ const request = dialogRequests.credential({includeUsername: false});
+ const wrapper = shallow(buildApp({request}));
+
+ assert.isTrue(wrapper.find('DialogView').prop('acceptEnabled'));
+ });
+
+ it('is disabled if includeUsername is true and the username is empty', function() {
+ const request = dialogRequests.credential({includeUsername: true});
+ const wrapper = shallow(buildApp({request}));
+
+ assert.isFalse(wrapper.find('DialogView').prop('acceptEnabled'));
+ });
- wrapper.find('.github-DialogLabelButton').simulate('click').simulate('click');
+ it('is enabled if includeUsername is true and the username is populated', function() {
+ const request = dialogRequests.credential({includeUsername: true});
+ const wrapper = shallow(buildApp({request}));
+ wrapper.find('.github-Credential-username').simulate('change', {target: {value: 'nonempty'}});
- const passwordInput = wrapper.find('.github-CredentialDialog-Password');
- assert.equal(passwordInput.prop('type'), 'password');
+ assert.isTrue(wrapper.find('DialogView').prop('acceptEnabled'));
});
});
});
diff --git a/test/views/dialog-view.test.js b/test/views/dialog-view.test.js
new file mode 100644
index 0000000000..e6ea829350
--- /dev/null
+++ b/test/views/dialog-view.test.js
@@ -0,0 +1,169 @@
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import DialogView from '../../lib/views/dialog-view';
+import AutoFocus from '../../lib/autofocus';
+
+describe('DialogView', function() {
+ let atomEnv;
+
+ beforeEach(function() {
+ atomEnv = global.buildAtomEnvironment();
+ });
+
+ afterEach(function() {
+ atomEnv.destroy();
+ });
+
+ function buildApp(overrides = {}) {
+ return (
+ {}}
+ cancel={() => {}}
+ children={}
+ {...overrides}
+ />
+ );
+ }
+
+ it('includes common dialog elements', function() {
+ const wrapper = shallow(buildApp());
+
+ assert.isTrue(wrapper.exists('Panel[location="modal"]'));
+ assert.isTrue(wrapper.exists('.github-Dialog'));
+ assert.isTrue(wrapper.exists('Commands'));
+ assert.isTrue(wrapper.exists('main.github-DialogForm'));
+ assert.isTrue(wrapper.exists('footer.github-DialogFooter'));
+ assert.isTrue(wrapper.exists('.github-DialogInfo'));
+ assert.isTrue(wrapper.exists('.github-DialogButtons'));
+ assert.isTrue(wrapper.exists('.btn.github-Dialog-cancelButton'));
+ assert.isTrue(wrapper.exists('.btn.btn-primary'));
+
+ assert.isFalse(wrapper.exists('header.github-DialogPrompt'));
+ assert.isFalse(wrapper.exists('.loading-spinner-small'));
+ assert.isFalse(wrapper.exists('.error-messages'));
+ });
+
+ describe('customization', function() {
+ it('includes a prompt banner if the prompt prop is provided', function() {
+ const wrapper = shallow(buildApp({prompt: 'some text'}));
+ assert.strictEqual(wrapper.find('header.github-DialogPrompt').text(), 'some text');
+ });
+
+ it('inserts custom form contents', function() {
+ const wrapper = shallow(buildApp({
+ children: ,
+ }));
+ assert.isTrue(wrapper.exists('main .custom'));
+ });
+
+ it('displays a spinner and custom message when in progress', function() {
+ const wrapper = shallow(buildApp({
+ progressMessage: 'crunching numbers',
+ inProgress: true,
+ }));
+ assert.isTrue(wrapper.exists('.loading-spinner-small'));
+ assert.strictEqual(wrapper.find('.github-DialogProgress-message').text(), 'crunching numbers');
+ });
+
+ it('omits the spinner when no progress message is provided', function() {
+ const wrapper = shallow(buildApp({
+ inProgress: true,
+ }));
+ assert.isFalse(wrapper.exists('.loading-spinner-small'));
+ assert.isFalse(wrapper.exists('.github-DialogProgress-message'));
+ });
+
+ it('uses a custom classes and label for the accept button', function() {
+ const wrapper = shallow(buildApp({
+ acceptClassName: 'icon icon-repo-clone',
+ acceptText: 'Engage',
+ }));
+
+ const button = wrapper.find('.btn-primary');
+ assert.isTrue(button.hasClass('icon'));
+ assert.isTrue(button.hasClass('icon-repo-clone'));
+ assert.strictEqual(button.text(), 'Engage');
+ });
+ });
+
+ describe('tabbing', function() {
+ it('defaults the tabIndex of the buttons to 0', function() {
+ const wrapper = shallow(buildApp());
+
+ assert.strictEqual(wrapper.find('.github-Dialog-cancelButton').prop('tabIndex'), 0);
+ assert.strictEqual(wrapper.find('.btn-primary').prop('tabIndex'), 0);
+ });
+
+ it('customizes the tabIndex of the standard buttons', function() {
+ const wrapper = shallow(buildApp({
+ cancelTabIndex: 10,
+ acceptTabIndex: 20,
+ }));
+
+ assert.strictEqual(wrapper.find('.github-Dialog-cancelButton').prop('tabIndex'), 10);
+ assert.strictEqual(wrapper.find('.btn-primary').prop('tabIndex'), 20);
+ });
+
+ it('recaptures focus after it leaves the dialog element', function() {
+ const autofocus = new AutoFocus();
+ const wrapper = shallow(buildApp({autofocus}));
+
+ sinon.spy(autofocus, 'trigger');
+ wrapper.find('.github-Dialog').simulate('transitionEnd');
+ assert.isTrue(autofocus.trigger.called);
+ });
+ });
+
+ it('displays an error with a friendly explanation', function() {
+ const e = new Error('unfriendly');
+ e.userMessage = 'friendly';
+
+ const wrapper = shallow(buildApp({error: e}));
+ assert.strictEqual(wrapper.find('.error-messages li').text(), 'friendly');
+ });
+
+ it('falls back to presenting the regular error message', function() {
+ const e = new Error('other');
+
+ const wrapper = shallow(buildApp({error: e}));
+ assert.strictEqual(wrapper.find('.error-messages li').text(), 'other');
+ });
+
+ it('calls the accept callback on core:confirm event', function() {
+ const accept = sinon.spy();
+ const wrapper = shallow(buildApp({accept}));
+
+ wrapper.find('Command[command="core:confirm"]').prop('callback')();
+ assert.isTrue(accept.called);
+ });
+
+ it('calls the accept callback on an accept button click', function() {
+ const accept = sinon.spy();
+ const wrapper = shallow(buildApp({accept}));
+
+ wrapper.find('.btn-primary').simulate('click');
+ assert.isTrue(accept.called);
+ });
+
+ it('calls the cancel callback on a core:cancel event', function() {
+ const cancel = sinon.spy();
+ const wrapper = shallow(buildApp({cancel}));
+
+ wrapper.find('Command[command="core:cancel"]').prop('callback')();
+ assert.isTrue(cancel.called);
+ });
+
+ it('calls the cancel callback on a cancel button click', function() {
+ const cancel = sinon.spy();
+ const wrapper = shallow(buildApp({cancel}));
+
+ wrapper.find('.github-Dialog-cancelButton').simulate('click');
+ assert.isTrue(cancel.called);
+ });
+});
diff --git a/test/views/init-dialog.test.js b/test/views/init-dialog.test.js
index 974f7e5d8f..d8f11f9623 100644
--- a/test/views/init-dialog.test.js
+++ b/test/views/init-dialog.test.js
@@ -1,69 +1,70 @@
import React from 'react';
-import {mount} from 'enzyme';
+import {shallow} from 'enzyme';
import path from 'path';
import InitDialog from '../../lib/views/init-dialog';
+import {dialogRequests} from '../../lib/controllers/dialogs-controller';
describe('InitDialog', function() {
- let atomEnv, config, commandRegistry;
- let app, wrapper, didAccept, didCancel;
+ let atomEnv;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
- config = atomEnv.config;
- commandRegistry = atomEnv.commands;
- sinon.stub(config, 'get').returns(path.join('home', 'me', 'codes'));
-
- didAccept = sinon.stub();
- didCancel = sinon.stub();
-
- app = (
-
- );
- wrapper = mount(app);
});
afterEach(function() {
atomEnv.destroy();
});
- const setTextIn = function(selector, text) {
- wrapper.find(selector).getDOMNode().getModel().setText(text);
- wrapper.update();
- };
+ function buildApp(overrides = {}) {
+ return (
+
+ );
+ }
- it('defaults to your project home path', function() {
- const text = wrapper.find('atom-text-editor').getDOMNode().getModel().getText();
- assert.equal(text, path.join('home', 'me', 'codes'));
+ it('defaults the destination directory to the dirPath parameter', function() {
+ const wrapper = shallow(buildApp({
+ request: dialogRequests.init({dirPath: path.join('/home/me/src')}),
+ }));
+ assert.strictEqual(wrapper.find('AtomTextEditor').prop('buffer').getText(), path.join('/home/me/src'));
});
- it('disables the initialize button with no project path', function() {
- setTextIn('.github-ProjectPath atom-text-editor', '');
+ it('disables the initialize button when the project path is empty', function() {
+ const wrapper = shallow(buildApp({}));
- assert.isTrue(wrapper.find('button.icon-repo-create').prop('disabled'));
+ assert.isTrue(wrapper.find('DialogView').prop('acceptEnabled'));
+ wrapper.find('AtomTextEditor').prop('buffer').setText('');
+ assert.isFalse(wrapper.find('DialogView').prop('acceptEnabled'));
+ wrapper.find('AtomTextEditor').prop('buffer').setText('/some/path');
+ assert.isTrue(wrapper.find('DialogView').prop('acceptEnabled'));
});
- it('enables the initialize button when the project path is populated', function() {
- setTextIn('.github-ProjectPath atom-text-editor', path.join('somewhere', 'else'));
+ it('calls the request accept method with the chosen path', function() {
+ const accept = sinon.spy();
+ const request = dialogRequests.init({dirPath: __dirname});
+ request.onAccept(accept);
- assert.isFalse(wrapper.find('button.icon-repo-create').prop('disabled'));
- });
+ const wrapper = shallow(buildApp({request}));
+ wrapper.find('AtomTextEditor').prop('buffer').setText('/some/path');
+ wrapper.find('DialogView').prop('accept')();
- it('calls the acceptance callback', function() {
- setTextIn('.github-ProjectPath atom-text-editor', '/somewhere/directory/');
+ assert.isTrue(accept.calledWith('/some/path'));
+ });
- wrapper.find('button.icon-repo-create').simulate('click');
+ it('calls the request cancel callback', function() {
+ const cancel = sinon.spy();
+ const request = dialogRequests.init({dirPath: __dirname});
+ request.onCancel(cancel);
- assert.isTrue(didAccept.calledWith('/somewhere/directory/'));
- });
+ const wrapper = shallow(buildApp({request}));
- it('calls the cancellation callback', function() {
- wrapper.find('button.github-CancelButton').simulate('click');
- assert.isTrue(didCancel.called);
+ wrapper.find('DialogView').prop('cancel')();
+ assert.isTrue(cancel.called);
});
});
diff --git a/test/views/open-commit-dialog.test.js b/test/views/open-commit-dialog.test.js
index c37f81ccd8..7a0b0d3cd2 100644
--- a/test/views/open-commit-dialog.test.js
+++ b/test/views/open-commit-dialog.test.js
@@ -1,97 +1,133 @@
import React from 'react';
-import {mount} from 'enzyme';
+import {shallow} from 'enzyme';
-import OpenCommitDialog from '../../lib/views/open-commit-dialog';
+import OpenCommitDialog, {openCommitDetailItem} from '../../lib/views/open-commit-dialog';
+import {dialogRequests} from '../../lib/controllers/dialogs-controller';
+import CommitDetailItem from '../../lib/items/commit-detail-item';
+import {GitError} from '../../lib/git-shell-out-strategy';
+import * as reporterProxy from '../../lib/reporter-proxy';
describe('OpenCommitDialog', function() {
- let atomEnv, commandRegistry;
- let app, wrapper, didAccept, didCancel, isValidEntry;
+ let atomEnv;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
- commandRegistry = atomEnv.commands;
-
- didAccept = sinon.stub();
- didCancel = sinon.stub();
- isValidEntry = sinon.stub().returns(true);
-
- app = (
-
- );
- wrapper = mount(app);
});
afterEach(function() {
atomEnv.destroy();
});
- const setTextIn = function(selector, text) {
- wrapper.find(selector).getDOMNode().getModel().setText(text);
- };
+ function isValidRef(ref) {
+ return Promise.resolve(ref === 'abcd1234');
+ }
- describe('entering a commit sha', function() {
- it("updates the commit ref automatically if it hasn't been modified", function() {
- setTextIn('.github-CommitRef atom-text-editor', 'asdf1234');
+ function buildApp(overrides = {}) {
+ const request = dialogRequests.commit();
- assert.equal(wrapper.instance().getCommitRef(), 'asdf1234');
+ return (
+
+ );
+ }
+
+ describe('open button enablement', function() {
+ it('disables the open button with no commit ref', function() {
+ const wrapper = shallow(buildApp());
+
+ assert.isFalse(wrapper.find('DialogView').prop('acceptEnabled'));
});
- it('does update the ref if it was modified automatically', function() {
- setTextIn('.github-CommitRef atom-text-editor', 'asdf1234');
- assert.equal(wrapper.instance().getCommitRef(), 'asdf1234');
+ it('enables the open button when commit sha box is populated', function() {
+ const wrapper = shallow(buildApp());
+ wrapper.find('AtomTextEditor').prop('buffer').setText('abcd1234');
- setTextIn('.github-CommitRef atom-text-editor', 'zxcv5678');
- assert.equal(wrapper.instance().getCommitRef(), 'zxcv5678');
+ assert.isTrue(wrapper.find('DialogView').prop('acceptEnabled'));
});
});
- describe('open button enablement and error state', function() {
- it('disables the open button with no commit ref', function() {
- setTextIn('.github-CommitRef atom-text-editor', '');
- wrapper.update();
+ it('calls the acceptance callback with the entered ref', function() {
+ const accept = sinon.spy();
+ const request = dialogRequests.commit();
+ request.onAccept(accept);
- assert.isTrue(wrapper.find('button.icon-commit').prop('disabled'));
- assert.isFalse(wrapper.find('.error').exists());
- });
+ const wrapper = shallow(buildApp({request}));
+ wrapper.find('AtomTextEditor').prop('buffer').setText('abcd1234');
+ wrapper.find('DialogView').prop('accept')();
- it('disables the open button when the commit does not exist in repo', async function() {
- isValidEntry.returns(false);
- const ref = 'abcd1234';
- setTextIn('.github-CommitRef atom-text-editor', ref);
- wrapper.find('button.icon-commit').simulate('click');
+ assert.isTrue(accept.calledWith('abcd1234'));
+ wrapper.unmount();
+ });
- await assert.async.strictEqual(wrapper.update().find('.error').text(), `There is no commit associated with "${ref}" in this repository`);
- assert.isTrue(wrapper.find('button.icon-commit').prop('disabled'));
- });
+ it('calls the cancellation callback', function() {
+ const cancel = sinon.spy();
+ const request = dialogRequests.commit();
+ request.onCancel(cancel);
- it('enables the open button when commit sha box is populated with a valid sha', function() {
- setTextIn('.github-CommitRef atom-text-editor', 'abcd1234');
- wrapper.update();
+ const wrapper = shallow(buildApp({request}));
- assert.isFalse(wrapper.find('button.icon-commit').prop('disabled'));
- assert.isFalse(wrapper.find('.error').exists());
- });
+ wrapper.find('DialogView').prop('cancel')();
+ assert.isTrue(cancel.called);
});
- it('calls the acceptance callback after validation', async function() {
- isValidEntry.returns(true);
- const ref = 'abcd1234';
- setTextIn('.github-CommitRef atom-text-editor', ref);
+ describe('openCommitDetailItem()', function() {
+ let repository;
+
+ beforeEach(function() {
+ sinon.stub(atomEnv.workspace, 'open').resolves('item');
+ sinon.stub(reporterProxy, 'addEvent');
+
+ repository = {
+ getWorkingDirectoryPath() {
+ return __dirname;
+ },
+ getCommit(ref) {
+ if (ref === 'abcd1234') {
+ return Promise.resolve('ok');
+ }
+
+ if (ref === 'bad') {
+ const e = new GitError('bad ref');
+ e.code = 128;
+ return Promise.reject(e);
+ }
+
+ return Promise.reject(new GitError('other error'));
+ },
+ };
+ });
- wrapper.find('button.icon-commit').simulate('click');
+ it('opens a CommitDetailItem with the chosen valid ref and records an event', async function() {
+ assert.strictEqual(await openCommitDetailItem('abcd1234', {workspace: atomEnv.workspace, repository}), 'item');
+ assert.isTrue(atomEnv.workspace.open.calledWith(
+ CommitDetailItem.buildURI(__dirname, 'abcd1234'),
+ {searchAllPanes: true},
+ ));
+ assert.isTrue(reporterProxy.addEvent.calledWith(
+ 'open-commit-in-pane',
+ {package: 'github', from: OpenCommitDialog.name},
+ ));
+ });
- await assert.async.isTrue(didAccept.calledWith({ref}));
- wrapper.unmount();
- });
+ it('raises a friendly error if the ref is invalid', async function() {
+ const e = await openCommitDetailItem('bad', {workspace: atomEnv.workspace, repository}).then(
+ () => { throw new Error('unexpected success'); },
+ error => error,
+ );
+ assert.strictEqual(e.userMessage, 'There is no commit associated with that reference.');
+ });
- it('calls the cancellation callback', function() {
- wrapper.find('button.github-CancelButton').simulate('click');
- assert.isTrue(didCancel.called);
- wrapper.unmount();
+ it('passes other errors through directly', async function() {
+ const e = await openCommitDetailItem('nope', {workspace: atomEnv.workspace, repository}).then(
+ () => { throw new Error('unexpected success'); },
+ error => error,
+ );
+ assert.isUndefined(e.userMessage);
+ assert.strictEqual(e.message, 'other error');
+ });
});
});
diff --git a/test/views/open-issueish-dialog.test.js b/test/views/open-issueish-dialog.test.js
index b4caf29fe5..c338475f9f 100644
--- a/test/views/open-issueish-dialog.test.js
+++ b/test/views/open-issueish-dialog.test.js
@@ -1,95 +1,109 @@
import React from 'react';
-import {mount} from 'enzyme';
+import {shallow} from 'enzyme';
-import OpenIssueishDialog from '../../lib/views/open-issueish-dialog';
+import OpenIssueishDialog, {openIssueishItem} from '../../lib/views/open-issueish-dialog';
+import IssueishDetailItem from '../../lib/items/issueish-detail-item';
+import {dialogRequests} from '../../lib/controllers/dialogs-controller';
+import * as reporterProxy from '../../lib/reporter-proxy';
describe('OpenIssueishDialog', function() {
- let atomEnv, commandRegistry;
- let app, wrapper, didAccept, didCancel;
+ let atomEnv;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
- commandRegistry = atomEnv.commands;
-
- didAccept = sinon.stub();
- didCancel = sinon.stub();
-
- app = (
-
- );
- wrapper = mount(app);
+ sinon.stub(reporterProxy, 'addEvent').returns();
});
afterEach(function() {
atomEnv.destroy();
});
- const setTextIn = function(selector, text) {
- wrapper.find(selector).getDOMNode().getModel().setText(text);
- };
-
- describe('entering a issueish url', function() {
- it("updates the issue url automatically if it hasn't been modified", function() {
- setTextIn('.github-IssueishUrl atom-text-editor', 'https://github.com/atom/github/pull/1807');
+ function buildApp(overrides = {}) {
+ const request = dialogRequests.issueish();
- assert.equal(wrapper.instance().getIssueishUrl(), 'https://github.com/atom/github/pull/1807');
- });
-
- it('does update the issue url if it was modified automatically', function() {
- setTextIn('.github-IssueishUrl atom-text-editor', 'https://github.com/atom/github/pull/1807');
- assert.equal(wrapper.instance().getIssueishUrl(), 'https://github.com/atom/github/pull/1807');
-
- setTextIn('.github-IssueishUrl atom-text-editor', 'https://github.com/atom/github/issues/1655');
- assert.equal(wrapper.instance().getIssueishUrl(), 'https://github.com/atom/github/issues/1655');
- });
- });
+ return (
+
+ );
+ }
describe('open button enablement', function() {
it('disables the open button with no issue url', function() {
- setTextIn('.github-IssueishUrl atom-text-editor', '');
- wrapper.update();
+ const wrapper = shallow(buildApp());
- assert.isTrue(wrapper.find('button.icon-git-pull-request').prop('disabled'));
+ wrapper.find('.github-OpenIssueish-url').prop('buffer').setText('');
+ assert.isFalse(wrapper.find('DialogView').prop('acceptEnabled'));
});
it('enables the open button when issue url box is populated', function() {
- setTextIn('.github-IssueishUrl atom-text-editor', 'https://github.com/atom/github/pull/1807');
- wrapper.update();
+ const wrapper = shallow(buildApp());
+ wrapper.find('.github-OpenIssueish-url').prop('buffer').setText('https://github.com/atom/github/pull/1807');
- assert.isFalse(wrapper.find('button.icon-git-pull-request').prop('disabled'));
+ assert.isTrue(wrapper.find('DialogView').prop('acceptEnabled'));
});
});
- describe('parseUrl', function() {
- it('returns an object with repo owner, repo name, and issueish number', function() {
- setTextIn('.github-IssueishUrl atom-text-editor', 'https://github.com/atom/github/pull/1807');
+ it('calls the acceptance callback with the entered URL', function() {
+ const accept = sinon.spy();
+ const request = dialogRequests.issueish();
+ request.onAccept(accept);
+ const wrapper = shallow(buildApp({request}));
+ wrapper.find('.github-OpenIssueish-url').prop('buffer').setText('https://github.com/atom/github/pull/1807');
+ wrapper.find('DialogView').prop('accept')();
- assert.deepEqual(wrapper.instance().parseUrl(), {
- repoOwner: 'atom',
- repoName: 'github',
- issueishNumber: '1807',
- });
- });
+ assert.isTrue(accept.calledWith('https://github.com/atom/github/pull/1807'));
});
- it('calls the acceptance callback', function() {
- setTextIn('.github-IssueishUrl atom-text-editor', 'https://github.com/atom/github/pull/1807');
-
- wrapper.find('button.icon-git-pull-request').simulate('click');
+ it('calls the cancellation callback', function() {
+ const cancel = sinon.spy();
+ const request = dialogRequests.issueish();
+ request.onCancel(cancel);
+ const wrapper = shallow(buildApp({request}));
+ wrapper.find('DialogView').prop('cancel')();
- assert.isTrue(didAccept.calledWith({
- repoOwner: 'atom',
- repoName: 'github',
- issueishNumber: '1807',
- }));
+ assert.isTrue(cancel.called);
});
- it('calls the cancellation callback', function() {
- wrapper.find('button.github-CancelButton').simulate('click');
- assert.isTrue(didCancel.called);
+ describe('openIssueishItem', function() {
+ it('opens an item for a valid issue URL', async function() {
+ sinon.stub(atomEnv.workspace, 'open').resolves('item');
+ assert.strictEqual(
+ await openIssueishItem('https://github.com/atom/github/issues/2203', {
+ workspace: atomEnv.workspace, workdir: __dirname,
+ }),
+ 'item',
+ );
+ assert.isTrue(atomEnv.workspace.open.calledWith(
+ IssueishDetailItem.buildURI({
+ host: 'github.com', owner: 'atom', repo: 'github', number: 2203, workdir: __dirname,
+ }),
+ ));
+ });
+
+ it('opens an item for a valid PR URL', async function() {
+ sinon.stub(atomEnv.workspace, 'open').resolves('item');
+ assert.strictEqual(
+ await openIssueishItem('https://github.com/smashwilson/az-coordinator/pull/10', {
+ workspace: atomEnv.workspace, workdir: __dirname,
+ }),
+ 'item',
+ );
+ assert.isTrue(atomEnv.workspace.open.calledWith(
+ IssueishDetailItem.buildURI({
+ host: 'github.com', owner: 'smashwilson', repo: 'az-coordinator', number: 10, workdir: __dirname,
+ }),
+ ));
+ });
+
+ it('rejects with an error for an invalid URL', async function() {
+ await assert.isRejected(
+ openIssueishItem('https://azurefire.net/not-an-issue', {workspace: atomEnv.workspace, workdir: __dirname}),
+ 'Not a valid issue or pull request URL',
+ );
+ });
});
});
diff --git a/test/views/recent-commits-view.test.js b/test/views/recent-commits-view.test.js
index 9f494283c7..b0eda50bba 100644
--- a/test/views/recent-commits-view.test.js
+++ b/test/views/recent-commits-view.test.js
@@ -16,7 +16,7 @@ describe('RecentCommitsView', function() {
commits={[]}
isLoading={false}
selectedCommitSha=""
- commandRegistry={atomEnv.commands}
+ commands={atomEnv.commands}
undoLastCommit={() => { }}
openCommit={() => { }}
selectNextCommit={() => { }}
diff --git a/test/views/staging-view.test.js b/test/views/staging-view.test.js
index 996a4c6f6a..31f72f37bf 100644
--- a/test/views/staging-view.test.js
+++ b/test/views/staging-view.test.js
@@ -11,12 +11,12 @@ import {assertEqualSets} from '../helpers';
describe('StagingView', function() {
const workingDirectoryPath = '/not/real/';
- let atomEnv, commandRegistry, workspace, notificationManager;
+ let atomEnv, commands, workspace, notificationManager;
let app;
beforeEach(function() {
atomEnv = global.buildAtomEnvironment();
- commandRegistry = atomEnv.commands;
+ commands = atomEnv.commands;
workspace = atomEnv.workspace;
notificationManager = atomEnv.notifications;
@@ -31,7 +31,7 @@ describe('StagingView', function() {
stagedChanges={[]}
workingDirectoryPath={workingDirectoryPath}
hasUndoHistory={false}
- commandRegistry={commandRegistry}
+ commands={commands}
notificationManager={notificationManager}
workspace={workspace}
openFiles={noop}
@@ -98,7 +98,7 @@ describe('StagingView', function() {
.simulate('mousedown', {button: 0});
await wrapper.instance().mouseup();
- commandRegistry.dispatch(wrapper.getDOMNode(), 'core:confirm');
+ commands.dispatch(wrapper.getDOMNode(), 'core:confirm');
await assert.async.isTrue(attemptFileStageOperation.calledWith(['b.txt'], 'unstaged'));
});
@@ -113,7 +113,7 @@ describe('StagingView', function() {
.simulate('mousedown', {button: 0});
await wrapper.instance().mouseup();
- commandRegistry.dispatch(wrapper.getDOMNode(), 'core:confirm');
+ commands.dispatch(wrapper.getDOMNode(), 'core:confirm');
await assert.async.isTrue(attemptFileStageOperation.calledWith(['b.txt'], 'staged'));
});
@@ -683,7 +683,7 @@ describe('StagingView', function() {
it('invokes a callback only when a single file is selected', async function() {
await wrapper.instance().selectFirst();
- commandRegistry.dispatch(wrapper.getDOMNode(), 'core:move-left');
+ commands.dispatch(wrapper.getDOMNode(), 'core:move-left');
assert.isTrue(showFilePatchItem.calledWith('unstaged-1.txt'), 'Callback invoked with unstaged-1.txt');
@@ -693,7 +693,7 @@ describe('StagingView', function() {
const selectedFilePaths = wrapper.instance().getSelectedItems().map(item => item.filePath).sort();
assert.deepEqual(selectedFilePaths, ['unstaged-1.txt', 'unstaged-2.txt']);
- commandRegistry.dispatch(wrapper.getDOMNode(), 'core:move-left');
+ commands.dispatch(wrapper.getDOMNode(), 'core:move-left');
assert.equal(showFilePatchItem.callCount, 0);
});
@@ -702,7 +702,7 @@ describe('StagingView', function() {
await wrapper.instance().activateNextList();
await wrapper.instance().selectFirst();
- commandRegistry.dispatch(wrapper.getDOMNode(), 'core:move-left');
+ commands.dispatch(wrapper.getDOMNode(), 'core:move-left');
assert.isTrue(showMergeConflictFileForPath.calledWith('conflict-1.txt'), 'Callback invoked with conflict-1.txt');
@@ -711,7 +711,7 @@ describe('StagingView', function() {
const selectedFilePaths = wrapper.instance().getSelectedItems().map(item => item.filePath).sort();
assert.deepEqual(selectedFilePaths, ['conflict-1.txt', 'conflict-2.txt']);
- commandRegistry.dispatch(wrapper.getDOMNode(), 'core:move-left');
+ commands.dispatch(wrapper.getDOMNode(), 'core:move-left');
assert.equal(showMergeConflictFileForPath.callCount, 0);
});