From efd3cb30f9a0cea0b437b704ffc633c86f03cdf5 Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 28 Sep 2026 14:04:38 +0800 Subject: [PATCH] =?UTF-8?q?[CMS-VISUAL]=20D=20=E6=B5=8B=E8=AF=95:=20Portal?= =?UTF-8?q?DComponentsIT=205=20=E7=94=A8=E4=BE=8B=EF=BC=88=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=E6=B8=B2=E6=9F=93/topic/event/=E7=A9=BA=E6=80=81=20R-?= =?UTF-8?q?44/=E6=97=A5=E6=9C=9F=E6=8E=92=E5=BA=8F/=E5=BD=A2=E7=8A=B6?= =?UTF-8?q?=E5=8F=82=E6=95=B0=EF=BC=89+=20PersonServiceIT=204=20+=20HomeLa?= =?UTF-8?q?yout=20=E6=A0=A1=E9=AA=8C=202=20+=20purgeTables=20=E6=B8=85=20c?= =?UTF-8?q?ms=5Fperson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portal/PortalPageRendererTest.java | 6 +- .../cms/portal/PortalDComponentsIT.java | 199 ++++++++++++++++++ .../yunhai/cms/portal/PortalHomeLayoutIT.java | 2 +- .../cms/service/impl/PersonServiceIT.java | 110 ++++++++++ .../service/impl/SiteServiceHomeLayoutIT.java | 29 +++ .../cms/support/CmsIntegrationTestBase.java | 5 +- 6 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/yunhai/cms/portal/PortalDComponentsIT.java create mode 100644 src/test/java/com/yunhai/cms/service/impl/PersonServiceIT.java diff --git a/src/test/java/com/yunhai/cms/controller/portal/PortalPageRendererTest.java b/src/test/java/com/yunhai/cms/controller/portal/PortalPageRendererTest.java index 0a40caa..cb9388e 100644 --- a/src/test/java/com/yunhai/cms/controller/portal/PortalPageRendererTest.java +++ b/src/test/java/com/yunhai/cms/controller/portal/PortalPageRendererTest.java @@ -34,7 +34,7 @@ class PortalPageRendererTest { // 再用真实路径就让 .exists() 变真,用例守的"缺模板整页回退"语义就丢了。 when(registry.resource("scholar-blue", "service.html")) .thenReturn(new ClassPathResource("templates/themes/scholar-blue/__absent__.html")); - PortalPageRenderer renderer = new PortalPageRenderer(null, registry); + PortalPageRenderer renderer = new PortalPageRenderer(null, registry, null); assertThat(renderer.resolveEffectiveTheme(siteOf("scholar-blue"), "service.html")) .isEqualTo(ThemeRegistry.DEFAULT_THEME); @@ -46,7 +46,7 @@ class PortalPageRendererTest { ThemeRegistry registry = mock(ThemeRegistry.class); when(registry.resource("scholar-blue", "service.html")) .thenReturn(new ClassPathResource("templates/themes/scholar-blue/home.html")); - PortalPageRenderer renderer = new PortalPageRenderer(null, registry); + PortalPageRenderer renderer = new PortalPageRenderer(null, registry, null); assertThat(renderer.resolveEffectiveTheme(siteOf("scholar-blue"), "service.html")) .isEqualTo("scholar-blue"); @@ -58,7 +58,7 @@ class PortalPageRendererTest { ThemeRegistry registry = mock(ThemeRegistry.class); when(registry.resource("default", "guide.html")) .thenReturn(new ClassPathResource("templates/themes/default/__none__.html")); - PortalPageRenderer renderer = new PortalPageRenderer(null, registry); + PortalPageRenderer renderer = new PortalPageRenderer(null, registry, null); assertThat(renderer.resolveEffectiveTheme(siteOf("default"), "guide.html")) .isEqualTo(ThemeRegistry.DEFAULT_THEME); diff --git a/src/test/java/com/yunhai/cms/portal/PortalDComponentsIT.java b/src/test/java/com/yunhai/cms/portal/PortalDComponentsIT.java new file mode 100644 index 0000000..6ef4dc0 --- /dev/null +++ b/src/test/java/com/yunhai/cms/portal/PortalDComponentsIT.java @@ -0,0 +1,199 @@ +package com.yunhai.cms.portal; + +import com.yunhai.cms.entity.CmsBanner; +import com.yunhai.cms.entity.CmsPerson; +import com.yunhai.cms.entity.CmsSite; +import com.yunhai.cms.enums.ArticleStatusEnum; +import com.yunhai.cms.enums.CategoryTypeEnum; +import com.yunhai.cms.repository.CmsPersonRepository; +import com.yunhai.cms.support.CmsIntegrationTestBase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * docs/20 D 组件库 SSR 契约(campus-glow 皮肤 = 白名单外新类型落 common 骨架兜底): + * D-1 stats_band(count-up 数据位)/ D-2 quick_links 图标宫格 + 首字兜底 / D-3 featured_topics(topic 位) + * / D-5 video_tour / D-6 events(event 位 + 大日期块)/ D-7 people(cms_person) + * / D-8 gallery masonry / D-9 badges。 + * 空数据区块整段不渲染(R-44);动效/预览脚本随布局模式注入(R-41/R-42)。 + */ +@DisplayName("Portal D-Components - 首页新组件渲染(docs/20 D-1~D-9)") +class PortalDComponentsIT extends CmsIntegrationTestBase { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private CmsPersonRepository personRepository; + + private Integer siteId; + private Integer catId; + + @BeforeEach + void seed() { + purgeTables(); + CmsSite site = persistSite("dcomp", 1, "组件测试门户"); + site.setTheme("campus-glow"); + siteId = site.getId(); + catId = persistCategory(siteId, 0, "校园时讯", CategoryTypeEnum.LIST).getId(); + persistArticle(siteId, catId, "时讯文章一", ArticleStatusEnum.PUBLISHED, nowMinusSeconds(60)); + + // D-3/D-6:banner topic/event 位(扩展列) + CmsBanner topic = persistBanner(siteId, "topic", "2026 招生专题", null, null); + topic.setDescription("欢迎报考本站"); + topic.setCtaText("进入专题"); + topic.setCtaUrl("https://zs.example.edu.cn"); + bannerRepository.saveAndFlush(topic); + CmsBanner event = persistBanner(siteId, "event", "院士讲坛", null, null); + event.setEventDate(LocalDateTime.of(2026, 10, 15, 14, 0)); + event.setLocation("图书馆报告厅"); + bannerRepository.saveAndFlush(event); + + // D-7:人物(有头像 + 无头像首字兜底) + CmsPerson p1 = new CmsPerson(); + p1.setSiteId(siteId); + p1.setName("王立群"); + p1.setTitle("教授"); + p1.setBio("古典文学研究专家"); + p1.setAvatar("/upload/cms/avatar-wang.png"); + p1.setSortNo(1); + personRepository.saveAndFlush(p1); + CmsPerson p2 = new CmsPerson(); + p2.setSiteId(siteId); + p2.setName("李晓"); + p2.setTitle("长江学者"); + p2.setSortNo(2); + personRepository.saveAndFlush(p2); + + clearCaches(); + } + + @AfterEach + void clean() { + purgeTables(); + } + + private String body(MvcResult result) throws java.io.UnsupportedEncodingException { + return result.getResponse().getContentAsString(StandardCharsets.UTF_8); + } + + private void setConfig(String configJson) { + siteRepository.findByCode("dcomp").ifPresent(s -> { + s.setConfigJson(configJson); + siteRepository.saveAndFlush(s); + }); + clearCaches(); + } + + private String home() throws Exception { + return body(mockMvc.perform(get("/portal/dcomp/")).andExpect(status().isOk()).andReturn()); + } + + @Test + @DisplayName("D-1/2/5/7/8/9:骨架兜底渲染齐备(count-up 数据位/图标宫格/视频封面/人物卡/masonry/徽章)+ 动效脚本注入") + void skeleton_rendersAllNewComponents() throws Exception { + setConfig("{\"home_layout\":{\"version\":1,\"blocks\":[" + + "{\"id\":\"s1\",\"type\":\"stats_band\",\"title\":\"数说学校\",\"stats\":[" + + "{\"label\":\"在校生\",\"value\":\"3.2\",\"unit\":\"万\"}," + + "{\"label\":\"院士\",\"value\":\"12\"}]}," + + "{\"id\":\"q1\",\"type\":\"quick_links\",\"items\":[" + + "{\"name\":\"办事大厅\",\"url\":\"/service\",\"icon\":\"banshi\"}," + + "{\"name\":\"图书馆\",\"url\":\"https://lib.example.edu.cn\"}]}," + + "{\"id\":\"v1\",\"type\":\"video_tour\",\"title\":\"云端校园\",\"cover\":\"/upload/cms/vr-cover.png\",\"url\":\"https://vr.example.edu.cn\"}," + + "{\"id\":\"p1\",\"type\":\"people\",\"title\":\"名师风采\"}," + + "{\"id\":\"g1\",\"type\":\"gallery\",\"layout\":\"masonry\"}," + + "{\"id\":\"b1\",\"type\":\"badges\",\"badgeItems\":[{\"label\":\"软科排名\",\"desc\":\"前100强\",\"image\":\"/upload/cms/badge.png\"}]}" + + "]}}"); + String html = home(); + + // D-1:计数带 + count-up 数据位 + assertThat(html).contains("数说学校").contains("在校生") + .contains("data-plb-countup=\"3.2\"").contains("院士"); + // D-2:图标宫格——选中图标渲染 SVG(icon fragment),未选走首字徽章(图名文本存在) + assertThat(html).contains("办事大厅").contains("plb-qicon"); + assertThat(html).contains("图书馆"); + // D-5:视频封面 + 播放按钮 + assertThat(html).contains("plb-video").contains("vr.example.edu.cn"); + // D-7:人物(有头像 img + 无头像首字「李」徽章文本) + assertThat(html).contains("王立群").contains("古典文学研究专家").contains("李晓"); + // D-8:masonry 布局类 + assertThat(html).contains("masonry"); + // D-9:徽章 + assertThat(html).contains("软科排名").contains("前100强"); + // R-42:骨架共享动效脚本注入 + assertThat(html).contains("__cmsMotionBound"); + // R-41:预览监听脚本注入 + assertThat(html).contains("__cmsLayoutPreviewBound"); + // F2:区块 id 锚点 + assertThat(html).contains("data-plb-id=\"s1\""); + } + + @Test + @DisplayName("D-3/D-6:topic/event 位数据渲染(专题描述/CTA + 大日期块/地点)") + void topicAndEvents_renderFromBannerPositions() throws Exception { + setConfig("{\"home_layout\":{\"version\":1,\"blocks\":[" + + "{\"id\":\"t1\",\"type\":\"featured_topics\",\"title\":\"专题推荐\"}," + + "{\"id\":\"e1\",\"type\":\"events\",\"title\":\"学术活动\"}" + + "]}}"); + String html = home(); + + // D-3:专题卡(banner topic 位扩展列) + assertThat(html).contains("2026 招生专题").contains("欢迎报考本站") + .contains("进入专题").contains("zs.example.edu.cn"); + // D-6:活动(banner event 位 + event_date/location) + assertThat(html).contains("院士讲坛").contains("图书馆报告厅").contains("15"); + } + + @Test + @DisplayName("R-44:空数据区块整段不渲染(无 stats/people/badges 数据 = 无对应 markup,不留空白占位)") + void emptyData_blocksNotRendered() throws Exception { + // people 用真实但无人物挂载的栏目构造空态(树外 id 会被防注入守卫忽略=显示全部) + setConfig("{\"home_layout\":{\"version\":1,\"blocks\":[" + + "{\"id\":\"s1\",\"type\":\"stats_band\",\"title\":\"数说学校\"}," // 未配 stats + + "{\"id\":\"p1\",\"type\":\"people\",\"categoryId\":" + catId + "}," + + "{\"id\":\"x1\",\"type\":\"badges\",\"badgeItems\":[]}" // 空徽章 + + "]}}"); + String html = home(); + assertThat(html).doesNotContain("数说学校"); // stats 空 → 整块不渲染 + assertThat(html).doesNotContain("王立群"); // people 过滤无命中 → 整块不渲染 + assertThat(html).doesNotContain("