2026-05-13-report-student-header.md 7.9 KB

Report Student Header Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a consistent PDF/HTML page header to both exam sprint reports that displays the student name from StudentName, with the logo area left empty for a future asset.

Architecture: The report renderers already own final HTML generation. Add template placeholders for studentName and generatedAtText, fill them from the existing report content or payload contract, and keep escaping at the renderer boundary. The left logo slot is CSS-only empty space in both templates.

Tech Stack: Java 17 records, Spring component renderers, static HTML templates, JUnit 5, AssertJ.


Files

  • Modify: abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java
  • Modify: abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.java
  • Modify: abilities/exam-sprint/infrastructure/src/main/resources/templates/outlook-exam-sprint-report-template.html
  • Modify: abilities/exam-sprint/infrastructure/src/main/resources/templates/achievement-exam-sprint-report-template.html
  • Modify: abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java
  • Modify: abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java

Task 1: Add failing renderer tests for the shared page header

  • Step 1: Add Outlook header assertions

In ClasspathOutlookExamSprintReportRendererTest, add a test that renders callerVocabularyPayload() and asserts:

assertThat(html)
        .contains("class=\"report-header\"")
        .contains("class=\"header-logo\"")
        .contains("个人学情报告")
        .contains("class=\"header-student-name\">20260318测试</div>")
        .contains("class=\"header-generated-at\"")
        .doesNotContain("{{studentName}}")
        .doesNotContain("{{generatedAtText}}");
  • Step 2: Add Achievement header assertions

In ClasspathAchievementExamSprintReportRendererTest, add a test that renders sampleContent() and asserts:

assertThat(html)
        .contains("class=\"report-header\"")
        .contains("class=\"header-logo\"")
        .contains("个人学情报告")
        .contains("class=\"header-student-name\">测试临考</div>")
        .contains("class=\"header-generated-at\"")
        .doesNotContain("{{studentName}}")
        .doesNotContain("{{generatedAtText}}");
  • Step 3: Add escaping coverage

Extend existing escaping tests so malicious student names are escaped. For achievement, create content with studentName set to 测试<script>alert(1)</script> and assert escaped text appears. For outlook, mutate StudentName in the JsonNode to the same value and assert escaped text appears.

  • Step 4: Run focused tests and verify RED

Run:

./mvnw -pl abilities/exam-sprint/infrastructure -Dtest=ClasspathOutlookExamSprintReportRendererTest,ClasspathAchievementExamSprintReportRendererTest test

Expected: tests fail because report-header, studentName, and generatedAtText placeholders do not exist or are not replaced yet.

Task 2: Implement template placeholders and renderer replacements

  • Step 1: Add shared header markup to both templates

Insert this block immediately after <div class="report-container"> in both templates:

<header class="report-header">
    <div class="header-logo" aria-hidden="true"></div>
    <div class="header-main">
        <div class="header-report-type">个人学情报告</div>
        <div class="header-student-name">{{studentName}}</div>
    </div>
    <div class="header-generated-at">{{generatedAtText}}</div>
</header>

Add CSS before </style> in both templates:

.report-header {
    display: table;
    width: 100%;
    table-layout: fixed;
    border-bottom: 3px solid #111;
    margin-bottom: 28px;
    padding-bottom: 10px;
}

.header-logo,
.header-main,
.header-generated-at {
    display: table-cell;
    vertical-align: top;
}

.header-logo {
    width: 180px;
}

.header-main {
    text-align: center;
    color: #68768a;
}

.header-report-type {
    font-size: 13px;
    line-height: 1.5;
}

.header-student-name {
    margin-top: 4px;
    font-size: 13px;
    line-height: 1.5;
}

.header-generated-at {
    width: 260px;
    color: #68768a;
    font-size: 12px;
    line-height: 1.5;
    text-align: right;
    white-space: nowrap;
}
  • Step 2: Fill placeholders in Outlook renderer

In ClasspathOutlookExamSprintReportRenderer.render, add replacements for {{studentName}} and {{generatedAtText}} before returning the template string:

.replace("{{studentName}}", escape(payloadContract.studentName()))
.replace("{{generatedAtText}}", escape(formatGeneratedAt(generatedAt)))

Add a private formatter:

private String formatGeneratedAt(Instant generatedAt) {
    return generatedAt == null ? "" : generatedAt.toString();
}

Reuse the existing escape(String value) method in this renderer.

  • Step 3: Fill placeholders in Achievement renderer

In ClasspathAchievementExamSprintReportRenderer.placeholders, add:

placeholders.put("studentName", escape(reportContent.studentName()));
placeholders.put("generatedAtText", escape(formatGeneratedAt(generatedAt)));

Pass generatedAt into placeholders(...) from render(...). Add a private formatter:

private String formatGeneratedAt(Instant generatedAt) {
    return generatedAt == null ? "" : generatedAt.toString();
}
  • Step 4: Run focused tests and verify GREEN

Run:

./mvnw -pl abilities/exam-sprint/infrastructure -Dtest=ClasspathOutlookExamSprintReportRendererTest,ClasspathAchievementExamSprintReportRendererTest test

Expected: both renderer test classes pass.

Task 3: Final verification

  • Step 1: Run module tests

Run:

./mvnw -pl abilities/exam-sprint/infrastructure test

Expected: infrastructure module tests pass.

  • Step 2: Inspect diff

Run:

git diff -- abilities/exam-sprint/infrastructure/src/main/resources/templates/outlook-exam-sprint-report-template.html abilities/exam-sprint/infrastructure/src/main/resources/templates/achievement-exam-sprint-report-template.html abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRenderer.java abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/outlook/ClasspathOutlookExamSprintReportRendererTest.java abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRendererTest.java

Expected: only the planned header, placeholder replacement, and tests changed.

Self-review

  • Spec coverage: both reports display StudentName in the page header; logo area is empty; generated time is present; escaping is tested.
  • Placeholder scan: no TBD, TODO, or unresolved implementation placeholders in this plan.
  • Type consistency: all referenced classes and paths exist in the current project; generatedAt is already part of renderer signatures.