Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Sign-in button is now disabled when token is empty or whitespace #248

Merged
merged 17 commits into from
Dec 12, 2017

Conversation

tvand7093
Copy link
Contributor

Description of the Change

When first signing in, the sign in button is enabled when there is no value in the token field. This changes makes it so that the sign in button is disabled when:

  1. The token field is empty
  2. The token is all whitespace

Once a valid character is typed into the field, the button becomes enabled. This also includes an error prompt in the event someone was able to click the button when the token is empty.

Alternate Designs

These change follow similar changes found here. For this reason, the code is rather similar and not many other decisions were made.

Benefits

This prevents the user from signing in with an empty token, which is more of a standard practice form validation practice.

Possible Drawbacks

N/A

Applicable Issues

#131

@tvand7093
Copy link
Contributor Author

Again, not sure why the build failed. I'm sure I'm doing something wrong since this happened on the last PR as well...

Copy link
Contributor

@jasonrudolph jasonrudolph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 Hi @tvand7093! I took a quick scan through the code and noted a few initial questions/suggestions. Would you mind addressing those things? Once that's done, I can take a closer look at this pull request.

Again, not sure why the build failed. I'm sure I'm doing something wrong since this happened on the last PR as well...

I'm not sure why it's failing. I don't think it's due to anything you're doing. I pushed up a branch containing your commit, and the build passed on that branch. 🤔

Can you try merging master into your branch? Maybe that will help. 🤷‍♂️🤞

