|
|
@@ -25,11 +25,14 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
private static final String SVG_CJK_FONT_FAMILY = "font-family=\"'MiSans VF', MiSans, ReportFont, sans-serif\"";
|
|
|
private static final Pattern SVG_START_TAG_PATTERN = Pattern.compile("<svg\\b[^>]*>");
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖官方上游词汇 payload 触发完整展望报告渲染时,应保留设计稿动态结构并输出由词汇数据计算出的核心数值。
|
|
|
+ */
|
|
|
@Test
|
|
|
void renderBuildsOutlookHtmlAlignedWithDesignDraftDynamicStructure() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
|
|
|
- String html = renderer.render(unmodeledOutlookContent(samplePayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
+ String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
|
|
|
assertThat(html)
|
|
|
.contains("class=\"analysis-table\"")
|
|
|
@@ -68,19 +71,22 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
.contains("王雷宇")
|
|
|
.contains("705词 | 高考命中:237词 | 命中率:33.8%")
|
|
|
.contains("+19分")
|
|
|
- .contains("考纲总量:<span class='highlight'>4200词</span>")
|
|
|
- .contains("已掌握:<span class='highlight'>2701词(64.31%)</span>")
|
|
|
- .contains("未掌握:<span class='highlight'>1499词(35.69%)</span>")
|
|
|
- .contains("真题总词:961词 | 生词量:847词(88.14%)")
|
|
|
- .contains("生词占比降至74.51%")
|
|
|
- .contains("基础必会词:综合掌握率62%")
|
|
|
- .contains("核心常考词:综合掌握率54%")
|
|
|
- .contains("拔高拉分词:综合掌握率41%")
|
|
|
- .contains("高频词:86.0%(优先学习)")
|
|
|
- .contains("中频词:78.0%(重点突破)")
|
|
|
- .contains("低频词:62.0%(酌情学习)");
|
|
|
+ .contains("考纲总量:<span class='highlight'>10词</span>")
|
|
|
+ .contains("已掌握:<span class='highlight'>4词(40.00%)</span>")
|
|
|
+ .contains("未掌握:<span class='highlight'>6词(60.00%)</span>")
|
|
|
+ .contains("真题总词:5词 | 生词量:3词(60.00%)")
|
|
|
+ .contains("生词占比降至60.00%")
|
|
|
+ .contains("基础必会词:综合掌握率75%")
|
|
|
+ .contains("核心常考词:综合掌握率50%")
|
|
|
+ .contains("拔高拉分词:综合掌握率25%")
|
|
|
+ .contains("高频词:56.7%(优先学习)")
|
|
|
+ .contains("中频词:47.5%(重点突破)")
|
|
|
+ .contains("低频词:30.0%(酌情学习)");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖官方上游词汇 payload 中词频与掌握度组合变化时,应复用词频区间计算逻辑生成所有图表数值。
|
|
|
+ */
|
|
|
@Test
|
|
|
void renderAdaptsCallerVocabularyPayloadIntoOutlookReportBands() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
@@ -92,6 +98,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
.contains("已掌握:<span class='highlight'>4词(40.00%)</span>")
|
|
|
.contains("未掌握:<span class='highlight'>6词(60.00%)</span>")
|
|
|
.contains("真题总词:5词 | 生词量:3词(60.00%)")
|
|
|
+ .contains("冲刺后生词:3词,生词占比降至60.00%")
|
|
|
.contains("基础必会词:综合掌握率75%")
|
|
|
.contains("核心常考词:综合掌握率50%")
|
|
|
.contains("拔高拉分词:综合掌握率25%")
|
|
|
@@ -103,33 +110,9 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
.contains("+19分");
|
|
|
}
|
|
|
|
|
|
- @Test
|
|
|
- void renderKeepsStructuredPayloadPathWhenOnlyStageVocabularyFieldIsAdded() throws Exception {
|
|
|
- ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
- ObjectNode payload = samplePayload().deepCopy();
|
|
|
- payload.put("StageVocabulary", 4200);
|
|
|
- ((ObjectNode) payload.get("highFrequencyVocabularyChart")).put("basicCorePercent", 77);
|
|
|
-
|
|
|
- String html = renderer.render(unmodeledOutlookContent(payload), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
-
|
|
|
- assertThat(html)
|
|
|
- .contains("基础必会词:综合掌握率77%")
|
|
|
- .contains("高频词:86.0%(优先学习)");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void renderSupportsLegacyHighScorePercentForStructuredOutlookPayload() throws Exception {
|
|
|
- ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
-
|
|
|
- String html = renderer.render(unmodeledOutlookContent(legacyStructuredPayloadWithHighScorePercent()),
|
|
|
- Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
-
|
|
|
- assertThat(html)
|
|
|
- .contains("基础必会词:综合掌握率77%")
|
|
|
- .contains("核心常考词:综合掌握率62%")
|
|
|
- .contains("拔高拉分词:综合掌握率62%");
|
|
|
- }
|
|
|
-
|
|
|
+ /**
|
|
|
+ * 覆盖官方词汇 payload 计算出 0 值区间时,对应柱状图高度应保持为 0 且不被最小高度兜底抬高。
|
|
|
+ */
|
|
|
@Test
|
|
|
void renderUsesZeroHeightForZeroValueBarsWithoutForcingMinimumHeight() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
@@ -144,25 +127,30 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
.contains("低频词:0.0%(酌情学习)");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖官方上游词汇 payload 渲染三组柱状图时,应为图表补充可见坐标轴刻度。
|
|
|
+ */
|
|
|
@Test
|
|
|
void renderAddsAxisTicksToThreeBarCharts() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
|
|
|
- String html = renderer.render(unmodeledOutlookContent(samplePayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
+ String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
|
|
|
assertThat(html)
|
|
|
.contains("class='chart-axis-tick chart-axis-tick-y'")
|
|
|
.contains("class='chart-axis-tick chart-axis-tick-x'")
|
|
|
- .containsPattern("<text class='chart-axis-tick-label chart-axis-tick-label-y' x='26' y='54' text-anchor='end' fill='#7f8b97' font-size='11'>1000</text>")
|
|
|
.containsPattern("<text class='chart-axis-tick-label chart-axis-tick-label-y' x='26' y='54' text-anchor='end' fill='#7f8b97' font-size='11'>100</text>")
|
|
|
- .containsPattern("<text class='chart-axis-tick-label chart-axis-tick-label-y' x='26' y='54' text-anchor='end' fill='#7f8b97' font-size='11'>100</text>");
|
|
|
+ .containsPattern("<text class='chart-axis-tick-label chart-axis-tick-label-y' x='26' y='54' text-anchor='end' fill='#7f8b97' font-size='11'>250</text>");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖官方上游词汇 payload 渲染所有内联 SVG 时,每个 SVG 起始标签都应声明 Batik 可识别的 CJK 字体族。
|
|
|
+ */
|
|
|
@Test
|
|
|
void renderDeclaresBatikCjkFontFamilyOnEveryInlineSvg() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
|
|
|
- String html = renderer.render(unmodeledOutlookContent(samplePayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
+ String html = renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
|
|
|
assertThat(html)
|
|
|
.contains("<svg class='syllabus-donut-chart' font-family=\"'MiSans VF', MiSans, ReportFont, sans-serif\"")
|
|
|
@@ -173,6 +161,9 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
assertThat(svgStartTags(html)).hasSize(5).allSatisfy(svg -> assertThat(svg).contains(SVG_CJK_FONT_FAMILY));
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖 renderer 类型判断时,仅 OUTLOOK 报告类型应被该 renderer 支持。
|
|
|
+ */
|
|
|
@Test
|
|
|
void supportsOnlyOutlookReportType() {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
@@ -181,16 +172,22 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
assertThat(renderer.supports(ReportType.ACHIEVEMENT)).isFalse();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖渲染官方上游词汇 payload 时,应使用构造器注入的 ObjectMapper 完成唯一 public contract 反序列化。
|
|
|
+ */
|
|
|
@Test
|
|
|
void renderUsesInjectedObjectMapperForPayloadDeserialization() throws Exception {
|
|
|
TrackingObjectMapper objectMapper = new TrackingObjectMapper();
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(objectMapper);
|
|
|
|
|
|
- renderer.render(unmodeledOutlookContent(samplePayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
+ renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
|
|
|
assertThat(objectMapper.treeToValueCalled).isTrue();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖渲染官方上游词汇 payload 并加载 classpath 模板时,模板输入流应在渲染结束后关闭。
|
|
|
+ */
|
|
|
@Test
|
|
|
void renderClosesTemplateInputStreamAfterLoadingClasspathTemplate() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
@@ -200,7 +197,7 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
try {
|
|
|
Thread.currentThread().setContextClassLoader(trackingClassLoader);
|
|
|
|
|
|
- renderer.render(unmodeledOutlookContent(samplePayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
+ renderer.render(unmodeledOutlookContent(callerVocabularyPayload()), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
} finally {
|
|
|
Thread.currentThread().setContextClassLoader(originalClassLoader);
|
|
|
}
|
|
|
@@ -208,62 +205,53 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
assertThat(trackingClassLoader.inputStream.closed).isTrue();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖官方 payload 中当前不展示的调用方文本字段包含脚本时,渲染结果不应暴露这些外部注入文本且静态阶段文案保持不变。
|
|
|
+ */
|
|
|
@Test
|
|
|
- void renderEscapesHtmlSensitiveCharactersAndScriptPayloads() throws Exception {
|
|
|
+ void renderDoesNotExposeCallerControlledTextFieldsThatAreNotPartOfHtmlView() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
|
|
|
- JsonNode payload = payloadWithEscapingSamples();
|
|
|
+ JsonNode payload = payloadWithCallerControlledTextSamples();
|
|
|
String html = renderer.render(unmodeledOutlookContent(payload), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
|
|
|
assertThat(html)
|
|
|
- .contains("李<同学>")
|
|
|
- .contains("春季&高考英语")
|
|
|
- .contains("Ability "Bot"")
|
|
|
- .contains("O'Brien")
|
|
|
- .contains("<script>alert(1)</script>")
|
|
|
+ .contains("考前半个月·核心突击期")
|
|
|
+ .contains("考前半小时·临阵巩固期")
|
|
|
+ .doesNotContain("注入学生")
|
|
|
+ .doesNotContain("evil-word")
|
|
|
+ .doesNotContain("注入文章")
|
|
|
.doesNotContain("<script>alert(1)</script>")
|
|
|
.doesNotContain("<script>");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖官方 payload 计算出的考纲掌握率达到 100% 时,考纲进度图应使用完整圆而非弧线。
|
|
|
+ */
|
|
|
@Test
|
|
|
- void renderFallsBackToDefaultThemeColorWhenPayloadColorIsInvalid() throws Exception {
|
|
|
+ void renderUsesCircleForSyllabusProgressWhenPercentReachesHundred() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
|
|
|
- JsonNode payload = payloadWithInvalidThemeColors();
|
|
|
- String html = renderer.render(unmodeledOutlookContent(payload), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
-
|
|
|
- assertThat(html)
|
|
|
- .containsPattern("class='chart-column high-band-column'[^>]*fill='#448aff'")
|
|
|
- .containsPattern("class='chart-column mid-band-column'[^>]*fill='#448aff'")
|
|
|
- .containsPattern("class='chart-column low-band-column'[^>]*fill='#448aff'")
|
|
|
- .doesNotContain("onload=")
|
|
|
- .doesNotContain("javascript:alert(1)");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void renderUsesCircleForSyllabusAndCaseProgressWhenPercentReachesHundred() throws Exception {
|
|
|
- ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
-
|
|
|
- JsonNode payload = payloadWithProgressPercent(100, 100.0);
|
|
|
+ JsonNode payload = payloadWithSyllabusProgressPercent(100);
|
|
|
String html = renderer.render(unmodeledOutlookContent(payload), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
|
|
|
assertThat(html)
|
|
|
.contains("class='donut-mastered-full-circle'")
|
|
|
- .contains("class='case-progress-full-circle'")
|
|
|
- .doesNotContain("class='donut-mastered-arc'")
|
|
|
- .doesNotContain("class='case-progress-arc'");
|
|
|
+ .doesNotContain("class='donut-mastered-arc'");
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 覆盖官方 payload 计算出的考纲掌握率为 0% 时,考纲已掌握弧线应被省略且未掌握弧线仍存在。
|
|
|
+ */
|
|
|
@Test
|
|
|
- void renderSkipsProgressArcWhenSyllabusAndCasePercentAreZeroOrBelow() throws Exception {
|
|
|
+ void renderSkipsProgressArcWhenSyllabusPercentIsZeroOrBelow() throws Exception {
|
|
|
ClasspathOutlookExamSprintReportRenderer renderer = new ClasspathOutlookExamSprintReportRenderer(OBJECT_MAPPER);
|
|
|
|
|
|
- JsonNode payload = payloadWithProgressPercent(0, 0.0);
|
|
|
+ JsonNode payload = payloadWithSyllabusProgressPercent(0);
|
|
|
String html = renderer.render(unmodeledOutlookContent(payload), Instant.parse("2026-01-03T08:00:00Z"));
|
|
|
|
|
|
assertThat(html)
|
|
|
.doesNotContain("class='donut-mastered-arc'")
|
|
|
- .doesNotContain("class='case-progress-arc'")
|
|
|
.contains("class='donut-unmastered-arc'");
|
|
|
}
|
|
|
|
|
|
@@ -308,107 +296,6 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private JsonNode samplePayload() throws Exception {
|
|
|
- return OBJECT_MAPPER.readTree("""
|
|
|
- {
|
|
|
- "reportMetadata": {
|
|
|
- "reportVersionLabel": "2026 词汇展望报告",
|
|
|
- "learnerName": "李同学",
|
|
|
- "targetExamName": "春季高考英语",
|
|
|
- "sprintPeriodLabel": "30 天考前冲刺",
|
|
|
- "authorName": "Ability Bot"
|
|
|
- },
|
|
|
- "readinessOverview": {
|
|
|
- "summary": "基础较稳,具备短期冲刺提分空间。",
|
|
|
- "currentStage": "冲刺提升期",
|
|
|
- "keyInsight": "高频与常考词群是提分关键。",
|
|
|
- "readinessScore": 72
|
|
|
- },
|
|
|
- "syllabusMasteryChart": {
|
|
|
- "totalWordCount": 4200,
|
|
|
- "masteredWordCount": 2701,
|
|
|
- "unmasteredWordCount": 1499,
|
|
|
- "masteryPercent": 64,
|
|
|
- "summaryLabel": "考纲词汇掌握概览",
|
|
|
- "recommendation": "优先补齐高考核心场景词。"
|
|
|
- },
|
|
|
- "pastPaperVocabularyChart": {
|
|
|
- "totalWordCount": 961,
|
|
|
- "unknownWordCountBeforeSprint": 847,
|
|
|
- "unknownWordCountAfterSprint": 716,
|
|
|
- "projectedScoreGainLabel": "预计提分5-15分",
|
|
|
- "recommendation": "先压降真题生词占比。"
|
|
|
- },
|
|
|
- "highFrequencyVocabularyChart": {
|
|
|
- "basicCorePercent": 62,
|
|
|
- "frequentCorePercent": 54,
|
|
|
- "advancedScorePercent": 41,
|
|
|
- "highlightLabel": "拉分词是提分核心突破项"
|
|
|
- },
|
|
|
- "vocabularyFrequencyBandChart": {
|
|
|
- "bars": [
|
|
|
- {"bandLabel": "高频词", "currentValue": 86.0, "priorityLabel": "优先学习", "themeColor": "#448aff"},
|
|
|
- {"bandLabel": "中频词", "currentValue": 78.0, "priorityLabel": "重点突破", "themeColor": "#4caf50"},
|
|
|
- {"bandLabel": "低频词", "currentValue": 62.0, "priorityLabel": "酌情学习", "themeColor": "#ff9800"}
|
|
|
- ]
|
|
|
- },
|
|
|
- "frequencyPlan": {
|
|
|
- "cards": [
|
|
|
- {
|
|
|
- "cadencePerWeek": 1,
|
|
|
- "scoreGainLabel": "+5分",
|
|
|
- "winRatePercent": 38,
|
|
|
- "recommended": false,
|
|
|
- "badgeLabel": "稳健",
|
|
|
- "emphasisIcon": "①"
|
|
|
- },
|
|
|
- {
|
|
|
- "cadencePerWeek": 2,
|
|
|
- "scoreGainLabel": "+10分",
|
|
|
- "winRatePercent": 55,
|
|
|
- "recommended": false,
|
|
|
- "badgeLabel": "均衡",
|
|
|
- "emphasisIcon": "②"
|
|
|
- },
|
|
|
- {
|
|
|
- "cadencePerWeek": 3,
|
|
|
- "scoreGainLabel": "+15分",
|
|
|
- "winRatePercent": 72,
|
|
|
- "recommended": true,
|
|
|
- "badgeLabel": "推荐",
|
|
|
- "emphasisIcon": "③"
|
|
|
- },
|
|
|
- {
|
|
|
- "cadencePerWeek": 5,
|
|
|
- "scoreGainLabel": "+20分",
|
|
|
- "winRatePercent": 88,
|
|
|
- "recommended": false,
|
|
|
- "badgeLabel": "冲刺",
|
|
|
- "emphasisIcon": "④"
|
|
|
- }
|
|
|
- ],
|
|
|
- "recommendationTitle": "💡建议策略",
|
|
|
- "recommendationSummary": "7 天提分冲刺是首选节奏,按词频优先级记忆,不浪费时间;只攻克高频/中频核心词,2周15小时速记500-800必考词,快速缩小生词缺口。",
|
|
|
- "phaseSuggestions": [
|
|
|
- {"title": "考前半个月·核心突击期", "description": "围绕高频词建立记忆闭环。"},
|
|
|
- {"title": "考前半小时·临阵巩固期", "description": "结合真题词做循环巩固。"}
|
|
|
- ]
|
|
|
- },
|
|
|
- "scoreImprovementCaseStudy": {
|
|
|
- "headline": "真实提分 · 效果可复制",
|
|
|
- "learnerName": "王雷宇",
|
|
|
- "studyPeriodLabel": "考前半个月·核心突击期",
|
|
|
- "memorizedWordCount": 705,
|
|
|
- "examHitWordCount": 237,
|
|
|
- "hitRatePercent": 33.8,
|
|
|
- "baselineScoreLabel": "70分以下",
|
|
|
- "finalScore": 89,
|
|
|
- "scoreGain": 19
|
|
|
- }
|
|
|
- }
|
|
|
- """);
|
|
|
- }
|
|
|
-
|
|
|
private JsonNode callerVocabularyPayload() throws Exception {
|
|
|
return OBJECT_MAPPER.readTree("""
|
|
|
{
|
|
|
@@ -444,16 +331,6 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
""");
|
|
|
}
|
|
|
|
|
|
- private JsonNode legacyStructuredPayloadWithHighScorePercent() throws Exception {
|
|
|
- ObjectNode payload = samplePayload().deepCopy();
|
|
|
- ObjectNode highFrequencyVocabularyChart = (ObjectNode) payload.get("highFrequencyVocabularyChart");
|
|
|
- highFrequencyVocabularyChart.put("basicCorePercent", 77);
|
|
|
- highFrequencyVocabularyChart.remove("frequentCorePercent");
|
|
|
- highFrequencyVocabularyChart.remove("advancedScorePercent");
|
|
|
- highFrequencyVocabularyChart.put("highScorePercent", 62);
|
|
|
- return payload;
|
|
|
- }
|
|
|
-
|
|
|
private JsonNode callerVocabularyPayloadWithZeroValueBars() throws Exception {
|
|
|
ObjectNode payload = (ObjectNode) callerVocabularyPayload();
|
|
|
payload.withArray("StudentWordsLatest").forEach(node -> {
|
|
|
@@ -478,47 +355,22 @@ class ClasspathOutlookExamSprintReportRendererTest {
|
|
|
return svgStartTags;
|
|
|
}
|
|
|
|
|
|
- private JsonNode payloadWithEscapingSamples() throws Exception {
|
|
|
- ObjectNode payload = samplePayload().deepCopy();
|
|
|
-
|
|
|
- ObjectNode frequencyPlan = (ObjectNode) payload.get("frequencyPlan");
|
|
|
- ObjectNode firstPhaseSuggestion = (ObjectNode) frequencyPlan.withArray("phaseSuggestions").get(0);
|
|
|
- firstPhaseSuggestion.put("title", "春季&高考英语");
|
|
|
- frequencyPlan.put("recommendationSummary", "<script>alert(1)</script>");
|
|
|
-
|
|
|
- ObjectNode scoreImprovementCaseStudy = (ObjectNode) payload.get("scoreImprovementCaseStudy");
|
|
|
- scoreImprovementCaseStudy.put("learnerName", "李<同学>");
|
|
|
- scoreImprovementCaseStudy.put("headline", "Ability \"Bot\"");
|
|
|
- scoreImprovementCaseStudy.put("baselineScoreLabel", "O'Brien");
|
|
|
-
|
|
|
- return payload;
|
|
|
- }
|
|
|
-
|
|
|
- private JsonNode payloadWithInvalidThemeColors() throws Exception {
|
|
|
- ObjectNode payload = samplePayload().deepCopy();
|
|
|
- payload.with("vocabularyFrequencyBandChart").withArray("bars").forEach(node -> {
|
|
|
- ObjectNode bar = (ObjectNode) node;
|
|
|
- if ("高频词".equals(bar.path("bandLabel").asText())) {
|
|
|
- bar.put("themeColor", "\" onload=\"alert(1)");
|
|
|
- return;
|
|
|
- }
|
|
|
- if ("中频词".equals(bar.path("bandLabel").asText())) {
|
|
|
- bar.put("themeColor", "url(javascript:alert(1))");
|
|
|
- return;
|
|
|
- }
|
|
|
- bar.put("themeColor", "#12");
|
|
|
- });
|
|
|
+ private JsonNode payloadWithCallerControlledTextSamples() throws Exception {
|
|
|
+ ObjectNode payload = (ObjectNode) callerVocabularyPayload();
|
|
|
+ payload.put("StudentName", "注入学生<script>alert(1)</script>");
|
|
|
+ payload.put("StageName", "注入阶段<script>alert(1)</script>");
|
|
|
+ payload.put("StageExaminName", "注入考试<script>alert(1)</script>");
|
|
|
+ payload.put("TestPaperTitle", "注入文章<script>alert(1)</script>");
|
|
|
+ ((ObjectNode) payload.withArray("StudentWordsLatest").get(0)).put("WordSpell", "evil-word<script>alert(1)</script>");
|
|
|
return payload;
|
|
|
}
|
|
|
|
|
|
- private JsonNode payloadWithProgressPercent(int masteryPercent, double hitRatePercent) throws Exception {
|
|
|
- ObjectNode payload = samplePayload().deepCopy();
|
|
|
- ObjectNode syllabusMasteryChart = (ObjectNode) payload.get("syllabusMasteryChart");
|
|
|
- syllabusMasteryChart.put("totalWordCount", 100);
|
|
|
- syllabusMasteryChart.put("masteredWordCount", Math.max(0, Math.min(100, masteryPercent)));
|
|
|
- syllabusMasteryChart.put("unmasteredWordCount", 100 - Math.max(0, Math.min(100, masteryPercent)));
|
|
|
- syllabusMasteryChart.put("masteryPercent", masteryPercent);
|
|
|
- ((ObjectNode) payload.get("scoreImprovementCaseStudy")).put("hitRatePercent", hitRatePercent);
|
|
|
+ private JsonNode payloadWithSyllabusProgressPercent(int masteryPercent) throws Exception {
|
|
|
+ ObjectNode payload = (ObjectNode) callerVocabularyPayload();
|
|
|
+ int boundedMasteryPercent = Math.max(0, Math.min(100, masteryPercent));
|
|
|
+ payload.put("StageVocabulary", 100);
|
|
|
+ payload.put("MastedWordCount", boundedMasteryPercent);
|
|
|
+ payload.put("UnMastedWordCount", 100 - boundedMasteryPercent);
|
|
|
return payload;
|
|
|
}
|
|
|
}
|