fix(ui): add project and task error summaries
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
- **Requirement IDs:** `UI-001`, `UI-002`, `UI-004`, `UI-009`, `I1-PLAT-06`, `I1-UI-04`
|
- **Requirement IDs:** `UI-001`, `UI-002`, `UI-004`, `UI-009`, `I1-PLAT-06`, `I1-UI-04`
|
||||||
- **Scenario IDs:** `AC-UI-001`, `AC-UI-002`, `AC-UI-005`
|
- **Scenario IDs:** `AC-UI-001`, `AC-UI-002`, `AC-UI-005`
|
||||||
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.AccountTemplateIntegrationTest`
|
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.AccountTemplateIntegrationTest`
|
||||||
- **Implementation commit:** `pending`
|
- **Implementation commits:** `7dd61b9`, `f48fc63`, `f9ddef6`
|
||||||
|
|
||||||
## Protected behavior
|
## Protected behavior
|
||||||
|
|
||||||
@@ -90,6 +90,7 @@ PostgreSQL 18.4
|
|||||||
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
|
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
|
||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
Total time: 18.361 s
|
Total time: 18.361 s
|
||||||
|
```
|
||||||
|
|
||||||
Bootstrap-specific affected suite:
|
Bootstrap-specific affected suite:
|
||||||
|
|
||||||
@@ -100,7 +101,6 @@ Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
|
|||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
Total time: 17.350 s
|
Total time: 17.350 s
|
||||||
```
|
```
|
||||||
```
|
|
||||||
|
|
||||||
## External-test boundaries
|
## External-test boundaries
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
- **Requirement IDs:** `UI-001`, `UI-002`, `UI-003`, `UI-008`, `I1-ATT-03`, `I1-UI-04`
|
- **Requirement IDs:** `UI-001`, `UI-002`, `UI-003`, `UI-008`, `I1-ATT-03`, `I1-UI-04`
|
||||||
- **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`, `AC-UI-001`, `AC-UI-005`
|
- **Scenario IDs:** `AC-ATT-003`, `AC-ATT-004`, `AC-UI-001`, `AC-UI-005`
|
||||||
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.AttendanceTemplateIntegrationTest`
|
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.AttendanceTemplateIntegrationTest`
|
||||||
- **Implementation commit:** `pending`
|
- **Implementation commit:** `3064485`
|
||||||
|
|
||||||
## Protected behavior
|
## Protected behavior
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
- **Requirement IDs:** `UI-003`, `UI-004`, `UI-007`, `UI-009`, `UI-013`, `I1-UI-04`
|
- **Requirement IDs:** `UI-003`, `UI-004`, `UI-007`, `UI-009`, `UI-013`, `I1-UI-04`
|
||||||
- **Scenario IDs:** `AC-UI-002`, `AC-UI-003`, `AC-UI-005`
|
- **Scenario IDs:** `AC-UI-002`, `AC-UI-003`, `AC-UI-005`
|
||||||
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.ProjectTaskShellContractTest#projectAndTaskPageUsesSharedDesktopShell`, `com.lab.labtimesheet.feature.project.controller.ProjectControllerTest`, `com.lab.labtimesheet.feature.task.controller.TaskControllerTest`
|
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.controller.ProjectTaskShellContractTest#projectAndTaskPageUsesSharedDesktopShell`, `com.lab.labtimesheet.feature.project.controller.ProjectControllerTest`, `com.lab.labtimesheet.feature.task.controller.TaskControllerTest`
|
||||||
- **Implementation commit:** `pending`
|
- **Implementation and final-Project integration commits:** `401f676`, `4849e0b`
|
||||||
|
|
||||||
## Protected behavior
|
## Protected behavior
|
||||||
|
|
||||||
Every Iteration 1 Project and Task page uses the same authenticated desktop shell, local assets, role-aware Project navigation, table containment, form controls, empty states, status badges, and `dd/MM/yyyy` date presentation. Existing capability-gated actions, server routes, validation, authentication, and CSRF contracts remain unchanged.
|
Every Iteration 1 Project and Task page uses the same authenticated desktop shell, local assets, role-aware Project navigation, table containment, form controls, empty states, status badges, and `dd/MM/yyyy` date presentation. Existing capability-gated actions, server routes, validation, authentication, and CSRF contracts remain unchanged.
|
||||||
|
Project and Task forms provide both an error summary and inline field errors for failed server validation.
|
||||||
|
|
||||||
## Test method
|
## Test method
|
||||||
|
|
||||||
@@ -70,7 +71,36 @@ BUILD SUCCESS
|
|||||||
Total time: 4.020 s
|
Total time: 4.020 s
|
||||||
```
|
```
|
||||||
|
|
||||||
The first affected attempt additionally caught Thymeleaf trying to resolve a `null` action fragment on five pages. Replacing `null` with Thymeleaf's empty fragment token made the identical 25-test command green.
|
The first affected attempt additionally caught Thymeleaf trying to resolve a `null` action fragment on five pages. Replacing `null` with Thymeleaf's empty fragment token made the identical 25-test command green. After merging the final Project activation pin, the one overlapping detail template retained both the shell and the capability-gated activation form; the focused Project/shell set passed 25 tests.
|
||||||
|
|
||||||
|
Final-review form-summary regression:
|
||||||
|
|
||||||
|
```text
|
||||||
|
./mvnw -Dtest=ProjectTaskShellContractTest test
|
||||||
|
RED: Tests run: 10, Failures: 2, Errors: 0, Skipped: 0
|
||||||
|
Both forms were missing #fields.hasAnyErrors() and #fields.allErrors().
|
||||||
|
|
||||||
|
./mvnw -Dtest=ProjectTaskShellContractTest,ProjectControllerTest,TaskControllerTest test
|
||||||
|
GREEN: Tests run: 29, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 4.214 s
|
||||||
|
```
|
||||||
|
|
||||||
|
## Final delivery gate
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm ci
|
||||||
|
added 34 packages; audited 35 packages; 0 vulnerabilities
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
Tailwind CSS v4.3.3: Done in 68ms
|
||||||
|
|
||||||
|
./mvnw test
|
||||||
|
PostgreSQL 18.4 via Testcontainers
|
||||||
|
Tests run: 152, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
36 Surefire reports
|
||||||
|
BUILD SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
## External-test boundaries
|
## External-test boundaries
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
- **Test type:** Web and unit
|
- **Test type:** Web and unit
|
||||||
- **Requirement IDs:** `AUTH-003`, `UI-003`, `UI-013`, `I1-UI-03`
|
- **Requirement IDs:** `AUTH-003`, `UI-003`, `UI-013`, `I1-UI-03`
|
||||||
- **Scenario IDs:** `AC-AUTH-002`, `AC-UI-005`
|
- **Scenario IDs:** `AC-AUTH-002`, `AC-UI-005`
|
||||||
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.service.DashboardServiceTest`, `com.lab.labtimesheet.feature.reporting.controller.DashboardControllerWebTest`, `com.lab.labtimesheet.feature.reporting.controller.AdminDashboardWebTest`, `com.lab.labtimesheet.feature.reporting.ReportingArchitectureTest`
|
- **Test class/method:** `com.lab.labtimesheet.feature.reporting.service.DashboardServiceTest`, `com.lab.labtimesheet.feature.reporting.controller.DashboardControllerWebTest`, `com.lab.labtimesheet.feature.reporting.controller.AdminDashboardWebTest`, `com.lab.labtimesheet.feature.reporting.controller.RoleDashboardWebIntegrationTest`, `com.lab.labtimesheet.feature.reporting.ReportingArchitectureTest`
|
||||||
- **Implementation commit:** `pending`
|
- **Implementation and integration-test commits:** `b1c6b17`, `cbdbd8e`
|
||||||
|
|
||||||
## Protected behavior
|
## Protected behavior
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
## Test method
|
## Test method
|
||||||
|
|
||||||
The unit test supplies mocked concrete public feature services to the reporting coordinator and independently checks the exact Admin, Mentor, and Intern view DTOs, including Task-status and attendance-state translation. Negative cases prove that a forged authority, locked account, missing account, or inactive internship cannot produce a dashboard. The MVC slice proves role-to-template routing and authentication. The PostgreSQL web test bootstraps a real Admin through `BootstrapService` and exercises the complete authenticated route without SQL fixtures.
|
The unit test supplies mocked concrete public feature services to the reporting coordinator and independently checks the exact Admin, Mentor, and Intern view DTOs, including Task-status and attendance-state translation. Negative cases prove that a forged authority, locked account, missing account, or inactive internship cannot produce a dashboard. The MVC slice proves role-to-template routing and authentication. PostgreSQL web tests bootstrap a real Admin and create/activate Mentor and Intern identities, SMTP configuration, a Project, and a Task only through public application services; they then exercise all three authenticated dashboard roles without repository, entity, JDBC, or SQL fixtures.
|
||||||
|
|
||||||
## Hand-derived expected result
|
## Hand-derived expected result
|
||||||
|
|
||||||
@@ -74,8 +74,16 @@ PostgreSQL 18.4 via Testcontainers
|
|||||||
Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
|
Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
|
||||||
BUILD SUCCESS
|
BUILD SUCCESS
|
||||||
Total time: 11.409 s
|
Total time: 11.409 s
|
||||||
|
|
||||||
|
Production-shaped Mentor/Intern query journey:
|
||||||
|
|
||||||
|
./mvnw -Dtest=RoleDashboardWebIntegrationTest test
|
||||||
|
PostgreSQL 18.4 via Testcontainers
|
||||||
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
Total time: 11.119 s
|
||||||
```
|
```
|
||||||
|
|
||||||
## External-test boundaries
|
## External-test boundaries
|
||||||
|
|
||||||
The focused tests prove reporting composition, route selection, denial behavior, and one production-shaped Admin journey. Feature-owned suites separately prove the Project, Task, Attendance, and Account query semantics. Browser viewport behavior remains an integrated UI gate.
|
The focused tests prove reporting composition, route selection, denial behavior, and production-shaped Admin, Mentor, and Intern journeys. Feature-owned suites separately prove additional Project, Task, Attendance, and Account query semantics. Browser viewport behavior remains an external UI boundary.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
- **Requirement IDs:** `UI-005`, `UI-006`, `UI-010`, `UI-018`, `I1-UI-02`
|
- **Requirement IDs:** `UI-005`, `UI-006`, `UI-010`, `UI-018`, `I1-UI-02`
|
||||||
- **Scenario IDs:** `AC-UI-003`, `AC-UI-005`
|
- **Scenario IDs:** `AC-UI-003`, `AC-UI-005`
|
||||||
- **Test class/method:** `com.lab.labtimesheet.ui.UiContractWebTest#themeTokensMeetTextFocusAndMeaningfulBoundaryContrast`
|
- **Test class/method:** `com.lab.labtimesheet.ui.UiContractWebTest#themeTokensMeetTextFocusAndMeaningfulBoundaryContrast`
|
||||||
- **Implementation commit:** `pending`
|
- **Implementation commit:** `3343745`
|
||||||
|
|
||||||
## Protected behavior
|
## Protected behavior
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -10,6 +10,10 @@
|
|||||||
<main>
|
<main>
|
||||||
<p class="page-description">Define the initial Project window and Leader. Membership changes remain server-authorized.</p>
|
<p class="page-description">Define the initial Project window and Leader. Membership changes remain server-authorized.</p>
|
||||||
<form class="panel form-panel form-grid" method="post" th:action="@{/projects}" th:object="${projectForm}">
|
<form class="panel form-panel form-grid" method="post" th:action="@{/projects}" th:object="${projectForm}">
|
||||||
|
<div class="alert alert-error" role="alert" th:if="${#fields.hasAnyErrors()}">
|
||||||
|
<strong>Please correct the highlighted fields.</strong>
|
||||||
|
<ul><li th:each="error : ${#fields.allErrors()}" th:text="${error}">Validation error</li></ul>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field-label" for="name">Name</label>
|
<label class="field-label" for="name">Name</label>
|
||||||
<input class="control" id="name" th:field="*{name}" required maxlength="160" th:attr="aria-invalid=${#fields.hasErrors('name')}">
|
<input class="control" id="name" th:field="*{name}" required maxlength="160" th:attr="aria-invalid=${#fields.hasErrors('name')}">
|
||||||
@@ -20,8 +24,8 @@
|
|||||||
<textarea class="control" id="description" rows="5" th:field="*{description}"></textarea>
|
<textarea class="control" id="description" rows="5" th:field="*{description}"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-grid form-grid-three">
|
<div class="form-grid form-grid-three">
|
||||||
<div class="field"><label class="field-label" for="startDate">Start date</label><input class="control" id="startDate" type="date" th:field="*{startDate}" required></div>
|
<div class="field"><label class="field-label" for="startDate">Start date</label><input class="control" id="startDate" type="date" th:field="*{startDate}" required th:attr="aria-invalid=${#fields.hasErrors('startDate')}"><p class="field-error" role="alert" th:if="${#fields.hasErrors('startDate')}" th:errors="*{startDate}">Start date error</p></div>
|
||||||
<div class="field"><label class="field-label" for="endDate">End date</label><input class="control" id="endDate" type="date" th:field="*{endDate}" required></div>
|
<div class="field"><label class="field-label" for="endDate">End date</label><input class="control" id="endDate" type="date" th:field="*{endDate}" required th:attr="aria-invalid=${#fields.hasErrors('endDate') or #fields.hasErrors('dateRangeValid')}"><p class="field-error" role="alert" th:if="${#fields.hasErrors('endDate')}" th:errors="*{endDate}">End date error</p><p class="field-error" role="alert" th:if="${#fields.hasErrors('dateRangeValid')}" th:errors="*{dateRangeValid}">Date range error</p></div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field-label" for="leader">Initial Leader user ID</label>
|
<label class="field-label" for="leader">Initial Leader user ID</label>
|
||||||
<input class="control" id="leader" type="number" min="1" th:field="*{initialLeaderUserId}" required th:attr="aria-invalid=${#fields.hasErrors('initialLeaderUserId')}">
|
<input class="control" id="leader" type="number" min="1" th:field="*{initialLeaderUserId}" required th:attr="aria-invalid=${#fields.hasErrors('initialLeaderUserId')}">
|
||||||
|
|||||||
@@ -10,11 +10,12 @@
|
|||||||
<main>
|
<main>
|
||||||
<p class="page-description">Assign work to a current eligible Project member.</p>
|
<p class="page-description">Assign work to a current eligible Project member.</p>
|
||||||
<form class="panel form-panel form-grid" method="post" th:action="@{/projects/{projectId}/tasks(projectId=${projectId})}" th:object="${taskForm}">
|
<form class="panel form-panel form-grid" method="post" th:action="@{/projects/{projectId}/tasks(projectId=${projectId})}" th:object="${taskForm}">
|
||||||
|
<div class="alert alert-error" role="alert" th:if="${#fields.hasAnyErrors()}"><strong>Please correct the highlighted fields.</strong><ul><li th:each="error : ${#fields.allErrors()}" th:text="${error}">Validation error</li></ul></div>
|
||||||
<div class="field"><label class="field-label" for="title">Title</label><input class="control" id="title" type="text" maxlength="200" required th:field="*{title}" th:attr="aria-invalid=${#fields.hasErrors('title')}"><p class="field-error" role="alert" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Title error</p></div>
|
<div class="field"><label class="field-label" for="title">Title</label><input class="control" id="title" type="text" maxlength="200" required th:field="*{title}" th:attr="aria-invalid=${#fields.hasErrors('title')}"><p class="field-error" role="alert" th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Title error</p></div>
|
||||||
<div class="field"><label class="field-label" for="description">Description</label><textarea class="control" id="description" rows="5" th:field="*{description}"></textarea></div>
|
<div class="field"><label class="field-label" for="description">Description</label><textarea class="control" id="description" rows="5" th:field="*{description}"></textarea></div>
|
||||||
<div class="form-grid form-grid-three">
|
<div class="form-grid form-grid-three">
|
||||||
<div class="field"><label class="field-label" for="assigneeMembershipId">Assignee</label><select class="control" id="assigneeMembershipId" required th:field="*{assigneeMembershipId}" th:attr="aria-invalid=${#fields.hasErrors('assigneeMembershipId')}"><option value="">Select an assignee</option><option th:each="assignee : ${assignees}" th:value="${assignee.membershipId}" th:text="${assignee.displayName}">Member</option></select><p class="field-error" role="alert" th:if="${#fields.hasErrors('assigneeMembershipId')}" th:errors="*{assigneeMembershipId}">Assignee error</p></div>
|
<div class="field"><label class="field-label" for="assigneeMembershipId">Assignee</label><select class="control" id="assigneeMembershipId" required th:field="*{assigneeMembershipId}" th:attr="aria-invalid=${#fields.hasErrors('assigneeMembershipId')}"><option value="">Select an assignee</option><option th:each="assignee : ${assignees}" th:value="${assignee.membershipId}" th:text="${assignee.displayName}">Member</option></select><p class="field-error" role="alert" th:if="${#fields.hasErrors('assigneeMembershipId')}" th:errors="*{assigneeMembershipId}">Assignee error</p></div>
|
||||||
<div class="field"><label class="field-label" for="dueDate">Due date</label><input class="control" id="dueDate" type="date" th:field="*{dueDate}"></div>
|
<div class="field"><label class="field-label" for="dueDate">Due date</label><input class="control" id="dueDate" type="date" th:field="*{dueDate}" th:attr="aria-invalid=${#fields.hasErrors('dueDate')}"><p class="field-error" role="alert" th:if="${#fields.hasErrors('dueDate')}" th:errors="*{dueDate}">Due date error</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions"><a class="button" th:href="@{/projects/{projectId}/tasks(projectId=${projectId})}">Cancel</a><button class="button button-primary" type="submit">Create Task</button></div>
|
<div class="form-actions"><a class="button" th:href="@{/projects/{projectId}/tasks(projectId=${projectId})}">Cancel</a><button class="button button-primary" type="submit">Create Task</button></div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+14
@@ -24,6 +24,16 @@ class ProjectTaskShellContractTest {
|
|||||||
.doesNotContain("<head>");
|
.doesNotContain("<head>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("projectAndTaskForms")
|
||||||
|
void projectAndTaskFormProvidesAnErrorSummary(String relativeTemplate) throws IOException {
|
||||||
|
String template = Files.readString(TEMPLATES.resolve(relativeTemplate));
|
||||||
|
|
||||||
|
assertThat(template)
|
||||||
|
.contains("#fields.hasAnyErrors()")
|
||||||
|
.contains("#fields.allErrors()");
|
||||||
|
}
|
||||||
|
|
||||||
private static Stream<String> projectAndTaskTemplates() {
|
private static Stream<String> projectAndTaskTemplates() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
"projects/list.html",
|
"projects/list.html",
|
||||||
@@ -35,4 +45,8 @@ class ProjectTaskShellContractTest {
|
|||||||
"tasks/form.html",
|
"tasks/form.html",
|
||||||
"tasks/detail.html");
|
"tasks/detail.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Stream<String> projectAndTaskForms() {
|
||||||
|
return Stream.of("projects/form.html", "tasks/form.html");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user