@@ -14,6 +14,9 @@ class SignInComponent {
this.disposables.add(this.props.commandRegistry.add(this.element, {
'core:confirm': this.signIn.bind(this)
}))
this.props.workspace.observeTextEditors(editor => {
this.attachChangeEvent()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? This will observe all text editors in the workspace here, and that seems like more than we need.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed there so that way when the dialog first comes up, the editor is wired up to the event. If this code is commented out, then the button will never be able to change state. I have a test case that demonstrates this. If there is a more appropriate way to handle this, I'm all ears and will address it ASAP!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed there so that way when the dialog first comes up, the editor is wired up to the event.

We should be able to subscribe to change events right after the call to etch.initialize. [docs]

I think this will do the trick:

diff --git a/lib/sign-in-component.js b/lib/sign-in-component.js
index 2f94c31..62d4d2b 100644
--- a/lib/sign-in-component.js
+++ b/lib/sign-in-component.js
@@ -7,6 +7,12 @@ class SignInComponent {
   constructor (props) {
     this.props = props
     etch.initialize(this)
+
+    this.refs.editor.onDidChange(() => {
+      const token = this.refs.editor.getText().trim()
+      this.refs.loginButton.disabled = !token
+    })
+
     this.disposables = new CompositeDisposable()
     this.disposables.add(this.props.authenticationProvider.onDidChange(() => {
       etch.update(this)
@@ -14,9 +20,6 @@ class SignInComponent {
     this.disposables.add(this.props.commandRegistry.add(this.element, {
       'core:confirm': this.signIn.bind(this)
     }))
-    this.props.workspace.observeTextEditors(editor => {
-      this.attachChangeEvent()
-    })
   }
 
   destroy () {
@@ -29,11 +32,8 @@ class SignInComponent {
     etch.update(this)
   }
 
-  readAfterUpdate () {
-    this.attachChangeEvent()
-  }
-
   render () {
     return $.div({className: 'SignInComponent', tabIndex: -1},
       $.span({className: 'SignInComponent-GitHubLogo'}),
       $.h3(null, 'Sign in with GitHub'),
@@ -78,21 +78,9 @@ class SignInComponent {
       : null
   }
 
-  attachChangeEvent () {
-    const previousTokenEditor = this.editor
-    this.editor = this.refs.editor
-
-    if (!previousTokenEditor && this.editor) {
-      this.editor.onDidChange(() => {
-        const token = this.refs.editor.getText().trim()
-        this.refs.loginButton.disabled = !token
-      })
-    }
-  }
-
   async signIn () {
     const token = this.refs.editor.getText().trim()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m still new to using etch how so I wasn’t aware of when exactly the refs were available. Learn something new every day!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You and me both! I just learned this today, too. 😸

@@ -955,6 +955,88 @@ suite('TeletypePackage', function () {
assert(description.includes('some error'))
})

test('disables button when empty token specified', async () => {
{
const env = buildAtomEnvironment()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation here appears to be inconsistent with the rest of the code in the repository. Can you please update it to use 2 spaces for indentation.

For style guidelines, please see https://standardjs.com/#the-rules and CONTRIBUTING.md#javascript-styleguide.

Copy link
Contributor Author

@tvand7093 tvand7093 Nov 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the pattern as seen in tests above where there are multiple cases for one test method.
My bad, I see it now 😉

@tvand7093
Copy link
Contributor Author

@jasonrudolph as for the build, the only thing I can think of is that I'm doing this on a Mac, so perhaps there is something strange secretly going on...

@tvand7093
Copy link
Contributor Author

After looking at a diff between the two build logs is the TEST_TIMEOUT_IN_MS env variable is being set in your build and not mine. Perhaps that is causing some issues? 🤔

@jasonrudolph
Copy link
Contributor

After looking at a diff between the two build logs is the TEST_TIMEOUT_IN_MS env variable is being set in your build and not mine. Perhaps that is causing some issues?

Nice catch. That was indeed the problem. The build is green now. 😅

I'll circle back around to take another look at this pull request 🔜.

Copy link
Contributor

@jasonrudolph jasonrudolph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tvand7093: Thanks for pushing this forward. I've requested a few changes below. I hope this makes sense. If you have any questions, just let me know. 🙇

@@ -14,6 +14,9 @@ class SignInComponent {
this.disposables.add(this.props.commandRegistry.add(this.element, {
'core:confirm': this.signIn.bind(this)
}))
this.props.workspace.observeTextEditors(editor => {
this.attachChangeEvent()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is needed there so that way when the dialog first comes up, the editor is wired up to the event.

We should be able to subscribe to change events right after the call to etch.initialize. [docs]

I think this will do the trick:

diff --git a/lib/sign-in-component.js b/lib/sign-in-component.js
index 2f94c31..62d4d2b 100644
--- a/lib/sign-in-component.js
+++ b/lib/sign-in-component.js
@@ -7,6 +7,12 @@ class SignInComponent {
   constructor (props) {
     this.props = props
     etch.initialize(this)
+
+    this.refs.editor.onDidChange(() => {
+      const token = this.refs.editor.getText().trim()
+      this.refs.loginButton.disabled = !token
+    })
+
     this.disposables = new CompositeDisposable()
     this.disposables.add(this.props.authenticationProvider.onDidChange(() => {
       etch.update(this)
@@ -14,9 +20,6 @@ class SignInComponent {
     this.disposables.add(this.props.commandRegistry.add(this.element, {
       'core:confirm': this.signIn.bind(this)
     }))
-    this.props.workspace.observeTextEditors(editor => {
-      this.attachChangeEvent()
-    })
   }
 
   destroy () {
@@ -29,11 +32,8 @@ class SignInComponent {
     etch.update(this)
   }
 
-  readAfterUpdate () {
-    this.attachChangeEvent()
-  }
-
   render () {
     return $.div({className: 'SignInComponent', tabIndex: -1},
       $.span({className: 'SignInComponent-GitHubLogo'}),
       $.h3(null, 'Sign in with GitHub'),
@@ -78,21 +78,9 @@ class SignInComponent {
       : null
   }
 
-  attachChangeEvent () {
-    const previousTokenEditor = this.editor
-    this.editor = this.refs.editor
-
-    if (!previousTokenEditor && this.editor) {
-      this.editor.onDidChange(() => {
-        const token = this.refs.editor.getText().trim()
-        this.refs.loginButton.disabled = !token
-      })
-    }
-  }
-
   async signIn () {
     const token = this.refs.editor.getText().trim()

{
const env = buildAtomEnvironment()
const pack = await buildPackage(env, {signIn: false})
//this is to trigger the on change event in the token input
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you have a moment, would you mind merging the latest code from the master branch into this branch? Once you've done that, you'll see some linter violations reported in the build. Locally, you can run npm run lint to see those violations. Once they're resolved, the build should be green for you again. 😄

assert.equal(message, 'Invalid login token')
assert(description.includes('The login token must not be empty. Please insert a valid token and try again.'))
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test/teletype-package.test.js is intended to provide high-level integration tests for the package as a whole. I like having all these scenarios tested for the sign-in component, but I'd prefer to have most of these scenarios covered in a separate test file that's focused specifically on the behavior of the sign-in component.

With that in mind, I'd love to see a "happy path" integration test in this file, and have the other scenarios live in a file that's more of a unit test for the sign-in component. As an example, there's a high-level integration test for the JoinPortalComponent here:

test('prompting for a portal ID when joining', async () => {
const pack = await buildPackage(buildAtomEnvironment())
await pack.consumeStatusBar(new FakeStatusBar())
assert(!pack.portalStatusBarIndicator.isPopoverVisible())
await pack.joinPortal()
assert(pack.portalStatusBarIndicator.isPopoverVisible())
const {popoverComponent} = pack.portalStatusBarIndicator
const {portalListComponent} = popoverComponent.refs
const {joinPortalComponent} = portalListComponent.refs
const {portalIdEditor} = joinPortalComponent.refs
assert(portalIdEditor.element.contains(document.activeElement))
})

And the more detailed scenarios are covered in more of a unit test fashion here:

test('joining portals', async () => {
const {component} = await buildComponent()
const {joinPortalComponent, guestPortalBindingsContainer} = component.refs
assert(joinPortalComponent.refs.joinPortalLabel)
assert(!joinPortalComponent.refs.portalIdEditor)
assert(!joinPortalComponent.refs.joiningSpinner)
assert(!joinPortalComponent.refs.joinButton)
await joinPortalComponent.showPrompt()
assert(!joinPortalComponent.refs.joinPortalLabel)
assert(joinPortalComponent.refs.joinButton.disabled)
assert(joinPortalComponent.refs.portalIdEditor)
assert(!joinPortalComponent.refs.joiningSpinner)
// Attempt to join without inserting a portal id.
await joinPortalComponent.joinPortal()
assert.equal(component.props.notificationManager.errorCount, 1)
assert(!joinPortalComponent.refs.joinPortalLabel)
assert(!joinPortalComponent.refs.joiningSpinner)
assert(joinPortalComponent.refs.portalIdEditor)
assert(joinPortalComponent.refs.joinButton.disabled)
// Insert an invalid portal id.
joinPortalComponent.refs.portalIdEditor.setText('invalid-portal-id')
assert(joinPortalComponent.refs.joinButton.disabled)
await joinPortalComponent.joinPortal()
assert.equal(component.props.notificationManager.errorCount, 2)
assert(!joinPortalComponent.refs.joinPortalLabel)
assert(!joinPortalComponent.refs.joiningSpinner)
assert(joinPortalComponent.refs.portalIdEditor)
// Insert a valid portal id.
const hostPortalBindingManager = await buildPortalBindingManager()
const {portal: hostPortal} = await hostPortalBindingManager.createHostPortalBinding()
joinPortalComponent.refs.portalIdEditor.setText(hostPortal.id)
assert(!joinPortalComponent.refs.joinButton.disabled)
joinPortalComponent.joinPortal()
await condition(() => (
!joinPortalComponent.refs.joinPortalLabel &&
joinPortalComponent.refs.joiningSpinner &&
!joinPortalComponent.refs.portalIdEditor
))
await condition(() => (
joinPortalComponent.refs.joinPortalLabel &&
!joinPortalComponent.refs.joiningSpinner &&
!joinPortalComponent.refs.portalIdEditor
))
await condition(() => queryParticipantElements(guestPortalBindingsContainer).length === 2)
assert(queryParticipantElement(guestPortalBindingsContainer, 1))
assert(queryParticipantElement(guestPortalBindingsContainer, 2))
// Insert a valid portal id but with leading and trailing whitespace.
await joinPortalComponent.showPrompt()
joinPortalComponent.refs.portalIdEditor.setText('\t ' + hostPortal.id + '\n\r\n')
joinPortalComponent.joinPortal()
await condition(() => (
!joinPortalComponent.refs.joinPortalLabel &&
joinPortalComponent.refs.joiningSpinner &&
!joinPortalComponent.refs.portalIdEditor
))
await condition(() => (
joinPortalComponent.refs.joinPortalLabel &&
!joinPortalComponent.refs.joiningSpinner &&
!joinPortalComponent.refs.portalIdEditor
))
await condition(() => queryParticipantElements(guestPortalBindingsContainer).length === 2)
assert(queryParticipantElement(guestPortalBindingsContainer, 1))
assert(queryParticipantElement(guestPortalBindingsContainer, 2))
// Simulate another guest joining the portal.
const newGuestPortalBindingManager = await buildPortalBindingManager()
await newGuestPortalBindingManager.createGuestPortalBinding(hostPortal.id)
await condition(() => queryParticipantElements(guestPortalBindingsContainer).length === 3)
assert(queryParticipantElement(guestPortalBindingsContainer, 1))
assert(queryParticipantElement(guestPortalBindingsContainer, 2))
assert(queryParticipantElement(guestPortalBindingsContainer, 3))
})

Would you be up for taking a similar approach here?

const token = this.refs.editor.getText().trim()

if (!token) {
this.props.notificationManager.addError('Invalid login token', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use renderErrorMessage here instead of a notification?

If we use a notification, that notification will appear all the way in the upper-right corner of the app. If we use renderErrorMessage, we can show the error message right next to the token field (i.e., right next to the error 🌟 ).

@tvand7093
Copy link
Contributor Author

@jasonrudolph thanks for all the feedback! I can for sure take care of these changes. I will probably get to them over the next few evenings (aside from thanksgiving)! 🦃

@@ -31,6 +30,13 @@ class SignInComponent {
return etch.update(this)
}

readAfterUpdate () {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the view refreshes, we have to rewire the onDidChange event otherwise the sign in button will be stuck in a disabled state, even when a token value is input.


await signInComponent.signIn()

// TODO: Find out why these refs are not updating.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonrudolph: This test case is supposed to test what happens when a token value is provided, the user clicks sign in, and the sign in fails. Testing using atom, it resets the token field and disables the button. However, it seems that I cannot get the refs here to be updated to actually test how it worked. I have banged my head on this all weekend... Any ideas?

@jasonrudolph
Copy link
Contributor

@tvand7093: Thanks for the updates. I've got this queued up for review. I'll follow up once I've reviewed the latest updates. 🙇

@jasonrudolph
Copy link
Contributor

@tvand7093: Just a quick note to let you know that I haven't forgotten about you. We're wrapping up some big updates, and then I'll circle back around to this pull request.

To eliminate the need to wire up an observer multiple times [1], always
render the login element, but set it to `display: none` when the signing
in.

Also, fix the failing test [2] by clearing the text when sign-in fails.

[1]: atom#248 (comment)

[2]: atom#248 (comment)
Use the existing integration test to verify that signing in with an
invalid token will disable the sign-in button and clear the token text.
@jasonrudolph
Copy link
Contributor

@tvand7093: I pushed up a few updates, mostly related to preventing the need for resubscribing to text editor change events each time the popover is re-rendered. 😄

Thanks again for this contribution! 🙇🍻

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants