Skip to content

Commit 19b2937

Browse files
committed
Improve simple-math GUI input validation
Reject blank, negative, and decimal values instead of treating them as zero. Surface an accessible validation message, hide stale calculations while input is invalid, and keep divide-by-zero feedback explicit. Also add form constraints, prevent the browser favicon 404, and improve mobile and keyboard focus styling. Expand the GUI tests to cover the new validation and reset behavior.
1 parent 14140b4 commit 19b2937

4 files changed

Lines changed: 172 additions & 62 deletions

File tree

simple-math/src/main/resources/static/index.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<title>Simple Math</title>
7+
<link rel="icon" href="data:,">
78
<link rel="stylesheet" href="style.css">
89
</head>
910
<body>
1011
<div class="container">
1112
<h1>Simple Math</h1>
1213
<label for="firstInput">What is the first number?</label>
13-
<input type="number" id="firstInput" placeholder="Enter the first number" value="10"/>
14+
<input type="number" id="firstInput" placeholder="Enter the first number" value="10" min="0" step="1"
15+
required aria-describedby="validationMessage"/>
1416
<br>
1517
<label for="secondInput">What is the second number?</label>
16-
<input type="number" id="secondInput" placeholder="Enter the second number" value="5"/>
18+
<input type="number" id="secondInput" placeholder="Enter the second number" value="5" min="0" step="1"
19+
required aria-describedby="validationMessage"/>
20+
<div class="validation-message" id="validationMessage" role="alert" aria-live="polite"></div>
1721
<div class="calculation" id="addition"></div>
1822
<div class="calculation" id="subtraction"></div>
1923
<div class="calculation" id="multiplication"></div>

simple-math/src/main/resources/static/script.js

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,61 @@ const subtraction = document.getElementById('subtraction');
55
const multiplication = document.getElementById('multiplication');
66
const division = document.getElementById('division');
77
const resetButton = document.getElementById('resetButton');
8+
const validationMessage = document.getElementById('validationMessage');
9+
10+
const INVALID_INPUT_MESSAGE = 'Enter whole numbers greater than or equal to zero.';
811

912
function formatNumber(num) {
1013
return num % 1 === 0 ? num : parseFloat(num.toFixed(2));
1114
}
1215

16+
function parseWholeNumber(input) {
17+
const value = input.value.trim();
18+
19+
if (value === '') {
20+
return null;
21+
}
22+
23+
const number = Number(value);
24+
return Number.isSafeInteger(number) && number >= 0 ? number : null;
25+
}
26+
27+
function clearCalculations() {
28+
addition.textContent = '';
29+
subtraction.textContent = '';
30+
multiplication.textContent = '';
31+
division.textContent = '';
32+
}
33+
34+
function updateInputValidity(firstNumber, secondNumber) {
35+
firstInput.setAttribute('aria-invalid', firstNumber === null ? 'true' : 'false');
36+
secondInput.setAttribute('aria-invalid', secondNumber === null ? 'true' : 'false');
37+
}
38+
1339
function updateCalculations() {
14-
const firstNumber = parseFloat(firstInput.value) || 0;
15-
const secondNumber = parseFloat(secondInput.value) || 0;
16-
addition.textContent = `${firstNumber} + ${secondNumber} = ${firstNumber + secondNumber}`;
17-
subtraction.textContent = `${firstNumber} - ${secondNumber} = ${firstNumber - secondNumber}`;
18-
multiplication.textContent = `${firstNumber} × ${secondNumber} = ${firstNumber * secondNumber}`;
19-
const quotient = formatNumber(firstNumber / secondNumber);
20-
division.textContent = secondNumber !== 0 ? `${firstNumber} ÷ ${secondNumber} = ${quotient}`
40+
const firstNumber = parseWholeNumber(firstInput);
41+
const secondNumber = parseWholeNumber(secondInput);
42+
updateInputValidity(firstNumber, secondNumber);
43+
44+
if (firstNumber === null || secondNumber === null) {
45+
validationMessage.textContent = INVALID_INPUT_MESSAGE;
46+
clearCalculations();
47+
return;
48+
}
49+
50+
validationMessage.textContent = '';
51+
addition.textContent = `${firstNumber} + ${secondNumber} = ${formatNumber(firstNumber + secondNumber)}`;
52+
subtraction.textContent = `${firstNumber} - ${secondNumber} = ${formatNumber(firstNumber - secondNumber)}`;
53+
multiplication.textContent = `${firstNumber} × ${secondNumber} = ${formatNumber(firstNumber * secondNumber)}`;
54+
division.textContent = secondNumber !== 0 ? `${firstNumber} ÷ ${secondNumber} = ${formatNumber(firstNumber / secondNumber)}`
2155
: 'Cannot divide by zero!';
2256
}
2357

