Skip to content

Commit

Permalink
[#12557] Instructor edit feedback session page: NumberFormatException…
Browse files Browse the repository at this point in the history
… when inputting decimal numbers into distribute points questions (#12558)

* Fix min-max point bug in const-sum questions

* Fix lint issues

* Move ceil's implementation to QuestionEditDetailsFormComponent

* Address lint issues

* Prevent non-numerical characters from being inputted

* Add tests for onPointsInput

* Fix component tests lint issues

* Restrict length of input of numbers

* Abstract methods out into question-edit-details-form

* Update test descriptions

* Update min-value for stepper of num-scale question

* Add method to restrict float inputs

---------

Co-authored-by: Jason Qiu <[email protected]>
  • Loading branch information
dlimyy and jasonqiu212 authored Oct 3, 2023
1 parent defc75f commit 2ea7f48
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<div class="col-sm-6">
<input id="total-points-radio" class="form-check-input" type="radio" [ngModel]="model.pointsPerOption" [value]="false"
(ngModelChange)="triggerModelChange('pointsPerOption', $event)" [disabled]="!isEditable" aria-label="Use Total Points Checkbox">
<input id="total-points" type="number" class="form-control" min="1" step="1"
<input id="total-points" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'points')"
[ngModel]="!model.pointsPerOption ? model.points : ''" (ngModelChange)="triggerModelChange('points', $event)" [disabled]="!isEditable || model.pointsPerOption" aria-label="Total Points Input">
</div>
<div class="col-sm-6 text-start">
Expand All @@ -47,7 +47,7 @@
<div class="col-sm-6">
<input id="per-option-points-radio" class="form-check-input" type="radio" [ngModel]="model.pointsPerOption" [value]="true"
(ngModelChange)="triggerModelChange('pointsPerOption', $event)" [disabled]="!isEditable" aria-label="Use Total Points Times Number of Options Checkbox">
<input id="per-option-points" type="number" class="form-control" min="1" step="1"
<input id="per-option-points" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'points')"
[ngModel]="model.pointsPerOption ? model.points : ''" (ngModelChange)="triggerModelChange('points', $event)" [disabled]="!isEditable || !model.pointsPerOption" aria-label="Points Input">
</div>
<div class="col-sm-6 text-start">
Expand All @@ -64,7 +64,7 @@
<input id="min-point-checkbox" class="form-check-input" type="checkbox"
[ngModel]="hasMinPoint"
(ngModelChange)="resetMinPoint($event)" [disabled]="!isEditable" aria-label="Minimum Value Checkbox">
<input id="min-point" type="number" class="form-control" min="1" step="1"
<input id="min-point" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'minPoint')"
[ngModel]="hasMinPoint ? model.minPoint : ''" (ngModelChange)="triggerModelChange('minPoint', $event)" [disabled]="!isEditable || !hasMinPoint" aria-label="Minimum Value Input">
<b class="ngb-tooltip-class" ngbTooltip="The minimum allocation of the points to an option, e.g if you specify 5 points here, the user must input a value larger than or equal to 5 for each option.">minimum per option</b>
</label>
Expand All @@ -76,7 +76,7 @@
<input id="max-point-checkbox" class="form-check-input" type="checkbox"
[ngModel]="hasMaxPoint"
(ngModelChange)="resetMaxPoint($event)" [disabled]="!isEditable" aria-label="Maximum Value Checkbox">
<input id="max-point" type="number" class="form-control" min="1" step="1"
<input id="max-point" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'maxPoint')"
[ngModel]="hasMaxPoint ? model.maxPoint : ''" (ngModelChange)="triggerModelChange('maxPoint', $event)" [disabled]="!isEditable || !hasMaxPoint" aria-label="Maximum Value Input">
<b class="ngb-tooltip-class" ngbTooltip="The maximum allocation of the points to an option, e.g if you specify 30 points here, the user must input a value smaller than or equal to 30 for each option.">maximum per option</b>
</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { ConstsumOptionsFieldComponent } from './constsum-options-field/constsum-options-field.component';
import {
ConstsumOptionsQuestionEditDetailsFormComponent,
Expand Down Expand Up @@ -33,4 +34,52 @@ describe('ConstsumOptionsQuestionEditDetailsFormComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should prevent alphabetical character inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: 'b',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).toHaveBeenCalled();
});

it('should prevent decimal point inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: '.',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).toHaveBeenCalled();
});

it('should allow digit inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: '7',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).not.toHaveBeenCalled();
});

