Skip to content

LG-13346 | FormObject best practices for how_to_verify#10682

Merged
n1zyy merged 5 commits intomainfrom
mattw/LG-13346_formobject_how_to_verify
May 24, 2024
Merged

LG-13346 | FormObject best practices for how_to_verify#10682
n1zyy merged 5 commits intomainfrom
mattw/LG-13346_formobject_how_to_verify

Conversation

@n1zyy
Copy link
Contributor

@n1zyy n1zyy commented May 22, 2024

🎫 Ticket

Link to the relevant ticket:
LG-13346

🛠 Summary of changes

Applies some of the best practices described in this Google doc to bring this into line with how FormObject is used elsewhere.

There are two changes to behavior here:

  1. Rather than just enforcing presence of the selection parameter, we validate that it's an accepted value. It should be impossible for this to happen without someone manipulating the form, though.
  2. The error message is now actually displayed if the form is submitted without making a selection. Screenshot below.

👀 Screenshots

The red error condition is now shown if an invalid submission (somehow) occurs. It was previously not showing up.

Screenshot 2024-05-22 at 3 46 13 PM

n1zyy added 2 commits May 22, 2024 15:34
Normalizes the way we're using FormObjects.

changelog: Internal, FormObject normalization, Updates the HowToVerify flow

analytics.idv_doc_auth_how_to_verify_visited(**analytics_arguments)
@idv_how_to_verify_form = Idv::HowToVerifyForm.new(selection: @selection)
@idv_how_to_verify_form = Idv::HowToVerifyForm.new
Copy link
Contributor Author

Choose a reason for hiding this comment

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

AFAICT this code was a no-op. It was just determining what was passed into the form's constructor, but I don't believe it influenced the form's display.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah so I think this is old code from when the form was a pair radio buttons rather than a set of buttons that submit the form itself. These values would set the radio button to the previous selection if the user went back in the flow. At least that's a guess.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that is accurate. We had a button that would submit for radio button selection. The view was refactored and I think think got missed.

else
flash[:error] = result.first_error_message
redirect_to idv_how_to_verify_url
render :show, locals: { error: result.first_error_message }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By instantiating the form directly, we can just re-render the form.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a good idea from an analytics perspective as well, instead of sending folks back and getting a new event, we just get the single event from the first time they arrive. Which I think is closer to what we want to track 🤔

