diff --git a/lib/containers/comment-decorations-container.js b/lib/containers/comment-decorations-container.js index 81fd521b90..6cab8d4ae0 100644 --- a/lib/containers/comment-decorations-container.js +++ b/lib/containers/comment-decorations-container.js @@ -146,6 +146,7 @@ export default class CommentDecorationsContainer extends React.Component { if ( !props || !props.repository || !props.repository.ref || + !props.repository.ref.associatedPullRequests || props.repository.ref.associatedPullRequests.totalCount === 0 ) { // no loading spinner for you diff --git a/lib/containers/current-pull-request-container.js b/lib/containers/current-pull-request-container.js index 705f7ca965..13541bca76 100644 --- a/lib/containers/current-pull-request-container.js +++ b/lib/containers/current-pull-request-container.js @@ -4,9 +4,7 @@ import {QueryRenderer, graphql} from 'react-relay'; import {Disposable} from 'event-kit'; import {autobind, CHECK_SUITE_PAGE_SIZE, CHECK_RUN_PAGE_SIZE} from '../helpers'; -import { - RemotePropType, RemoteSetPropType, BranchSetPropType, OperationStateObserverPropType, EndpointPropType, -} from '../prop-types'; +import {RemotePropType, RemoteSetPropType, BranchSetPropType, EndpointPropType} from '../prop-types'; import IssueishListController, {BareIssueishListController} from '../controllers/issueish-list-controller'; import CreatePullRequestTile from '../views/create-pull-request-tile'; import RelayNetworkLayerManager from '../relay-network-layer-manager'; @@ -30,7 +28,6 @@ export default class CurrentPullRequestContainer extends React.Component { limit: PropTypes.number, // Repository model attributes - remoteOperationObserver: OperationStateObserverPropType.isRequired, remote: RemotePropType.isRequired, remotes: RemoteSetPropType.isRequired, branches: BranchSetPropType.isRequired, @@ -120,18 +117,10 @@ export default class CurrentPullRequestContainer extends React.Component { } renderEmptyResult() { - this.sub.dispose(); - this.sub = this.props.remoteOperationObserver.onDidComplete(() => this.forceUpdate()); - return ; } - renderQueryResult({error, props, retry}) { - if (retry) { - this.sub.dispose(); - this.sub = this.props.remoteOperationObserver.onDidComplete(retry); - } - + renderQueryResult({error, props}) { if (error) { return ( state.refresher.trigger()); + return { lastRepository: props.repository, - remoteOperationObserver: new OperationStateObserver(props.repository, PUSH, PULL, FETCH), + remoteOperationObserver, + observerSub, }; } return null; } + componentWillUnmount() { + this.state.observerSub.dispose(); + this.state.remoteOperationObserver.dispose(); + this.state.refresher.dispose(); + } + fetchRepositoryData = repository => { return yubikiri({ workingDirectory: repository.getWorkingDirectoryPath(), @@ -58,7 +90,7 @@ export default class GitHubTabContainer extends React.Component { return ( ); diff --git a/lib/containers/issueish-search-container.js b/lib/containers/issueish-search-container.js index 024eeb51d0..b8f45b5287 100644 --- a/lib/containers/issueish-search-container.js +++ b/lib/containers/issueish-search-container.js @@ -4,7 +4,7 @@ import {QueryRenderer, graphql} from 'react-relay'; import {Disposable} from 'event-kit'; import {autobind, CHECK_SUITE_PAGE_SIZE, CHECK_RUN_PAGE_SIZE} from '../helpers'; -import {SearchPropType, OperationStateObserverPropType, EndpointPropType} from '../prop-types'; +import {SearchPropType, EndpointPropType} from '../prop-types'; import IssueishListController, {BareIssueishListController} from '../controllers/issueish-list-controller'; import RelayNetworkLayerManager from '../relay-network-layer-manager'; @@ -17,7 +17,6 @@ export default class IssueishSearchContainer extends React.Component { // Search model limit: PropTypes.number, search: SearchPropType.isRequired, - remoteOperationObserver: OperationStateObserverPropType.isRequired, // Action methods onOpenIssueish: PropTypes.func.isRequired, @@ -89,12 +88,7 @@ export default class IssueishSearchContainer extends React.Component { ); } - renderQueryResult({error, props, retry}) { - if (retry) { - this.sub.dispose(); - this.sub = this.props.remoteOperationObserver.onDidComplete(retry); - } - + renderQueryResult({error, props}) { if (error) { return ( ); } @@ -77,4 +86,6 @@ export default class GitHubTabController extends React.Component { e.preventDefault(); return this.props.repository.setConfig('atomGithub.currentRemote', remote.getName()); } + + openBoundPublishDialog = () => this.props.openPublishDialog(this.props.repository); } diff --git a/lib/controllers/issueish-searches-controller.js b/lib/controllers/issueish-searches-controller.js index b991fb0a7e..1a9281db88 100644 --- a/lib/controllers/issueish-searches-controller.js +++ b/lib/controllers/issueish-searches-controller.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {shell} from 'electron'; -import { - RemotePropType, RemoteSetPropType, BranchSetPropType, OperationStateObserverPropType, EndpointPropType, -} from '../prop-types'; +import {RemotePropType, RemoteSetPropType, BranchSetPropType, EndpointPropType} from '../prop-types'; import Search from '../models/search'; import IssueishSearchContainer from '../containers/issueish-search-container'; import CurrentPullRequestContainer from '../containers/current-pull-request-container'; @@ -31,7 +29,6 @@ export default class IssueishSearchesController extends React.Component { workspace: PropTypes.object.isRequired, // Repository model attributes - remoteOperationObserver: OperationStateObserverPropType.isRequired, workingDirectory: PropTypes.string, remote: RemotePropType.isRequired, remotes: RemoteSetPropType.isRequired, @@ -60,7 +57,6 @@ export default class IssueishSearchesController extends React.Component { repository={this.props.repository} token={this.props.token} endpoint={this.props.endpoint} - remoteOperationObserver={this.props.remoteOperationObserver} remote={this.props.remote} remotes={this.props.remotes} branches={this.props.branches} @@ -79,7 +75,6 @@ export default class IssueishSearchesController extends React.Component { token={this.props.token} endpoint={this.props.endpoint} search={search} - remoteOperationObserver={this.props.remoteOperationObserver} onOpenIssueish={this.onOpenIssueish} onOpenSearch={this.onOpenSearch} diff --git a/lib/controllers/remote-controller.js b/lib/controllers/remote-controller.js index 75097cd3d2..c7121b9f20 100644 --- a/lib/controllers/remote-controller.js +++ b/lib/controllers/remote-controller.js @@ -4,9 +4,7 @@ import {shell} from 'electron'; import {autobind} from '../helpers'; import {incrementCounter} from '../reporter-proxy'; -import { - RemotePropType, RemoteSetPropType, BranchSetPropType, OperationStateObserverPropType, EndpointPropType, -} from '../prop-types'; +import {RemotePropType, RemoteSetPropType, BranchSetPropType, EndpointPropType} from '../prop-types'; import IssueishSearchesController from './issueish-searches-controller'; export default class RemoteController extends React.Component { @@ -25,7 +23,6 @@ export default class RemoteController extends React.Component { token: PropTypes.string.isRequired, // Repository derived attributes - remoteOperationObserver: OperationStateObserverPropType.isRequired, workingDirectory: PropTypes.string, workspace: PropTypes.object.isRequired, remote: RemotePropType.isRequired, @@ -49,7 +46,6 @@ export default class RemoteController extends React.Component { endpoint={this.props.endpoint} token={this.props.token} - remoteOperationObserver={this.props.remoteOperationObserver} workingDirectory={this.props.workingDirectory} repository={this.props.repository} diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js index bb512d626e..708fdc3561 100644 --- a/lib/controllers/root-controller.js +++ b/lib/controllers/root-controller.js @@ -312,6 +312,10 @@ export default class RootController extends React.Component { getCurrentWorkDirs={getCurrentWorkDirs} onDidChangeWorkDirs={onDidChangeWorkDirs} changeWorkingDirectory={this.props.changeWorkingDirectory} + openCreateDialog={this.openCreateDialog} + openPublishDialog={this.openPublishDialog} + openCloneDialog={this.openCloneDialog} + openGitTab={this.gitTabTracker.toggleFocus} /> )} diff --git a/lib/github-package.js b/lib/github-package.js index 3d187a43a3..f2fb23f72d 100644 --- a/lib/github-package.js +++ b/lib/github-package.js @@ -466,10 +466,10 @@ export default class GithubPackage { } this.workdirCache.invalidate(); - this.project.addPath(projectPath); - await this.scheduleActiveContextUpdate(); + + reporterProxy.addEvent('clone-repository', {project: 'github'}); } getRepositoryForWorkdir(projectPath) { diff --git a/lib/items/github-tab-item.js b/lib/items/github-tab-item.js index dc55141ef6..5a4452dc7c 100644 --- a/lib/items/github-tab-item.js +++ b/lib/items/github-tab-item.js @@ -12,6 +12,14 @@ export default class GitHubTabItem extends React.Component { loginModel: GithubLoginModelPropType.isRequired, documentActiveElement: PropTypes.func, + + changeWorkingDirectory: PropTypes.func.isRequired, + onDidChangeWorkDirs: PropTypes.func.isRequired, + getCurrentWorkDirs: PropTypes.func.isRequired, + openCreateDialog: PropTypes.func.isRequired, + openPublishDialog: PropTypes.func.isRequired, + openCloneDialog: PropTypes.func.isRequired, + openGitTab: PropTypes.func.isRequired, } static defaultProps = { diff --git a/lib/models/refresher.js b/lib/models/refresher.js new file mode 100644 index 0000000000..864cbd798a --- /dev/null +++ b/lib/models/refresher.js @@ -0,0 +1,26 @@ +/** + * Uniformly trigger a refetch of all GraphQL query containers within a scoped hierarchy. + */ +export default class Refresher { + constructor() { + this.dispose(); + } + + setRetryCallback(key, retryCallback) { + this.retryByKey.set(key, retryCallback); + } + + trigger() { + for (const [, retryCallback] of this.retryByKey) { + retryCallback(); + } + } + + deregister(key) { + this.retryByKey.delete(key); + } + + dispose() { + this.retryByKey = new Map(); + } +} diff --git a/lib/prop-types.js b/lib/prop-types.js index 78fd797320..aebfe77dd9 100644 --- a/lib/prop-types.js +++ b/lib/prop-types.js @@ -120,6 +120,12 @@ export const OperationStateObserverPropType = PropTypes.shape({ dispose: PropTypes.func.isRequired, }); +export const RefresherPropType = PropTypes.shape({ + setRetryCallback: PropTypes.func.isRequired, + trigger: PropTypes.func.isRequired, + deregister: PropTypes.func.isRequired, +}); + export const IssueishPropType = PropTypes.shape({ getNumber: PropTypes.func.isRequired, getTitle: PropTypes.func.isRequired, diff --git a/lib/relay-network-layer-manager.js b/lib/relay-network-layer-manager.js index f97ddd859b..0d359936b1 100644 --- a/lib/relay-network-layer-manager.js +++ b/lib/relay-network-layer-manager.js @@ -46,6 +46,10 @@ export function expectRelayQuery(operationPattern, response) { export function clearRelayExpectations() { responsesByQuery.clear(); + relayEnvironmentPerURL.clear(); + tokenPerURL.clear(); + fetchPerURL.clear(); + responsesByQuery.clear(); } function createFetchQuery(url) { diff --git a/lib/views/create-dialog.js b/lib/views/create-dialog.js index 1b474bfc32..59c005710b 100644 --- a/lib/views/create-dialog.js +++ b/lib/views/create-dialog.js @@ -5,6 +5,7 @@ import fs from 'fs-extra'; import CreateDialogContainer from '../containers/create-dialog-container'; import createRepositoryMutation from '../mutations/create-repository'; import {GithubLoginModelPropType} from '../prop-types'; +import {addEvent} from '../reporter-proxy'; export default class CreateDialog extends React.Component { static propTypes = { @@ -34,21 +35,29 @@ export async function createRepository( const result = await createRepositoryMutation(relayEnvironment, {name, ownerID, visibility}); const sourceURL = result.createRepository.repository[protocol === 'ssh' ? 'sshUrl' : 'url']; await clone(sourceURL, localPath, sourceRemoteName); + addEvent('create-github-repository', {package: 'github'}); } export async function publishRepository( {ownerID, name, visibility, protocol, sourceRemoteName}, {repository, relayEnvironment}, ) { - let defaultBranchName; - const branchSet = await repository.getBranches(); - const branchNames = new Set(branchSet.getNames()); - if (branchNames.has('master')) { + let defaultBranchName, wasEmpty; + if (repository.isEmpty()) { + wasEmpty = true; + await repository.init(); defaultBranchName = 'master'; } else { - const head = branchSet.getHeadBranch(); - if (head.isPresent()) { - defaultBranchName = head.getName(); + wasEmpty = false; + const branchSet = await repository.getBranches(); + const branchNames = new Set(branchSet.getNames()); + if (branchNames.has('master')) { + defaultBranchName = 'master'; + } else { + const head = branchSet.getHeadBranch(); + if (head.isPresent()) { + defaultBranchName = head.getName(); + } } } if (!defaultBranchName) { @@ -58,5 +67,10 @@ export async function publishRepository( const result = await createRepositoryMutation(relayEnvironment, {name, ownerID, visibility}); const sourceURL = result.createRepository.repository[protocol === 'ssh' ? 'sshUrl' : 'url']; const remote = await repository.addRemote(sourceRemoteName, sourceURL); - await repository.push(defaultBranchName, {remote, setUpstream: true}); + if (wasEmpty) { + addEvent('publish-github-repository', {package: 'github'}); + } else { + await repository.push(defaultBranchName, {remote, setUpstream: true}); + addEvent('init-publish-github-repository', {package: 'github'}); + } } diff --git a/lib/views/github-blank-nolocal.js b/lib/views/github-blank-nolocal.js new file mode 100644 index 0000000000..e7dca4a2d3 --- /dev/null +++ b/lib/views/github-blank-nolocal.js @@ -0,0 +1,29 @@ +/* istanbul ignore file */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function GitHubBlankNoLocal(props) { + return ( +
+
+

Welcome

+

How would you like to get started today?

+

+ +

+

+ +

+
+ ); +} + +GitHubBlankNoLocal.propTypes = { + openCreateDialog: PropTypes.func.isRequired, + openCloneDialog: PropTypes.func.isRequired, +}; diff --git a/lib/views/github-blank-noremote.js b/lib/views/github-blank-noremote.js new file mode 100644 index 0000000000..74a4ea6e68 --- /dev/null +++ b/lib/views/github-blank-noremote.js @@ -0,0 +1,25 @@ +/* istanbul ignore file */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function GitHubBlankNoRemote(props) { + return ( +
+
+

This repository has no remotes on GitHub.

+

+ +

+

+ Create a new GitHub repository and configure this git repository configured to push there. +

+
+ ); +} + +GitHubBlankNoRemote.propTypes = { + openBoundPublishDialog: PropTypes.func.isRequired, +}; diff --git a/lib/views/github-blank-uninitialized.js b/lib/views/github-blank-uninitialized.js new file mode 100644 index 0000000000..cba9a5642a --- /dev/null +++ b/lib/views/github-blank-uninitialized.js @@ -0,0 +1,37 @@ +/* istanbul ignore file */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import Octicon from '../atom/octicon'; + +export default function GitHubBlankUninitialized(props) { + return ( +
+
+
+

This repository is not yet version controlled by git.

+

+ +

+

+ Create a new GitHub repository, then track the existing content within this directory as a git repository + configured to push there. +

+

+ To initialize this directory as a git repository without publishing it to GitHub, visit the + +

+
+
+ ); +} + +GitHubBlankUninitialized.propTypes = { + openBoundPublishDialog: PropTypes.func.isRequired, + openGitTab: PropTypes.func.isRequired, +}; diff --git a/lib/views/github-tab-view.js b/lib/views/github-tab-view.js index 67354b6d7a..d80539e534 100644 --- a/lib/views/github-tab-view.js +++ b/lib/views/github-tab-view.js @@ -3,21 +3,25 @@ import PropTypes from 'prop-types'; import { GithubLoginModelPropType, RefHolderPropType, RemoteSetPropType, RemotePropType, BranchSetPropType, BranchPropType, - OperationStateObserverPropType, + RefresherPropType, } from '../prop-types'; import LoadingView from './loading-view'; import RemoteSelectorView from './remote-selector-view'; import TabHeaderView from './tab-header-view'; +import GitHubBlankNoLocal from './github-blank-nolocal'; +import GitHubBlankUninitialized from './github-blank-uninitialized'; +import GitHubBlankNoRemote from './github-blank-noremote'; import RemoteContainer from '../containers/remote-container'; export default class GitHubTabView extends React.Component { static propTypes = { workspace: PropTypes.object.isRequired, - remoteOperationObserver: OperationStateObserverPropType.isRequired, + refresher: RefresherPropType.isRequired, loginModel: GithubLoginModelPropType.isRequired, rootHolder: RefHolderPropType.isRequired, workingDirectory: PropTypes.string, + repository: PropTypes.object.isRequired, branches: BranchSetPropType.isRequired, currentBranch: BranchPropType.isRequired, remotes: RemoteSetPropType.isRequired, @@ -32,6 +36,10 @@ export default class GitHubTabView extends React.Component { changeWorkingDirectory: PropTypes.func.isRequired, onDidChangeWorkDirs: PropTypes.func.isRequired, getCurrentWorkDirs: PropTypes.func.isRequired, + openCreateDialog: PropTypes.func.isRequired, + openBoundPublishDialog: PropTypes.func.isRequired, + openCloneDialog: PropTypes.func.isRequired, + openGitTab: PropTypes.func.isRequired, } render() { @@ -50,6 +58,24 @@ export default class GitHubTabView extends React.Component { return ; } + if (this.props.repository.isAbsent() || this.props.repository.isAbsentGuess()) { + return ( + + ); + } + + if (this.props.repository.isEmpty()) { + return ( + + ); + } + if (this.props.currentRemote.isPresent()) { // Single, chosen or unambiguous remote return ( @@ -57,7 +83,7 @@ export default class GitHubTabView extends React.Component { loginModel={this.props.loginModel} endpoint={this.props.currentRemote.getEndpoint()} - remoteOperationObserver={this.props.remoteOperationObserver} + refresher={this.props.refresher} pushInProgress={this.props.pushInProgress} workingDirectory={this.props.workingDirectory} workspace={this.props.workspace} @@ -82,16 +108,8 @@ export default class GitHubTabView extends React.Component { ); } - // No remotes available - // TODO: display a view that lets you create a repository on GitHub return ( -
-
-

No Remotes

-
- This repository does not have any remotes hosted at GitHub.com. -
-
+ ); } diff --git a/styles/github-blank.less b/styles/github-blank.less new file mode 100644 index 0000000000..ff68b0b339 --- /dev/null +++ b/styles/github-blank.less @@ -0,0 +1,70 @@ +@import "variables"; + +.github-Blank { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + + &-body { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; + } + + &-footer { + flex-shrink: 0; + padding: @component-padding*2 @component-padding; + } + + &-LargeIcon:before { + margin-right: 0; + margin-top: @component-padding * 5; + margin-bottom: @component-padding * 5; + width: auto; + height: auto; + font-size: 8em; + color: mix(@base-background-color, @text-color, 66%); // 2/3 of bg color + } + + &-banner, &-context , &-explanation , &-LargeIcon { + text-align: center; + } + + &-option { + display: flex; + flex-direction: row; + align-items: center; + + &--explained { + margin-bottom: @component-padding/2; + } + } + + &-actionBtn { + flex: 1; + margin: 0 10%; + } + + &-explanation { + margin-left: 5%; + margin-right: 5%; + color: @text-color-subtle; + } + + &-tabLink { + margin-left: 0.5em; + border-color: inherit; + background-color: inherit; + border-style: none; + border-width: 0; + padding: 0; + text-decoration: underline; + + .icon { + padding-right: 0; + } + } +} diff --git a/test/containers/current-pull-request-container.test.js b/test/containers/current-pull-request-container.test.js index 8370c82e44..4d529a31b9 100644 --- a/test/containers/current-pull-request-container.test.js +++ b/test/containers/current-pull-request-container.test.js @@ -3,7 +3,6 @@ import {shallow} from 'enzyme'; import {QueryRenderer} from 'react-relay'; import CurrentPullRequestContainer from '../../lib/containers/current-pull-request-container'; -import {ManualStateObserver} from '../helpers'; import {queryBuilder} from '../builder/graphql/query'; import Remote from '../../lib/models/remote'; import RemoteSet from '../../lib/models/remote-set'; @@ -15,12 +14,6 @@ import repositoryQuery from '../../lib/containers/__generated__/remoteContainerQ import currentQuery from '../../lib/containers/__generated__/currentPullRequestContainerQuery.graphql.js'; describe('CurrentPullRequestContainer', function() { - let observer; - - beforeEach(function() { - observer = new ManualStateObserver(); - }); - function buildApp(overrideProps = {}) { const origin = new Remote('origin', 'git@github.com:atom/github.git'); const upstreamBranch = Branch.createRemoteTracking('refs/remotes/origin/master', 'origin', 'refs/heads/master'); @@ -37,7 +30,6 @@ describe('CurrentPullRequestContainer', function() { token="1234" endpoint={origin.getEndpoint()} - remoteOperationObserver={observer} remote={origin} remotes={remotes} branches={branches} @@ -209,15 +201,4 @@ describe('CurrentPullRequestContainer', function() { assert.isTrue(filterFn({getHeadRepositoryID: () => '100'})); assert.isFalse(filterFn({getHeadRepositoryID: () => '12'})); }); - - it('performs the query again when a remote operation completes', function() { - const wrapper = shallow(buildApp()); - - const props = queryBuilder(currentQuery).build(); - const retry = sinon.spy(); - wrapper.find(QueryRenderer).renderProp('render')({error: null, props, retry}); - - observer.trigger(); - assert.isTrue(retry.called); - }); }); diff --git a/test/containers/github-tab-container.test.js b/test/containers/github-tab-container.test.js index f557eaee88..4ccd4c11b2 100644 --- a/test/containers/github-tab-container.test.js +++ b/test/containers/github-tab-container.test.js @@ -1,56 +1,124 @@ import React from 'react'; -import {mount} from 'enzyme'; +import {mount, shallow} from 'enzyme'; -import {cloneRepository} from '../helpers'; +import {buildRepository, cloneRepository} from '../helpers'; import GitHubTabContainer from '../../lib/containers/github-tab-container'; +import GitHubTabController from '../../lib/controllers/github-tab-controller'; import Repository from '../../lib/models/repository'; -import {gitHubTabContainerProps} from '../fixtures/props/github-tab-props'; +import {InMemoryStrategy} from '../../lib/shared/keytar-strategy'; +import GithubLoginModel from '../../lib/models/github-login-model'; +import RefHolder from '../../lib/models/ref-holder'; describe('GitHubTabContainer', function() { - let atomEnv; + let atomEnv, repository, defaultRepositoryData; - beforeEach(function() { + beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); + repository = await buildRepository(await cloneRepository()); + + defaultRepositoryData = { + workingDirectory: repository.getWorkingDirectoryPath(), + allRemotes: await repository.getRemotes(), + branches: await repository.getBranches(), + selectedRemoteName: 'origin', + aheadCount: 0, + pushInProgress: false, + }; }); afterEach(function() { atomEnv.destroy(); }); - function buildApp(overrideProps = {}) { - const repository = Repository.absent(); - return ; + function buildApp(props = {}) { + return ( + {}} + onDidChangeWorkDirs={() => {}} + getCurrentWorkDirs={() => []} + openCreateDialog={() => {}} + openPublishDialog={() => {}} + openCloneDialog={() => {}} + openGitTab={() => {}} + + {...props} + /> + ); } - describe('operation state observer', function() { - it('creates an observer on the current repository', function() { - const repository = Repository.absent(); - const wrapper = mount(buildApp({repository})); + describe('refresher', function() { + let wrapper, retry; + + function stubRepository(repo) { + sinon.stub(repo.getOperationStates(), 'isFetchInProgress').returns(false); + sinon.stub(repo.getOperationStates(), 'isPushInProgress').returns(false); + sinon.stub(repo.getOperationStates(), 'isPullInProgress').returns(false); + } + + function simulateOperation(repo, name, middle = () => {}) { + const accessor = `is${name[0].toUpperCase()}${name.slice(1)}InProgress`; + const methodStub = repo.getOperationStates()[accessor]; + methodStub.returns(true); + repo.state.didUpdate(); + middle(); + methodStub.returns(false); + repo.state.didUpdate(); + } + + beforeEach(function() { + wrapper = shallow(buildApp()); + const childWrapper = wrapper.find('ObserveModel').renderProp('children')(defaultRepositoryData); + + retry = sinon.spy(); + const refresher = childWrapper.find(GitHubTabController).prop('refresher'); + refresher.setRetryCallback(Symbol('key'), retry); - const observer = wrapper.state('remoteOperationObserver'); - assert.strictEqual(observer.repository, repository); + stubRepository(repository); + }); + + it('triggers a refresh when the current repository completes a fetch, push, or pull', function() { + assert.isFalse(retry.called); + + simulateOperation(repository, 'fetch', () => assert.isFalse(retry.called)); + assert.strictEqual(retry.callCount, 1); + + simulateOperation(repository, 'push', () => assert.strictEqual(retry.callCount, 1)); + assert.strictEqual(retry.callCount, 2); - wrapper.setProps({}); - assert.strictEqual(wrapper.state('remoteOperationObserver'), observer); + simulateOperation(repository, 'pull', () => assert.strictEqual(retry.callCount, 2)); + assert.strictEqual(retry.callCount, 3); }); - it('creates a new observer when the repository changes', function() { - const repository0 = Repository.absent(); - const wrapper = mount(buildApp({repository: repository0})); + it('un-observes an old repository and observes a new one', async function() { + const other = await buildRepository(await cloneRepository()); + stubRepository(other); + wrapper.setProps({repository: other}); + + simulateOperation(repository, 'fetch'); + assert.isFalse(retry.called); + + simulateOperation(other, 'fetch'); + assert.isTrue(retry.called); + }); - const observer0 = wrapper.state('remoteOperationObserver'); + it('un-observes the repository when unmounting', function() { + wrapper.unmount(); - const repository1 = Repository.absent(); - wrapper.setProps({repository: repository1}); - assert.notStrictEqual(wrapper.state('remoteOperationObserver'), observer0); + simulateOperation(repository, 'fetch'); + assert.isFalse(retry.called); }); }); describe('while loading', function() { it('passes isLoading to its view', async function() { - const repository = new Repository(await cloneRepository()); - assert.isTrue(repository.isLoading()); - const wrapper = mount(buildApp({repository})); + const loadingRepo = new Repository(await cloneRepository()); + assert.isTrue(loadingRepo.isLoading()); + const wrapper = mount(buildApp({repository: loadingRepo})); assert.isTrue(wrapper.find('GitHubTabController').prop('isLoading')); }); @@ -59,9 +127,9 @@ describe('GitHubTabContainer', function() { describe('once loaded', function() { it('renders the controller', async function() { const workdir = await cloneRepository(); - const repository = new Repository(workdir); - await repository.getLoadPromise(); - const wrapper = mount(buildApp({repository})); + const presentRepo = new Repository(workdir); + await presentRepo.getLoadPromise(); + const wrapper = mount(buildApp({repository: presentRepo})); await assert.async.isFalse(wrapper.update().find('GitHubTabController').prop('isLoading')); assert.strictEqual(wrapper.find('GitHubTabController').prop('workingDirectory'), workdir); diff --git a/test/containers/issueish-search-container.test.js b/test/containers/issueish-search-container.test.js index 5de719ba9d..c08d771643 100644 --- a/test/containers/issueish-search-container.test.js +++ b/test/containers/issueish-search-container.test.js @@ -88,18 +88,13 @@ describe('IssueishSearchContainer', function() { }); describe('when the query errors', function() { - // eslint-disable-next-line no-unused-vars - let stub; + // Consumes the failing Relay Query console error beforeEach(function() { - stub = sinon.stub(console, 'error'); - // eslint-disable-next-line no-console - console.error.withArgs( + sinon.stub(console, 'error').withArgs( 'Error encountered in subquery', sinon.match.defined.and(sinon.match.hasNested('errors[0].message', sinon.match('uh oh'))), - ).callsFake(() => {}); - // eslint-disable-next-line no-console - console.error.callThrough(); + ).callsFake(() => {}).callThrough(); }); it('passes an empty result list and an error prop to the controller', async function() { @@ -170,73 +165,4 @@ describe('IssueishSearchContainer', function() { assert.isTrue(controller.prop('results').some(node => node.number === 1)); assert.isTrue(controller.prop('results').some(node => node.number === 2)); }); - - it('performs the query again when a remote operation completes', async function() { - const {promise: promise0, resolve: resolve0, disable: disable0} = expectRelayQuery({ - name: 'issueishSearchContainerQuery', - variables: { - query: 'type:pr author:me', - first: 20, - checkSuiteCount: CHECK_SUITE_PAGE_SIZE, - checkSuiteCursor: null, - checkRunCount: CHECK_RUN_PAGE_SIZE, - checkRunCursor: null, - }, - }, op => { - return relayResponseBuilder(op) - .search(s => { - s.issueCount(1); - s.addNode(n => n.bePullRequest(pr => { - pr.number(1); - pr.commits(conn => conn.addNode()); - })); - }) - .build(); - }); - - const search = new Search('pull requests', 'type:pr author:me'); - const wrapper = mount(buildApp({search})); - resolve0(); - await promise0; - - assert.isTrue( - wrapper.update().find('BareIssueishListController').prop('results').some(node => node.number === 1), - ); - - disable0(); - const {promise: promise1, resolve: resolve1} = expectRelayQuery({ - name: 'issueishSearchContainerQuery', - variables: { - query: 'type:pr author:me', - first: 20, - checkSuiteCount: CHECK_SUITE_PAGE_SIZE, - checkSuiteCursor: null, - checkRunCount: CHECK_RUN_PAGE_SIZE, - checkRunCursor: null, - }, - }, op => { - return relayResponseBuilder(op) - .search(s => { - s.issueCount(1); - s.addNode(n => n.bePullRequest(pr => { - pr.number(2); - pr.commits(conn => conn.addNode()); - })); - }) - .build(); - }); - - resolve1(); - await promise1; - - assert.isTrue( - wrapper.update().find('BareIssueishListController').prop('results').some(node => node.number === 1), - ); - - observer.trigger(); - - await assert.async.isTrue( - wrapper.update().find('BareIssueishListController').prop('results').some(node => node.number === 2), - ); - }); }); diff --git a/test/containers/remote-container.test.js b/test/containers/remote-container.test.js index beda1e34f2..b6a1cb09eb 100644 --- a/test/containers/remote-container.test.js +++ b/test/containers/remote-container.test.js @@ -10,8 +10,8 @@ import RemoteSet from '../../lib/models/remote-set'; import Branch, {nullBranch} from '../../lib/models/branch'; import BranchSet from '../../lib/models/branch-set'; import GithubLoginModel from '../../lib/models/github-login-model'; -import {nullOperationStateObserver} from '../../lib/models/operation-state-observer'; import {getEndpoint} from '../../lib/models/endpoint'; +import Refresher from '../../lib/models/refresher'; import {InMemoryStrategy, INSUFFICIENT, UNAUTHENTICATED} from '../../lib/shared/keytar-strategy'; import remoteQuery from '../../lib/containers/__generated__/remoteContainerQuery.graphql'; @@ -39,7 +39,7 @@ describe('RemoteContainer', function() { loginModel={model} endpoint={getEndpoint('github.com')} - remoteOperationObserver={nullOperationStateObserver} + refresher={new Refresher()} workingDirectory={__dirname} notifications={atomEnv.notifications} workspace={atomEnv.workspace} diff --git a/test/controllers/github-tab-controller.test.js b/test/controllers/github-tab-controller.test.js index 5843176be0..ad5e3c2fd2 100644 --- a/test/controllers/github-tab-controller.test.js +++ b/test/controllers/github-tab-controller.test.js @@ -1,32 +1,60 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {gitHubTabControllerProps} from '../fixtures/props/github-tab-props'; import GitHubTabController from '../../lib/controllers/github-tab-controller'; import Repository from '../../lib/models/repository'; import BranchSet from '../../lib/models/branch-set'; import Branch, {nullBranch} from '../../lib/models/branch'; import RemoteSet from '../../lib/models/remote-set'; import Remote from '../../lib/models/remote'; +import {InMemoryStrategy} from '../../lib/shared/keytar-strategy'; +import GithubLoginModel from '../../lib/models/github-login-model'; +import RefHolder from '../../lib/models/ref-holder'; +import Refresher from '../../lib/models/refresher'; + +import {buildRepository, cloneRepository} from '../helpers'; describe('GitHubTabController', function() { - let atomEnv; + let atomEnv, repository; - beforeEach(function() { + beforeEach(async function() { atomEnv = global.buildAtomEnvironment(); + repository = await buildRepository(await cloneRepository()); }); afterEach(function() { atomEnv.destroy(); }); - function buildApp(overrideProps = {}) { - const props = { - repository: Repository.absent(), - ...overrideProps, - }; - - return ; + function buildApp(props = {}) { + const repo = props.repository || repository; + + return ( + {}} + onDidChangeWorkDirs={() => {}} + getCurrentWorkDirs={() => []} + openCreateDialog={() => {}} + openPublishDialog={() => {}} + openCloneDialog={() => {}} + openGitTab={() => {}} + + {...props} + /> + ); } describe('derived view props', function() { @@ -77,28 +105,37 @@ describe('GitHubTabController', function() { describe('actions', function() { it('pushes a branch', async function() { - const repository = Repository.absent(); - sinon.stub(repository, 'push').resolves(true); - const wrapper = shallow(buildApp({repository})); + const absent = Repository.absent(); + sinon.stub(absent, 'push').resolves(true); + const wrapper = shallow(buildApp({repository: absent})); const branch = new Branch('abc'); const remote = new Remote('def', 'git@github.com:def/ghi.git'); assert.isTrue(await wrapper.find('GitHubTabView').prop('handlePushBranch')(branch, remote)); - assert.isTrue(repository.push.calledWith('abc', {remote, setUpstream: true})); + assert.isTrue(absent.push.calledWith('abc', {remote, setUpstream: true})); }); it('chooses a remote', async function() { - const repository = Repository.absent(); - sinon.stub(repository, 'setConfig').resolves(true); - const wrapper = shallow(buildApp({repository})); + const absent = Repository.absent(); + sinon.stub(absent, 'setConfig').resolves(true); + const wrapper = shallow(buildApp({repository: absent})); const remote = new Remote('aaa', 'git@github.com:aaa/aaa.git'); const event = {preventDefault: sinon.spy()}; assert.isTrue(await wrapper.find('GitHubTabView').prop('handleRemoteSelect')(event, remote)); assert.isTrue(event.preventDefault.called); - assert.isTrue(repository.setConfig.calledWith('atomGithub.currentRemote', 'aaa')); + assert.isTrue(absent.setConfig.calledWith('atomGithub.currentRemote', 'aaa')); + }); + + it('opens the publish dialog on the active repository', async function() { + const someRepo = await buildRepository(await cloneRepository()); + const openPublishDialog = sinon.spy(); + const wrapper = shallow(buildApp({repository: someRepo, openPublishDialog})); + + wrapper.find('GitHubTabView').prop('openBoundPublishDialog')(); + assert.isTrue(openPublishDialog.calledWith(someRepo)); }); }); }); diff --git a/test/controllers/issueish-searches-controller.test.js b/test/controllers/issueish-searches-controller.test.js index aa93c0c708..5ea652e42c 100644 --- a/test/controllers/issueish-searches-controller.test.js +++ b/test/controllers/issueish-searches-controller.test.js @@ -9,7 +9,6 @@ import Branch from '../../lib/models/branch'; import BranchSet from '../../lib/models/branch-set'; import Issueish from '../../lib/models/issueish'; import {getEndpoint} from '../../lib/models/endpoint'; -import {nullOperationStateObserver} from '../../lib/models/operation-state-observer'; import * as reporterProxy from '../../lib/reporter-proxy'; import remoteContainerQuery from '../../lib/containers/__generated__/remoteContainerQuery.graphql'; @@ -38,7 +37,6 @@ describe('IssueishSearchesController', function() { endpoint={getEndpoint('github.com')} repository={queryBuilder(remoteContainerQuery).build().repository} - remoteOperationObserver={nullOperationStateObserver} workingDirectory={__dirname} workspace={atomEnv.workspace} remote={origin} diff --git a/test/controllers/remote-controller.test.js b/test/controllers/remote-controller.test.js index 87c188d368..36e34cccf1 100644 --- a/test/controllers/remote-controller.test.js +++ b/test/controllers/remote-controller.test.js @@ -5,18 +5,19 @@ import {shell} from 'electron'; import BranchSet from '../../lib/models/branch-set'; import Branch, {nullBranch} from '../../lib/models/branch'; import Remote from '../../lib/models/remote'; +import RemoteSet from '../../lib/models/remote-set'; import {getEndpoint} from '../../lib/models/endpoint'; -import {nullOperationStateObserver} from '../../lib/models/operation-state-observer'; import RemoteController from '../../lib/controllers/remote-controller'; import * as reporterProxy from '../../lib/reporter-proxy'; describe('RemoteController', function() { - let atomEnv, remote, branchSet, currentBranch; + let atomEnv, remote, remoteSet, currentBranch, branchSet; beforeEach(function() { atomEnv = global.buildAtomEnvironment(); remote = new Remote('origin', 'git@github.com:atom/github'); + remoteSet = new RemoteSet([remote]); currentBranch = new Branch('master', nullBranch, nullBranch, true); branchSet = new BranchSet(); branchSet.add(currentBranch); @@ -27,26 +28,22 @@ describe('RemoteController', function() { }); function createApp(props = {}) { - const noop = () => {}; - return ( {}} {...props} /> diff --git a/test/fixtures/props/github-tab-props.js b/test/fixtures/props/github-tab-props.js deleted file mode 100644 index 55c83fe75a..0000000000 --- a/test/fixtures/props/github-tab-props.js +++ /dev/null @@ -1,67 +0,0 @@ -import {InMemoryStrategy} from '../../../lib/shared/keytar-strategy'; -import GithubLoginModel from '../../../lib/models/github-login-model'; -import RefHolder from '../../../lib/models/ref-holder'; -import OperationStateObserver, {PUSH, PULL, FETCH} from '../../../lib/models/operation-state-observer'; -import RemoteSet from '../../../lib/models/remote-set'; -import {nullRemote} from '../../../lib/models/remote'; -import BranchSet from '../../../lib/models/branch-set'; -import {nullBranch} from '../../../lib/models/branch'; - -export function gitHubTabItemProps(atomEnv, repository, overrides = {}) { - return { - workspace: atomEnv.workspace, - repository, - loginModel: new GithubLoginModel(InMemoryStrategy), - changeWorkingDirectory: () => {}, - onDidChangeWorkDirs: () => {}, - getCurrentWorkDirs: () => [], - ...overrides, - }; -} - -export function gitHubTabContainerProps(atomEnv, repository, overrides = {}) { - return { - ...gitHubTabItemProps(atomEnv, repository), - rootHolder: new RefHolder(), - ...overrides, - }; -} - -export function gitHubTabControllerProps(atomEnv, repository, overrides = {}) { - return { - ...gitHubTabContainerProps(atomEnv, repository), - remoteOperationObserver: new OperationStateObserver(repository, PUSH, PULL, FETCH), - workingDirectory: repository.getWorkingDirectoryPath(), - allRemotes: new RemoteSet(), - branches: new BranchSet(), - aheadCount: 0, - pushInProgress: false, - ...overrides, - }; -} - -export function gitHubTabViewProps(atomEnv, repository, overrides = {}) { - return { - workspace: atomEnv.workspace, - remoteOperationObserver: new OperationStateObserver(repository, PUSH, PULL, FETCH), - loginModel: new GithubLoginModel(InMemoryStrategy), - rootHolder: new RefHolder(), - - workingDirectory: repository.getWorkingDirectoryPath(), - branches: new BranchSet(), - currentBranch: nullBranch, - remotes: new RemoteSet(), - currentRemote: nullRemote, - manyRemotesAvailable: false, - aheadCount: 0, - pushInProgress: false, - - handlePushBranch: () => {}, - handleRemoteSelect: () => {}, - changeWorkingDirectory: () => {}, - onDidChangeWorkDirs: () => {}, - getCurrentWorkDirs: () => [], - - ...overrides, - }; -} diff --git a/test/items/github-tab-item.test.js b/test/items/github-tab-item.test.js index ae127b9a51..cf52d1e45d 100644 --- a/test/items/github-tab-item.test.js +++ b/test/items/github-tab-item.test.js @@ -3,8 +3,10 @@ import {mount} from 'enzyme'; import PaneItem from '../../lib/atom/pane-item'; import GitHubTabItem from '../../lib/items/github-tab-item'; +import GithubLoginModel from '../../lib/models/github-login-model'; +import {InMemoryStrategy} from '../../lib/shared/keytar-strategy'; + import {cloneRepository, buildRepository} from '../helpers'; -import {gitHubTabItemProps} from '../fixtures/props/github-tab-props'; describe('GitHubTabItem', function() { let atomEnv, repository; @@ -20,14 +22,26 @@ describe('GitHubTabItem', function() { atomEnv.destroy(); }); - function buildApp(overrideProps = {}) { - const props = gitHubTabItemProps(atomEnv, repository, overrideProps); + function buildApp(props = {}) { + const workspace = props.workspace || atomEnv.workspace; return ( - + {({itemHolder}) => ( {}} + onDidChangeWorkDirs={() => {}} + getCurrentWorkDirs={() => []} + openCreateDialog={() => {}} + openPublishDialog={() => {}} + openCloneDialog={() => {}} + openGitTab={() => {}} {...props} /> )} diff --git a/test/models/refresher.test.js b/test/models/refresher.test.js new file mode 100644 index 0000000000..5bec43d169 --- /dev/null +++ b/test/models/refresher.test.js @@ -0,0 +1,67 @@ +import Refresher from '../../lib/models/refresher'; + +describe('Refresher', function() { + let refresher; + + beforeEach(function() { + refresher = new Refresher(); + }); + + afterEach(function() { + refresher.dispose(); + }); + + it('calls the latest retry method registered per key instance when triggered', function() { + const keyOne = Symbol('one'); + const keyTwo = Symbol('two'); + + const one0 = sinon.spy(); + const one1 = sinon.spy(); + const two0 = sinon.spy(); + + refresher.setRetryCallback(keyOne, one0); + refresher.setRetryCallback(keyOne, one1); + refresher.setRetryCallback(keyTwo, two0); + + refresher.trigger(); + + assert.isFalse(one0.called); + assert.isTrue(one1.called); + assert.isTrue(two0.called); + }); + + it('deregisters a retry callback for a key', function() { + const keyOne = Symbol('one'); + const keyTwo = Symbol('two'); + + const one = sinon.spy(); + const two = sinon.spy(); + + refresher.setRetryCallback(keyOne, one); + refresher.setRetryCallback(keyTwo, two); + + refresher.deregister(keyOne); + + refresher.trigger(); + + assert.isFalse(one.called); + assert.isTrue(two.called); + }); + + it('deregisters all retry callbacks on dispose', function() { + const keyOne = Symbol('one'); + const keyTwo = Symbol('two'); + + const one = sinon.spy(); + const two = sinon.spy(); + + refresher.setRetryCallback(keyOne, one); + refresher.setRetryCallback(keyTwo, two); + + refresher.dispose(); + refresher.trigger(); + + assert.isFalse(one.called); + assert.isFalse(two.called); + }); +}); diff --git a/test/views/create-dialog.test.js b/test/views/create-dialog.test.js index ac877841bd..f48b72037e 100644 --- a/test/views/create-dialog.test.js +++ b/test/views/create-dialog.test.js @@ -292,6 +292,40 @@ describe('CreateDialog', function() { assert.isTrue(repository.push.calledWith('other-branch', {remote: CREATED_REMOTE, setUpstream: true})); }); + it('initializes an empty repository', async function() { + expectRelayQuery({ + name: createRepositoryQuery.operation.name, + variables: {input: {name: 'repo-name', ownerId: 'user0', visibility: 'PUBLIC'}}, + }, op => { + return relayResponseBuilder(op) + .createRepository(m => { + m.repository(r => { + r.sshUrl('ssh@github.com:user0/repo-name.git'); + r.url('https://github.com/user0/repo-name'); + }); + }) + .build(); + }).resolve(); + + const empty = await buildRepository(temp.mkdirSync()); + assert.isTrue(empty.isEmpty()); + + sinon.stub(empty, 'addRemote').resolves(CREATED_REMOTE); + sinon.stub(empty, 'push'); + + await publishRepository({ + ownerID: 'user0', + name: 'repo-name', + visibility: 'PUBLIC', + protocol: 'https', + sourceRemoteName: 'origin', + }, {repository: empty, relayEnvironment}); + + assert.isTrue(empty.isPresent()); + assert.isTrue(empty.addRemote.calledWith('origin', 'https://github.com/user0/repo-name')); + assert.isFalse(empty.push.called); + }); + it('fails if the source repository has no "master" or current branches', async function() { await repository.checkout('other-branch', {createNew: true}); await repository.checkout('HEAD^'); diff --git a/test/views/github-tab-view.test.js b/test/views/github-tab-view.test.js index fac8582d9b..928cd4134d 100644 --- a/test/views/github-tab-view.test.js +++ b/test/views/github-tab-view.test.js @@ -1,12 +1,19 @@ import React from 'react'; import {shallow} from 'enzyme'; +import temp from 'temp'; -import {gitHubTabViewProps} from '../fixtures/props/github-tab-props'; import Repository from '../../lib/models/repository'; import Remote, {nullRemote} from '../../lib/models/remote'; import RemoteSet from '../../lib/models/remote-set'; -import Branch from '../../lib/models/branch'; +import Branch, {nullBranch} from '../../lib/models/branch'; +import BranchSet from '../../lib/models/branch-set'; import GitHubTabView from '../../lib/views/github-tab-view'; +import {InMemoryStrategy} from '../../lib/shared/keytar-strategy'; +import GithubLoginModel from '../../lib/models/github-login-model'; +import RefHolder from '../../lib/models/ref-holder'; +import Refresher from '../../lib/models/refresher'; + +import {buildRepository, cloneRepository} from '../helpers'; describe('GitHubTabView', function() { let atomEnv; @@ -19,9 +26,39 @@ describe('GitHubTabView', function() { atomEnv.destroy(); }); - function buildApp(overrideProps = {}) { - const props = gitHubTabViewProps(atomEnv, overrideProps.repository || Repository.absent(), overrideProps); - return ; + function buildApp(props) { + const repo = props.repository || Repository.absent(); + + return ( + {}} + handleRemoteSelect={() => {}} + changeWorkingDirectory={() => {}} + onDidChangeWorkDirs={() => {}} + getCurrentWorkDirs={() => []} + openCreateDialog={() => {}} + openBoundPublishDialog={() => {}} + openCloneDialog={() => {}} + openGitTab={() => {}} + + {...props} + /> + ); } it('renders a LoadingView if data is still loading', function() { @@ -29,11 +66,34 @@ describe('GitHubTabView', function() { assert.isTrue(wrapper.find('LoadingView').exists()); }); - it('renders a RemoteContainer if a remote has been chosen', function() { + it('renders a no-local view when no local repository is found', function() { + const wrapper = shallow(buildApp({ + repository: Repository.absent(), + })); + assert.isTrue(wrapper.exists('GitHubBlankNoLocal')); + }); + + it('renders a uninitialized view when a local repository is not initialized', async function() { + const workdir = temp.mkdirSync(); + const repository = await buildRepository(workdir); + + const wrapper = shallow(buildApp({repository})); + assert.isTrue(wrapper.exists('GitHubBlankUninitialized')); + }); + + it('renders a no-remote view when the local repository has no remotes', async function() { + const repository = await buildRepository(await cloneRepository()); + + const wrapper = shallow(buildApp({repository, currentRemote: nullRemote, manyRemotesAvailable: false})); + assert.isTrue(wrapper.exists('GitHubBlankNoRemote')); + }); + + it('renders a RemoteContainer if a remote has been chosen', async function() { + const repository = await buildRepository(await cloneRepository()); const currentRemote = new Remote('aaa', 'git@github.com:aaa/bbb.git'); const currentBranch = new Branch('bbb'); const handlePushBranch = sinon.spy(); - const wrapper = shallow(buildApp({currentRemote, currentBranch, handlePushBranch})); + const wrapper = shallow(buildApp({repository, currentRemote, currentBranch, handlePushBranch})); const container = wrapper.find('RemoteContainer'); assert.isTrue(container.exists()); @@ -42,10 +102,12 @@ describe('GitHubTabView', function() { assert.isTrue(handlePushBranch.calledWith(currentBranch, currentRemote)); }); - it('renders a RemoteSelectorView when many remote choices are available', function() { + it('renders a RemoteSelectorView when many remote choices are available', async function() { + const repository = await buildRepository(await cloneRepository()); const remotes = new RemoteSet(); const handleRemoteSelect = sinon.spy(); const wrapper = shallow(buildApp({ + repository, remotes, currentRemote: nullRemote, manyRemotesAvailable: true, @@ -59,14 +121,9 @@ describe('GitHubTabView', function() { assert.isTrue(handleRemoteSelect.called); }); - it('renders a static message when no remotes are available', function() { - const wrapper = shallow(buildApp({currentRemote: nullRemote, manyRemotesAvailable: false})); - assert.isTrue(wrapper.find('.github-GitHub-noRemotes').exists()); - }); - - it('calls changeWorkingDirectory when a project is selected', async function() { + it('calls changeWorkingDirectory when a project is selected', function() { const changeWorkingDirectory = sinon.spy(); - const wrapper = shallow(await buildApp({changeWorkingDirectory})); + const wrapper = shallow(buildApp({changeWorkingDirectory})); wrapper.find('TabHeaderView').prop('handleWorkDirSelect')({target: {value: 'some-path'}}); assert.isTrue(changeWorkingDirectory.calledWith('some-path')); });