it('should allow number input with less than or equal to 9 digits', () => {
const inputElement = fixture.debugElement.query(By.css('#max-point')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '12345';
component.restrictIntegerInputLength(inputEvent, 'points');
expect((inputEvent.target as HTMLInputElement).value).toEqual('12345');
});

it('should restrict number input with more than 9 digits to 9 digits', () => {
const inputElement = fixture.debugElement.query(By.css('#max-point')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '123456789012345';
component.restrictIntegerInputLength(inputEvent, 'points');
expect((inputEvent.target as HTMLInputElement).value).toEqual('123456789');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="col-sm-3">
<input id="total-points-radio" class="form-check-input" type="radio" [ngModel]="model.pointsPerOption" [value]="false"
(ngModelChange)="triggerModelChange('pointsPerOption', $event)" [disabled]="!isEditable" aria-label="Use Total Points Checkbox">
<input id="total-points" type="number" class="form-control" min="1" step="1"
<input id="total-points" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'points')"
[ngModel]="!model.pointsPerOption ? model.points : ''" (ngModelChange)="triggerModelChange('points', $event)" [disabled]="!isEditable || model.pointsPerOption" aria-label="Total Points Input">
</div>
<div class="col-sm-9 text-start">
Expand All @@ -22,7 +22,7 @@
<div class="col-sm-3">
<input id="per-option-points-radio" class="form-check-input" type="radio" [ngModel]="model.pointsPerOption" [value]="true"
(ngModelChange)="triggerModelChange('pointsPerOption', $event)" [disabled]="!isEditable" aria-label="Use Total Points Times Number of Recipients Checkbox">
<input id="per-option-points" type="number" class="form-control" min="1" step="1"
<input id="per-option-points" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'points')"
[ngModel]="model.pointsPerOption ? model.points : ''" (ngModelChange)="triggerModelChange('points', $event)" [disabled]="!isEditable || !model.pointsPerOption" aria-label="Points Input">
</div>
<div class="col-sm-9 text-start">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import {
ConstsumRecipientsQuestionEditDetailsFormComponent,
} from './constsum-recipients-question-edit-details-form.component';
Expand Down Expand Up @@ -28,4 +29,52 @@ describe('ConstsumRecipientsQuestionEditDetailsFormComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should prevent alphabetical character inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: 'a',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).toHaveBeenCalled();
});

it('should prevent decimal point inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: '.',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).toHaveBeenCalled();
});

it('should allow digit inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: '6',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).not.toHaveBeenCalled();
});

it('should allow number input with less than or equal to 9 digits', () => {
const inputElement = fixture.debugElement.query(By.css('#total-points')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '12345';
component.restrictIntegerInputLength(inputEvent, 'points');
expect((inputEvent.target as HTMLInputElement).value).toEqual('12345');
});

it('should restrict number input with more than 9 digits to 9 digits', () => {
const inputElement = fixture.debugElement.query(By.css('#total-points')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '123456789012345';
component.restrictIntegerInputLength(inputEvent, 'points');
expect((inputEvent.target as HTMLInputElement).value).toEqual('123456789');
});
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
<div class="row">
<div class="col-sm-4">
<span class="ngb-tooltip-class" ngbTooltip="Minimum acceptable response value">Minimum value:</span>
<input id="min-value" type="number" class="form-control" [ngModel]="model.minScale" (ngModelChange)="triggerModelChange('minScale', $event === null ? $event : Math.ceil($event))" [disabled]="!isEditable" [step]="1" aria-label="Minimum Value Input">
<input id="min-value" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'minScale')"
[ngModel]="model.minScale" (ngModelChange)="triggerModelChange('minScale', $event)" [disabled]="!isEditable" aria-label="Minimum Value Input">
</div>
<div class="col-sm-4">
<span class="ngb-tooltip-class" ngbTooltip="Value to be increased/decreased each step">Increment:</span>
<input id="increment-value" type="number" class="form-control" [ngModel]="model.step" (ngModelChange)="triggerModelChange('step', $event)" [disabled]="!isEditable"
step="any" min="0" aria-label="Increment Value Input">
<input id="increment-value" type="number" (keypress)="onFloatInput($event)" class="form-control" [ngModel]="model.step" (ngModelChange)="triggerModelChange('step', $event)" [disabled]="!isEditable" (input) = "restrictFloatInputLength($event, 'step')"
step="any" min="0" max="999999999" aria-label="Increment Value Input">
</div>
<div class="col-sm-4">
<span class="ngb-tooltip-class" ngbTooltip="Maximum acceptable response value">Maximum value:</span>
<input id="max-value" type="number" class="form-control" [ngModel]="model.maxScale" (ngModelChange)="triggerModelChange('maxScale', $event === null ? $event : Math.ceil($event))" [disabled]="!isEditable" [step]="1" min="{{ Math.ceil(model.minScale + model.step) }}" aria-label="Maximum Value Input">
<input id="max-value" type="number" class="form-control" (keypress)="onIntegerInput($event)" (paste)="onPaste($event)" min="1" max="999999999" (input)="restrictIntegerInputLength($event, 'maxScale')"
[ngModel]="model.maxScale" (ngModelChange)="triggerModelChange('maxScale', $event)" [disabled]="!isEditable" aria-label="Maximum Value Input">
</div>
</div>
<br>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NumScaleQuestionEditDetailsFormComponent } from './num-scale-question-edit-details-form.component';