2458
firstInput.addEventListener('input', updateCalculations);
2559
secondInput.addEventListener('input', updateCalculations);
2660
resetButton.addEventListener('click', () => {
27-
firstInput.value = 10;
28-
secondInput.value = 5;
61+
firstInput.value = '10';
62+
secondInput.value = '5';
2963
updateCalculations();
3064
});
3165

simple-math/src/main/resources/static/style.css

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
15
body {
26
font-family: Helvetica, sans-serif;
37
margin: 0;
48
background-color: #f0f4f8;
5-
height: 100vh;
9+
min-height: 100dvh;
10+
padding: 16px;
611
display: flex;
712
justify-content: center;
813
align-items: center;
@@ -55,13 +60,24 @@ input[type="number"]:focus {
5560
border: 1px solid #ddd;
5661
}
5762

63+
.validation-message {
64+
min-height: 20px;
65+
margin-bottom: 12px;
66+
color: #a61b1b;
67+
font-size: 14px;
68+
}
69+
70+
input[aria-invalid="true"] {
71+
border-color: #a61b1b;
72+
}
73+
5874
#resetButton {
5975
margin-top: 10px;
6076
padding: 10px 20px;
6177
font-size: 16px;
6278
border: none;
6379
border-radius: 5px;
64-
background-color: #FF6347;
80+
background-color: #c2410c;
6581
color: white;
6682
cursor: pointer;
6783
display: block;
@@ -71,5 +87,16 @@ input[type="number"]:focus {
7187
}
7288

7389
#resetButton:hover {
74-
background-color: #ff4b3a;
90+
background-color: #9a3412;
91+
}
92+
93+
#resetButton:focus-visible {
94+
outline: 3px solid rgba(194, 65, 12, 0.35);
95+
outline-offset: 3px;
96+
}
97+
98+
@media (max-width: 480px) {
99+
.container {
100+
padding: 18px;
101+
}
75102
}
Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
package dev.delivercraft.math.gui;
22

3-
import static org.assertj.core.api.Assertions.assertThat;
4-
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
5-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6-
73
import org.htmlunit.WebClient;
8-
import org.htmlunit.html.HtmlDivision;
94
import org.htmlunit.html.HtmlNumberInput;
105
import org.htmlunit.html.HtmlPage;
11-
import java.io.IOException;
126
import org.junit.jupiter.api.Test;
137
import org.junit.jupiter.params.ParameterizedTest;
148
import org.junit.jupiter.params.provider.ValueSource;
@@ -18,9 +12,31 @@
1812
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
1913
import org.springframework.web.context.WebApplicationContext;
2014

