package cn.iocoder.yudao.module.qcreport.engine.render;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 画布安全性校验测试。
*
* 两类断言各守一件事:
*
* - 安全:凡是渲染时会原样拼进产物的脏内容(标签名、属性名、整段 CSS)都要被拦下,
* 否则模板作者就能往报告里塞脚本或提前闭合 {@code "
}"""));
assertEquals(1, problems.size());
assertTrue(problems.get(0).contains("整段 CSS"), problems.get(0));
}
@Test
@DisplayName("样式规则里出现「<」被拒:选择器、属性值、媒体查询条件都算")
void rejectsAngleBracketInStyleFragments() {
List bySelector = CanvasSafety.validate(grapes("""
{ "pages": [ { "frames": [ { "component": { "type": "wrapper", "components": [] } } ] } ],
"styles": [ { "selectors": [ "a" ], "style": { "color": "red" } } ] }"""));
assertTrue(bySelector.stream().anyMatch(p -> p.contains("选择器")), bySelector.toString());
List byValue = CanvasSafety.validate(grapes("""
{ "pages": [ { "frames": [ { "component": { "type": "wrapper", "components": [] } } ] } ],
"styles": [ { "selectors": [ "p" ], "style": { "content": "" } } ] }"""));
assertTrue(byValue.stream().anyMatch(p -> p.contains("content")), byValue.toString());
List byMedia = CanvasSafety.validate(grapes("""
{ "pages": [ { "frames": [ { "component": { "type": "wrapper", "components": [] } } ] } ],
"styles": [ { "selectors": [ "p" ], "mediaText": "printx", "style": { "color": "red" } } ] }"""));
assertTrue(byMedia.stream().anyMatch(p -> p.contains("媒体查询")), byMedia.toString());
}
@Test
@DisplayName("textnode 上的 tagName 不参与校验:渲染器根本不读它,不该因此拦住保存")
void ignoresTagNameOfTextNode() {
assertEquals(List.of(), CanvasSafety.validate(canvas("""
{ "type": "wrapper", "components": [
{ "type": "textnode", "tagName": "script", "content": "纯文本" } ] }""")));
}
@Test
@DisplayName("只沿 components 走:GrapesJS 挂在节点上的 docEl / head 元数据不触发误判")
void ignoresNonRenderedMetadata() {
// 真实画布里 frame 的根组件上就挂着 docEl:{tagName:"html"} 与 head:{type:"head"},渲染器两个都不读
assertEquals(List.of(), CanvasSafety.validate(grapes("""
{ "pages": [ { "frames": [ { "component": {
"type": "wrapper",
"docEl": { "tagName": "html" },
"head": { "type": "head", "components": [ { "type": "text", "tagName": "style" } ] },
"components": [ { "type": "text", "tagName": "p", "components": [] } ] } } ] } ] }""")));
}
@Test
@DisplayName("问题条数有上限:脏内容很多时也不要吐一屏提示")
void capsProblemCount() {
StringBuilder components = new StringBuilder();
for (int i = 0; i < 20; i++) {
components.append(i == 0 ? "" : ",").append("{ \"type\": \"text\", \"tagName\": \"script\" }");
}
List problems = CanvasSafety.validate(canvas(
"{ \"type\": \"wrapper\", \"components\": [ " + components + " ] }"));
assertEquals(8, problems.size(), "上限是 8 条,改完再存一次就能看到下一批");
}
@Test
@DisplayName("grapes 为空不报错(新建模板还没画东西)")
void toleratesNullGrapes() {
assertEquals(List.of(), CanvasSafety.validate(Map.of()));
assertEquals(List.of(), CanvasSafety.validate(Map.of("pages", List.of())));
}
/** 把用例写的根 wrapper 包成真实画布形状:grapes.pages[0].frames[0].component */
private static Map canvas(String wrapperJson) {
return grapes("{\"pages\":[{\"frames\":[{\"component\":" + wrapperJson + "}]}]}");
}
private static Map grapes(String grapesJson) {
try {
return MAPPER.readValue(grapesJson, new TypeReference