describe('NumScaleQuestionEditDetailsFormComponent', () => {
Expand All @@ -25,4 +26,103 @@ describe('NumScaleQuestionEditDetailsFormComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should prevent alphabetical character inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: 'e',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).toHaveBeenCalled();
});

it('should prevent decimal point inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: '.',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).toHaveBeenCalled();
});

it('should allow digit inputs in onIntegerInput', () => {
const event = new KeyboardEvent('keypress', {
key: '3',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onIntegerInput(event);
expect(eventSpy).not.toHaveBeenCalled();
});

it('should prevent alphabetical character inputs in onFloatInput', () => {
const event = new KeyboardEvent('keypress', {
key: 'e',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onFloatInput(event);
expect(eventSpy).toHaveBeenCalled();
});

it('should allow decimal point inputs in onFloatInput', () => {
const event = new KeyboardEvent('keypress', {
key: '.',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onFloatInput(event);
expect(eventSpy).not.toHaveBeenCalled();
});

it('should allow digit inputs in onFloatInput', () => {
const event = new KeyboardEvent('keypress', {
key: '3',
});

const eventSpy = jest.spyOn(event, 'preventDefault');
component.onFloatInput(event);
expect(eventSpy).not.toHaveBeenCalled();
});

it('should allow number inputs with less than or equal to 9 digits in restrictIntegerInputLength', () => {
const inputElement = fixture.debugElement.query(By.css('#max-value')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '12345';
component.restrictIntegerInputLength(inputEvent, 'minScale');
expect((inputEvent.target as HTMLInputElement).value).toEqual('12345');
});

it('should restrict number inputs with more than 9 digits to 9 digits in restrictIntegerInputLength', () => {
const inputElement = fixture.debugElement.query(By.css('#max-value')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '123456789012345';
component.restrictIntegerInputLength(inputEvent, 'minScale');
expect((inputEvent.target as HTMLInputElement).value).toEqual('123456789');
});

it(`should allow number inputs with less than or equal to 9 digits, inclusive of decimal
point in restrictFloatInputLength`, () => {
const inputElement = fixture.debugElement.query(By.css('#increment-value')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '12.34567';
component.restrictFloatInputLength(inputEvent, 'step');
expect((inputEvent.target as HTMLInputElement).value).toEqual('12.34567');
});

it(`should restrict number inputs with more than 9 digits, inclusive of decimal
point to 9 digits in restrictFloatInputLength`, () => {
const inputElement = fixture.debugElement.query(By.css('#increment-value')).nativeElement as HTMLInputElement;
const inputEvent = new InputEvent('input');
inputElement.dispatchEvent(inputEvent);
(inputEvent.target as HTMLInputElement).value = '1234567.891';
component.restrictFloatInputLength(inputEvent, 'step');
expect((inputEvent.target as HTMLInputElement).value).toEqual('1234567.8');
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,52 @@ export abstract class QuestionEditDetailsFormComponent<D extends FeedbackQuestio
triggerModelChangeBatch(obj: Partial<D>): void {
this.detailsChange.emit({ ...this.model, ...obj });
}

onIntegerInput(event: KeyboardEvent): void {
const { key } = event;
const isBackspace = key === 'Backspace';
const isDigit = /[0-9]/.test(key);
if (!isBackspace && !isDigit) {
event.preventDefault();
}
}

onFloatInput(event: KeyboardEvent): void {
const { key } = event;
const isBackspace = key === 'Backspace';
const isDecimal = key === '.';
const isDigit = /[0-9]/.test(key);
if (!isBackspace && !isDigit && !isDecimal) {
event.preventDefault();
}
}

onPaste(event: ClipboardEvent): void {
const { clipboardData } = event;
if (clipboardData == null) {
return;
}
const pastedText = clipboardData.getData('text');
const isDigit = /^\d+$/.test(pastedText);
if (!isDigit) {
event.preventDefault();
}
}

restrictIntegerInputLength(event : InputEvent, field: keyof D) : void {
const target : HTMLInputElement = event.target as HTMLInputElement;
if (target.value != null && target.value.length > 9) {
target.value = target.value.substring(0, 9);
this.triggerModelChange(field, parseInt(target.value, 10) as any);
}
}

restrictFloatInputLength(event : InputEvent, field: keyof D) : void {
const target : HTMLInputElement = event.target as HTMLInputElement;
if (target.value != null && target.value.length > 9) {
target.value = target.value.substring(0, 9);
this.triggerModelChange(field, parseFloat(target.value) as any);
}
}

}

0 comments on commit 2ea7f48

Please sign in to comment.