15+
import java.io.IOException;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
19+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
20+
2121
@WebMvcTest
2222
class ApplicationTest {
2323

24+
private static final String BAD_INPUT_MESSAGE = "Enter whole numbers greater than or equal to zero.";
25+
26+
private static final String FIRST_ID = "firstInput";
27+
28+
private static final String SECOND_ID = "secondInput";
29+
30+
private static final String MESSAGE_ID = "validationMessage";
31+
32+
private static final String ADDITION_ID = "addition";
33+
34+
private static final String SUBTRACTION_ID = "subtraction";
35+
36+
private static final String MULTIPLICATION_ID = "multiplication";
37+
38+
private static final String DIVISION_ID = "division";
39+
2440
private final MockMvc mockMvc;
2541

2642
private final WebClient webClient;
@@ -41,60 +57,105 @@ void indexFileIsAccessibleImplicitlyOrExplicitly(String path) throws Exception {
4157
void defaultCalculationsAreShown() throws IOException {
4258
HtmlPage page = getHtmlPage();
4359

44-
assertThat(firstInputNumberInput(page).getDefaultValue()).isEqualTo("10");
45-
assertThat(secondInputNumberInput(page).getDefaultValue()).isEqualTo("5");
46-
assertThat(additionDiv(page).getTextContent()).isEqualTo("10 + 5 = 15");
47-
assertThat(subtractionDiv(page).getTextContent()).isEqualTo("10 - 5 = 5");
48-
assertThat(multiplicationDiv(page).getTextContent()).isEqualTo("10 × 5 = 50");
49-
assertThat(divisionDiv(page).getTextContent()).isEqualTo("10 ÷ 5 = 2");
60+
assertThat(numberInput(page, FIRST_ID).getDefaultValue()).isEqualTo("10");
61+
assertThat(numberInput(page, SECOND_ID).getDefaultValue()).isEqualTo("5");
62+
assertThat(text(page, MESSAGE_ID)).isEmpty();
63+
assertThat(text(page, ADDITION_ID)).isEqualTo("10 + 5 = 15");
64+
assertThat(text(page, SUBTRACTION_ID)).isEqualTo("10 - 5 = 5");
65+
assertThat(text(page, MULTIPLICATION_ID)).isEqualTo("10 × 5 = 50");
66+
assertThat(text(page, DIVISION_ID)).isEqualTo("10 ÷ 5 = 2");
67+
}
68+
69+
@Test
70+
void pageMetadataAndNumberInputConstraintsAreShown() throws IOException {
71+
HtmlPage page = getHtmlPage();
72+
73+
assertThat(page.asXml()).contains("rel=\"icon\"");
74+
assertThat(numberInput(page, FIRST_ID).hasAttribute("required")).isTrue();
75+
assertThat(numberInput(page, FIRST_ID).getAttribute("min")).isEqualTo("0");
76+
assertThat(numberInput(page, FIRST_ID).getAttribute("step")).isEqualTo("1");
77+
assertThat(numberInput(page, SECOND_ID).hasAttribute("required")).isTrue();
78+
assertThat(numberInput(page, SECOND_ID).getAttribute("min")).isEqualTo("0");
79+
assertThat(numberInput(page, SECOND_ID).getAttribute("step")).isEqualTo("1");
5080
}
5181

5282
@Test
5383
void calculationsAreUpdatedOnUserInput() throws IOException {
5484
HtmlPage page = getHtmlPage();
55-
HtmlNumberInput firstInput = firstInputNumberInput(page);
56-
HtmlNumberInput secondInput = secondInputNumberInput(page);
85+
HtmlNumberInput firstInput = numberInput(page, FIRST_ID);
86+
HtmlNumberInput secondInput = numberInput(page, SECOND_ID);
5787

5888
firstInput.setValueAttribute("20");
5989
fireInputEventFor(firstInput);
6090
secondInput.setValueAttribute("4");
6191
fireInputEventFor(secondInput);
6292

63-
assertThat(additionDiv(page).getTextContent()).isEqualTo("20 + 4 = 24");
64-
assertThat(subtractionDiv(page).getTextContent()).isEqualTo("20 - 4 = 16");
65-
assertThat(multiplicationDiv(page).getTextContent()).isEqualTo("20 × 4 = 80");
66-
assertThat(divisionDiv(page).getTextContent()).isEqualTo("20 ÷ 4 = 5");
93+
assertThat(text(page, ADDITION_ID)).isEqualTo("20 + 4 = 24");
94+
assertThat(text(page, SUBTRACTION_ID)).isEqualTo("20 - 4 = 16");
95+
assertThat(text(page, MULTIPLICATION_ID)).isEqualTo("20 × 4 = 80");
96+
assertThat(text(page, DIVISION_ID)).isEqualTo("20 ÷ 4 = 5");
97+
}
98+
99+
@ParameterizedTest
100+
@ValueSource(strings = {"", "-1", "1.5"})
101+
void calculationsAreHiddenWhenInputIsInvalid(String invalidInput) throws IOException {
102+
HtmlPage page = getHtmlPage();
103+
HtmlNumberInput firstInput = numberInput(page, FIRST_ID);
104+
105+
firstInput.setValueAttribute(invalidInput);
106+
fireInputEventFor(firstInput);
107+
108+
assertThat(text(page, MESSAGE_ID)).isEqualTo(BAD_INPUT_MESSAGE);
109+
assertThat(text(page, ADDITION_ID)).isEmpty();
110+
assertThat(text(page, SUBTRACTION_ID)).isEmpty();
111+
assertThat(text(page, MULTIPLICATION_ID)).isEmpty();
112+
assertThat(text(page, DIVISION_ID)).isEmpty();
113+
}
114+
115+
@Test
116+
void validInputClearsValidationMessage() throws IOException {
117+
HtmlPage page = getHtmlPage();
118+
HtmlNumberInput firstInput = numberInput(page, FIRST_ID);
119+
120+
firstInput.setValueAttribute("-1");
121+
fireInputEventFor(firstInput);
122+
firstInput.setValueAttribute("12");
123+
fireInputEventFor(firstInput);
124+
125+
assertThat(text(page, MESSAGE_ID)).isEmpty();
126+
assertThat(text(page, ADDITION_ID)).isEqualTo("12 + 5 = 17");
67127
}
68128

69129
@Test
70130
void resetToDefaultButtonShouldWork() throws IOException {
71131
HtmlPage page = getHtmlPage();
72-
HtmlNumberInput firstInput = firstInputNumberInput(page);
132+
HtmlNumberInput firstInput = numberInput(page, FIRST_ID);
73133
firstInput.setValueAttribute("20");
74134
fireInputEventFor(firstInput);
75-
HtmlNumberInput secondInput = secondInputNumberInput(page);
135+
HtmlNumberInput secondInput = numberInput(page, SECOND_ID);
76136
secondInput.setValueAttribute("4");
77137
fireInputEventFor(secondInput);
78138

79139
page = page.getHtmlElementById("resetButton").click();
80140

81-
assertThat(firstInputNumberInput(page).getValue()).isEqualTo("10");
82-
assertThat(secondInputNumberInput(page).getValue()).isEqualTo("5");
83-
assertThat(additionDiv(page).getTextContent()).isEqualTo("10 + 5 = 15");
84-
assertThat(subtractionDiv(page).getTextContent()).isEqualTo("10 - 5 = 5");
85-
assertThat(multiplicationDiv(page).getTextContent()).isEqualTo("10 × 5 = 50");
86-
assertThat(divisionDiv(page).getTextContent()).isEqualTo("10 ÷ 5 = 2");
141+
assertThat(numberInput(page, FIRST_ID).getValue()).isEqualTo("10");
142+
assertThat(numberInput(page, SECOND_ID).getValue()).isEqualTo("5");
143+
assertThat(text(page, MESSAGE_ID)).isEmpty();
144+
assertThat(text(page, ADDITION_ID)).isEqualTo("10 + 5 = 15");
145+
assertThat(text(page, SUBTRACTION_ID)).isEqualTo("10 - 5 = 5");
146+
assertThat(text(page, MULTIPLICATION_ID)).isEqualTo("10 × 5 = 50");
147+
assertThat(text(page, DIVISION_ID)).isEqualTo("10 ÷ 5 = 2");
87148
}
88149

89150
@Test
90151
void divisionByZeroIsHandledProperlyByShowingAMessage() throws IOException {
91152
HtmlPage page = getHtmlPage();
92-
HtmlNumberInput secondInput = secondInputNumberInput(page);
153+
HtmlNumberInput secondInput = numberInput(page, SECOND_ID);
93154

94155
secondInput.setValueAttribute("0");
95156
fireInputEventFor(secondInput);
96157

97-
assertThat(divisionDiv(page).getTextContent()).isEqualTo("Cannot divide by zero!");
158+
assertThat(text(page, DIVISION_ID)).isEqualTo("Cannot divide by zero!");
98159
}
99160

100161
private HtmlPage getHtmlPage() throws IOException {
@@ -105,27 +166,11 @@ private static void fireInputEventFor(HtmlNumberInput numberInput) {
105166
numberInput.fireEvent("input");
106167
}
107168

108-
private HtmlNumberInput secondInputNumberInput(HtmlPage page) {
109-
return page.getHtmlElementById("secondInput");
110-
}
111-
112-
private HtmlNumberInput firstInputNumberInput(HtmlPage page) {
113-
return page.getHtmlElementById("firstInput");
114-
}
115-
116-
private HtmlDivision additionDiv(HtmlPage page) {
117-
return page.getHtmlElementById("addition");
118-
}
119-
120-
private HtmlDivision subtractionDiv(HtmlPage page) {
121-
return page.getHtmlElementById("subtraction");
122-
}
123-
124-
private HtmlDivision multiplicationDiv(HtmlPage page) {
125-
return page.getHtmlElementById("multiplication");
169+
private static HtmlNumberInput numberInput(HtmlPage page, String elementId) {
170+
return page.getHtmlElementById(elementId);
126171
}
127172

128-
private HtmlDivision divisionDiv(HtmlPage page) {
129-
return page.getHtmlElementById("division");
173+
private static String text(HtmlPage page, String elementId) {
174+
return page.getHtmlElementById(elementId).getTextContent();
130175
}
131176
}

0 commit comments

Comments
 (0)