Skip to content

Commit

Permalink
Add validation and error summary to comment form
Browse files Browse the repository at this point in the history
- Add check for empty comment on submit
- Add new error for when the comment is empty
- Add error type var to display the correct error
- Add tests

Ticket: https://phabricator.wikimedia.org/T367387
  • Loading branch information
Abban committed Sep 13, 2024
1 parent 0622d91 commit d703958
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 23 deletions.
14 changes: 11 additions & 3 deletions src/api/CommentResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@ export class ApiCommentResource implements CommentResource {
}

post( data: CommentRequest ): Promise<string> {
return axios.post( this.postEndpoint, data ).then( ( validationResult: AxiosResponse<CommentResponse> ) => {
if ( validationResult.data.status === 'OK' ) {
return Promise.reject();
const formData = new FormData();

formData.append( 'donationId', data.donationId.toString() );
formData.append( 'updateToken', data.updateToken );
formData.append( 'comment', data.comment );
formData.append( 'withName', data.withName.toString() );
formData.append( 'isPublic', data.isPublic.toString() );

return axios.post( this.postEndpoint, formData ).then( ( validationResult: AxiosResponse<CommentResponse> ) => {
if ( validationResult.data.status !== 'OK' ) {
return Promise.reject( validationResult.data.message );
}
return validationResult.data.message;
} );
Expand Down
70 changes: 57 additions & 13 deletions src/components/pages/donation_confirmation/DonationCommentPopUp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
<div v-else>
<p>{{ $t( 'donation_comment_popup_explanation' ) }}</p>

<ScrollTarget target-id="comment-scroll-target"/>
<TextField
input-type="textarea"
v-model="comment"
name="comment"
input-id="comment"
placeholder=""
:label="$t( 'donation_comment_popup_label' )"
:error-message="$t( 'donation_comment_popup_error' )"
:error-message="$t( commentError )"
:show-error="commentErrored"
:autofocus="true"
/>
Expand All @@ -45,6 +46,18 @@
{{ $t( 'donation_comment_popup_is_public' ) }}
</CheckboxField>

<ErrorSummary
:is-visible="commentErrored"
:items="[
{
validity: commentErrored ? Validity.INVALID : Validity.VALID,
message: $t( commentError ),
focusElement: 'comment',
scrollElement: 'comment-scroll-target'
},
]"
/>

<FormSummary :show-border="false">
<template #summary-buttons>
<FormButton
Expand All @@ -67,7 +80,7 @@
</template>

<script setup lang="ts">
import { computed, inject, onMounted, ref } from 'vue';
import { computed, inject, onMounted, ref, watch } from 'vue';
import { trackDynamicForm, trackFormSubmission } from '@src/util/tracking';
import { addressTypeFromName, AddressTypeModel } from '@src/view_models/AddressTypeModel';
import { Donation } from '@src/view_models/Donation';
Expand All @@ -76,6 +89,14 @@ import FormSummary from '@src/components/shared/FormSummary.vue';
import TextField from '@src/components/shared/form_fields/TextField.vue';
import CheckboxField from '@src/components/shared/form_fields/CheckboxField.vue';
import { CommentResource } from '@src/api/CommentResource';
import ErrorSummary from '@src/components/shared/validation_summary/ErrorSummary.vue';
import { Validity } from '@src/view_models/Validity';
import ScrollTarget from '@src/components/shared/ScrollTarget.vue';
enum CommentErrorTypes {
Empty,
Server
}
interface Props {
donation: Donation;
Expand All @@ -91,32 +112,55 @@ const comment = ref<string>( '' );
const commentIsPublic = ref<boolean>( false );
const commentHasPublicAuthorName = ref<boolean>( false );
const commentErrored = ref<boolean>( false );
const commentErrorType = ref<CommentErrorTypes>( CommentErrorTypes.Empty );
const commentHasBeenSubmitted = ref<boolean>( false );
const serverResponse = ref<string>( '' );
const serverError = ref<string>( '' );
const showPublishAuthor = computed<boolean>( () => addressTypeFromName( props.addressType ) !== AddressTypeModel.ANON );
const postComment = async (): Promise<void> => {
const postComment = (): Promise<void> => {
trackFormSubmission( commentForm.value );
try {
const message = await commentResource.post( {
donationId: props.donation.id,
updateToken: props.donation.updateToken,
comment: comment.value,
withName: commentHasPublicAuthorName.value,
isPublic: commentIsPublic.value,
} );
if ( comment.value === '' ) {
commentErrorType.value = CommentErrorTypes.Empty;
commentErrored.value = true;
return;
}
commentResource.post( {
donationId: props.donation.id,
updateToken: props.donation.updateToken,
comment: comment.value,
withName: commentHasPublicAuthorName.value,
isPublic: commentIsPublic.value,
} ).then( ( message: string ) => {
commentErrored.value = false;
commentHasBeenSubmitted.value = true;
serverResponse.value = message;
emit( 'disable-comment-link' );
} catch ( e ) {
} ).catch( ( message: string ) => {
commentErrorType.value = CommentErrorTypes.Server;
commentErrored.value = true;
}
serverError.value = message;
} );
};
onMounted( trackDynamicForm );
const commentError = computed<string>( () => {
if ( commentErrorType.value === CommentErrorTypes.Empty ) {
return 'donation_comment_popup_empty_error';
}
return serverError.value;
} );
watch( comment, ( newComment: string ) => {
if ( commentErrored.value && newComment !== '' ) {
commentErrored.value = false;
}
} );
</script>

<style lang="scss">
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/TestDoubles/FakeCommentResource.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CommentResource } from '@src/api/CommentResource';

export const successMessage = 'Success';
export const failureMessage = 'Fail';

export class FakeSucceedingCommentResource implements CommentResource {
post(): Promise<string> {
Expand All @@ -10,6 +11,6 @@ export class FakeSucceedingCommentResource implements CommentResource {

export class FakeFailingCommentResource implements CommentResource {
post(): Promise<string> {
return Promise.reject();
return Promise.reject( failureMessage );
}
}
48 changes: 42 additions & 6 deletions tests/unit/components/DonationCommentPopUp.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { mount } from '@vue/test-utils';
import DonationCommentPopUp from '@src/components/pages/donation_confirmation/DonationCommentPopUp.vue';
import { AddressTypeModel, addressTypeName } from '@src/view_models/AddressTypeModel';
import {
FakeFailingCommentResource,
FakeSucceedingCommentResource,
successMessage
} from '@test/unit/TestDoubles/FakeCommentResource';
import { failureMessage, FakeFailingCommentResource, FakeSucceedingCommentResource, successMessage } from '@test/unit/TestDoubles/FakeCommentResource';

describe( 'DonationCommentPopUp.vue', () => {
function getDefaultConfirmationData( isAnonymous: boolean ): any {
Expand Down Expand Up @@ -62,6 +58,44 @@ describe( 'DonationCommentPopUp.vue', () => {
expect( wrapper.find( '#withName' ).exists() ).toBeFalsy();
} );

it( 'shows error when comment is empty', async () => {
const wrapper = mount( DonationCommentPopUp, {
props: getDefaultConfirmationData( true ),
global: {
provide: {
commentResource: new FakeSucceedingCommentResource(),
},
},
} );

await wrapper.trigger( 'submit' );

expect( wrapper.find( '#comment-error' ).exists() ).toBeTruthy();
expect( wrapper.find( '.error-summary' ).exists() ).toBeTruthy();
expect( wrapper.find( '#comment-error' ).text() ).toStrictEqual( 'donation_comment_popup_empty_error' );
} );

it( 'resets error when comment text is entered', async () => {
const wrapper = mount( DonationCommentPopUp, {
props: getDefaultConfirmationData( true ),
global: {
provide: {
commentResource: new FakeSucceedingCommentResource(),
},
},
} );

await wrapper.trigger( 'submit' );

expect( wrapper.find( '#comment-error' ).exists() ).toBeTruthy();
expect( wrapper.find( '.error-summary' ).exists() ).toBeTruthy();

await wrapper.find( '#comment' ).setValue( 'My super great comment' );

expect( wrapper.find( '#comment-error' ).exists() ).toBeFalsy();
expect( wrapper.find( '.error-summary' ).exists() ).toBeFalsy();
} );

it( 'shows error when API response is rejected', async () => {
const wrapper = mount( DonationCommentPopUp, {
props: getDefaultConfirmationData( true ),
Expand All @@ -72,10 +106,12 @@ describe( 'DonationCommentPopUp.vue', () => {
},
} );

await wrapper.find( '#comment' ).setValue( 'My super great comment' );
await wrapper.trigger( 'submit' );

expect( wrapper.find( '#comment-error' ).exists() ).toBeTruthy();
expect( wrapper.find( '#comment-error' ).text() ).toStrictEqual( 'donation_comment_popup_error' );
expect( wrapper.find( '.error-summary' ).exists() ).toBeTruthy();
expect( wrapper.find( '#comment-error' ).text() ).toStrictEqual( failureMessage );
} );

it( 'shows message returned from API', async () => {
Expand Down

0 comments on commit d703958

Please sign in to comment.