validates :selection, inclusion: {
in: [REMOTE, IPP],
message: proc { I18n.t('errors.doc_auth.how_to_verify_form') },
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

So it appears that inclusion does not imply presence: with only lines 15-18 here, a nil selection was apparently accepted. So I have both.

Copy link
Contributor

Choose a reason for hiding this comment

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

But we had presence already, did it still accept a nil selection?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

presence worked as expected. I wanted to ensure we only accepted known values, so I had initially changed it from presence to inclusion, under the arguably-reasonable expectation that, since [REMOTE, IPP] does not include nil, it wouldn't be accepted. Turns out both are needed.

<%= error %>
<% end %>
<% end %>

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 am not a UX whiz, but this allows us to conditionally render the error message which was previously not showing up.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is fine. The semantics are handled by the AlertComponent itself, and you're doing something that looks like what other alert components are already doing. Is there a design to reference, though?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, you're making me realize I should probably run this by UX explicitly. We were previously setting a flash message to this string, but it wasn't displaying. So I ✨ fixed the glitch ✨, but I should get UX buy-in on this change.

end

context 'an invalid selection is submitted' do
# (This should only be possible if someone alters the form)
Copy link
Contributor Author

@n1zyy n1zyy May 22, 2024

Choose a reason for hiding this comment

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

Do you object to me calling this out? The buttons just select "ipp" or "remote" as options, but a mischievous user could submit something weird. Or is it evident enough why we would care about invalid selections even if the user doesn't have the selections?

Copy link
Contributor

Choose a reason for hiding this comment

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

How is this different from the form submission error case that's handled in the spec starting on line 266? Could those two specs be merged into one? I think that might be cleaner?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.

My thinking at the time was that 266 tests when no selection is made, while this test is when an invalid selection is made, which wasn't previously handled.

But "invalid" is ambiguous wording used in both tests, and they should result in the same behavior.

Now the question is, do I make this a mini shared example or just duplicate it? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

OK yeah, we have specific behavior expectations for no selection vs. some selection so now that you say I think we should maybe keep them separate. Mini shared example is my preference, since it's what we'd end up doing if this spec gets larger anyway, right?

step: 'how_to_verify',
analytics_id: 'Doc Auth',
skip_hybrid_handoff: nil,
'selection' => selection,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This key comes through as a string vs. symbol, which is gross. However, the other tests in here are doing the same thing, so it's... precedented grossness.

end
end

context 'undo/back' do
Copy link
Contributor Author

Choose a reason for hiding this comment

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


context 'undo/back' do
it 'sets skip_doc_auth to nil and does not redirect' do
put :update, params: { undo_step: true }
Copy link
Contributor Author

@n1zyy n1zyy May 22, 2024

Choose a reason for hiding this comment

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

This is doing an HTTP PUT with undo_step: true as a form parameter, which does not make sense to me. There is no apparent way for the user to do this, and no code in the controller to act on this—it appears that this is equivalent to what happens when you submit a blank form, which, again, we already test. So I removed this whole case.

Please let me know if I'm misinterpreting.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good move!

Copy link
Contributor

@gina-yamada gina-yamada May 23, 2024

Choose a reason for hiding this comment

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

I am not sure why put vs calling undo_step.call with args. Aside from that, this test was to show that when you move backwards in the flow that some of the values on idv_session get set back to nil (the values before making any selection on this page.)

Goal was to testing the highlighted pieces below:
Screenshot 2024-05-23 at 8 13 45 AM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah-ha, this is helpful context; thanks!

I'm inclined to think that this provides equivalent coverage: https://github.com/18F/identity-idp/blob/main/spec/controllers/idv/how_to_verify_controller_spec.rb#L259-L266

I think the only way to get undo_step to fire are to fail the validation. Are you on board with this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ummm, so I think the form submission error that you link to is checking that those items are nil because they are not set yet, not because they are set back to nil on undo. In other words, that test is viewing it as if it is someone's first time hitting this page, not that they are moving backwards. The undo step only fires on a form submission, not when folks hit the back button in the browser. So it's only called when an update action happens on a controller.

Copy link
Contributor

@gina-yamada gina-yamada May 23, 2024

Choose a reason for hiding this comment

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

I am not sure it captures it but think you can delete this test. I am not seeing tests for each step in our flow (address, ssn, etc.) that test undo_step but there is a spec testing undo_step so it is covered.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The bit I linked to is testing the update action. However, I went ahead and pulled all this out into a shared context which I think will make this all easier to follow.

Just waiting on ruby 3.3.1 to build because I pulled in main. 🤦‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That change is 58fa3b8 if that makes it easier to read.

expect(subject.idv_session.skip_doc_auth).to be_nil
expect(subject.idv_session.opted_in_to_in_person_proofing).to be_nil
expect(response).to redirect_to(idv_how_to_verify_url)
expect(response).to render_template :show
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We now let the form handle this for us, so we no longer need to set a flash message, and we just re-render the form (show template) vs. redirecting.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above: does it make sense to combine this test case with the 'an invalid selection is submitted' spec?

@n1zyy n1zyy marked this pull request as ready for review May 22, 2024 20:08
@n1zyy n1zyy requested review from a team, JackRyan1989 and gina-yamada May 22, 2024 20:08
Copy link
Contributor

@JackRyan1989 JackRyan1989 left a comment

Choose a reason for hiding this comment

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

Looks good! Tested and works as expected. Just have some qs!


analytics.idv_doc_auth_how_to_verify_visited(**analytics_arguments)
@idv_how_to_verify_form = Idv::HowToVerifyForm.new(selection: @selection)
@idv_how_to_verify_form = Idv::HowToVerifyForm.new
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah so I think this is old code from when the form was a pair radio buttons rather than a set of buttons that submit the form itself. These values would set the radio button to the previous selection if the user went back in the flow. At least that's a guess.

else
flash[:error] = result.first_error_message
redirect_to idv_how_to_verify_url
render :show, locals: { error: result.first_error_message }
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a good idea from an analytics perspective as well, instead of sending folks back and getting a new event, we just get the single event from the first time they arrive. Which I think is closer to what we want to track 🤔

validates :selection, inclusion: {
in: [REMOTE, IPP],
message: proc { I18n.t('errors.doc_auth.how_to_verify_form') },
}
Copy link
Contributor

Choose a reason for hiding this comment

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

But we had presence already, did it still accept a nil selection?

<%= error %>
<% end %>
<% end %>

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is fine. The semantics are handled by the AlertComponent itself, and you're doing something that looks like what other alert components are already doing. Is there a design to reference, though?

end

context 'an invalid selection is submitted' do
# (This should only be possible if someone alters the form)
Copy link
Contributor

Choose a reason for hiding this comment

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

How is this different from the form submission error case that's handled in the spec starting on line 266? Could those two specs be merged into one? I think that might be cleaner?


context 'undo/back' do
it 'sets skip_doc_auth to nil and does not redirect' do
put :update, params: { undo_step: true }
Copy link
Contributor

Choose a reason for hiding this comment

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

Good move!

expect(subject.idv_session.skip_doc_auth).to be_nil
expect(subject.idv_session.opted_in_to_in_person_proofing).to be_nil
expect(response).to redirect_to(idv_how_to_verify_url)
expect(response).to render_template :show
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above: does it make sense to combine this test case with the 'an invalid selection is submitted' spec?

end
let(:analytics_name) { :idv_doc_auth_how_to_verify_submitted }

shared_examples_for 'invalid form submissions' do
Copy link
Contributor

Choose a reason for hiding this comment

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

👨🏻‍🍳 💋

Copy link
Contributor

@JackRyan1989 JackRyan1989 left a comment

Choose a reason for hiding this comment

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

I think you still need UX to check the alert but the code LGTM.

@n1zyy n1zyy merged commit da81567 into main May 24, 2024
@n1zyy n1zyy deleted the mattw/LG-13346_formobject_how_to_verify branch May 24, 2024 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants