216 lines
7.5 KiB
Go
216 lines
7.5 KiB
Go
package seoser
|
|
|
|
import (
|
|
"encoding/json"
|
|
"html/template"
|
|
"testing"
|
|
"videoweb/app/service/configser"
|
|
"videoweb/app/service/playletser"
|
|
)
|
|
|
|
func TestPageJSONLD_DetailIsValidJSON(t *testing.T) {
|
|
detail := &playletser.PlayletInfoResp{
|
|
ID: 12,
|
|
Slug: "test-show",
|
|
Title: "测试短剧",
|
|
Desc: "剧情简介",
|
|
Cover: "https://cdn.example.com/cover.jpg",
|
|
OnlineAt: "2026-01-02 15:04:05",
|
|
TotalEpisode: 30,
|
|
TagNames: []string{"都市"},
|
|
}
|
|
raw := PageJSONLD("https://www.example.com", "https://www.example.com/detail/test-show", "/detail/test-show", map[string]any{"detail": detail}, configser.AppConfigVM{})
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
t.Fatalf("json-ld invalid: %v\n%s", err, raw)
|
|
}
|
|
graph, _ := parsed["@graph"].([]any)
|
|
if len(graph) < 3 {
|
|
t.Fatalf("expected organization + website + detail nodes, got %d", len(graph))
|
|
}
|
|
}
|
|
|
|
func TestPageJSONLD_PlayHasVideoObject(t *testing.T) {
|
|
playlet := &playletser.PlayletInfoResp{
|
|
ID: 1,
|
|
Slug: "example-show",
|
|
Title: "剧",
|
|
Cover: "https://cdn.example.com/a.jpg",
|
|
MediaCreatedAt: "2026-03-01 08:30:00",
|
|
MediaUpdatedAt: "2026-03-02 09:45:00",
|
|
TotalEpisode: 12,
|
|
TagNames: []string{"都市", "甜宠"},
|
|
}
|
|
chapter := &playletser.PlayletChapterInfoResp{
|
|
ID: 9,
|
|
PlayletID: 1,
|
|
CurrentEpisode: 2,
|
|
MediaTime: 90,
|
|
MediaSize: 2048,
|
|
Width: 1080,
|
|
Height: 1920,
|
|
VideoURL: "https://cdn.example.com/episode-2.m3u8",
|
|
CountBrowse: 321,
|
|
}
|
|
raw := PageJSONLD("https://www.example.com", "https://www.example.com/play/example-show/9", "/play/example-show/9", map[string]any{
|
|
"playlet": playlet,
|
|
"chapter": chapter,
|
|
}, configser.AppConfigVM{})
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
t.Fatalf("json-ld invalid: %v", err)
|
|
}
|
|
found := false
|
|
foundSeries := false
|
|
foundEpisode := false
|
|
for _, node := range parsed["@graph"].([]any) {
|
|
m, _ := node.(map[string]any)
|
|
if m["@type"] == "TVSeries" {
|
|
foundSeries = true
|
|
if m["numberOfEpisodes"] != float64(12) || m["datePublished"] == nil {
|
|
t.Fatalf("TVSeries missing episode/date fields: %v", m)
|
|
}
|
|
}
|
|
if m["@type"] == "TVEpisode" {
|
|
foundEpisode = true
|
|
if m["episodeNumber"] != float64(2) || m["associatedMedia"] == nil {
|
|
t.Fatalf("TVEpisode missing episode/media relation: %v", m)
|
|
}
|
|
}
|
|
if m["@type"] == "VideoObject" {
|
|
found = true
|
|
if m["uploadDate"] == nil || m["thumbnailUrl"] == nil {
|
|
t.Fatalf("VideoObject missing required fields: %v", m)
|
|
}
|
|
if m["uploadDate"] != "2026-03-01T08:30:00+08:00" {
|
|
t.Fatalf("VideoObject uploadDate = %v", m["uploadDate"])
|
|
}
|
|
if m["dateModified"] != "2026-03-02T09:45:00+08:00" {
|
|
t.Fatalf("VideoObject dateModified = %v", m["dateModified"])
|
|
}
|
|
if m["contentUrl"] != "https://cdn.example.com/episode-2.m3u8" {
|
|
t.Fatalf("VideoObject contentUrl = %v", m["contentUrl"])
|
|
}
|
|
if m["duration"] != "PT90S" || m["episodeNumber"] != float64(2) {
|
|
t.Fatalf("VideoObject missing duration/episode: %v", m)
|
|
}
|
|
if m["contentRating"] != "adult" || m["isFamilyFriendly"] != false {
|
|
t.Fatalf("VideoObject missing adult flags: %v", m)
|
|
}
|
|
if m["partOfSeries"] == nil || m["genre"] == nil {
|
|
t.Fatalf("VideoObject missing series/genre relation: %v", m)
|
|
}
|
|
if _, exists := m["embedUrl"]; exists {
|
|
t.Fatalf("landing page must not be emitted as embedUrl: %v", m["embedUrl"])
|
|
}
|
|
if m["interactionStatistic"] == nil {
|
|
t.Fatalf("VideoObject missing interactionStatistic: %v", m)
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("missing VideoObject")
|
|
}
|
|
if !foundSeries || !foundEpisode {
|
|
t.Fatalf("missing TVSeries/TVEpisode: series=%v episode=%v", foundSeries, foundEpisode)
|
|
}
|
|
}
|
|
|
|
func TestPageJSONLD_PaginatedCollectionHasItemList(t *testing.T) {
|
|
raw := PageJSONLD("https://www.example.com", "https://www.example.com/list/ai-short-play/page/2", "/list/ai-short-play/page/2", map[string]any{
|
|
"title": "短剧第2页",
|
|
"rankOffset": 20,
|
|
"list": []playletser.CommonData{
|
|
{ID: 1, Slug: "first"},
|
|
{ID: 2, Slug: "second"},
|
|
},
|
|
}, configser.AppConfigVM{})
|
|
graph := parseGraph(t, raw)
|
|
page := findNodeByType(t, graph, "CollectionPage")
|
|
if page["mainEntity"] == nil {
|
|
t.Fatalf("CollectionPage missing ItemList reference: %v", page)
|
|
}
|
|
items := findNodeByType(t, graph, "ItemList")
|
|
entries, _ := items["itemListElement"].([]any)
|
|
if len(entries) != 2 {
|
|
t.Fatalf("ItemList entries = %d", len(entries))
|
|
}
|
|
first, _ := entries[0].(map[string]any)
|
|
if first["position"] != float64(21) || first["url"] != "https://www.example.com/detail/first" {
|
|
t.Fatalf("first ItemList entry = %v", first)
|
|
}
|
|
}
|
|
|
|
func TestPageJSONLD_FAQMatchesVisibleItems(t *testing.T) {
|
|
raw := PageJSONLD("https://www.example.com", "https://www.example.com/home-road", "/home-road", map[string]any{
|
|
"title": "回家的路",
|
|
"faqItems": []FAQItem{
|
|
{Question: "入口打不开怎么办?", Answer: "请切换备用入口。"},
|
|
{Question: "如何确认安全?", Answer: "仅使用官方发布的地址。"},
|
|
},
|
|
}, configser.AppConfigVM{})
|
|
graph := parseGraph(t, raw)
|
|
faq := findNodeByType(t, graph, "FAQPage")
|
|
questions, _ := faq["mainEntity"].([]any)
|
|
if len(questions) != 2 {
|
|
t.Fatalf("FAQ questions = %d", len(questions))
|
|
}
|
|
question, _ := questions[0].(map[string]any)
|
|
if question["name"] != "入口打不开怎么办?" {
|
|
t.Fatalf("FAQ question = %v", question)
|
|
}
|
|
findNodeByType(t, graph, "BreadcrumbList")
|
|
}
|
|
|
|
func TestPageJSONLD_DetailBreadcrumbMatchesVisibleHierarchy(t *testing.T) {
|
|
detail := &playletser.PlayletInfoResp{ID: 12, Slug: "test-show", Title: "测试短剧"}
|
|
raw := PageJSONLD("https://www.example.com", "https://www.example.com/detail/test-show", "/detail/test-show", map[string]any{"detail": detail}, configser.AppConfigVM{})
|
|
crumb := findNodeByType(t, parseGraph(t, raw), "BreadcrumbList")
|
|
items, _ := crumb["itemListElement"].([]any)
|
|
if len(items) != 3 {
|
|
t.Fatalf("detail breadcrumb items = %d", len(items))
|
|
}
|
|
middle, _ := items[1].(map[string]any)
|
|
if middle["name"] != "短剧" || middle["item"] != "https://www.example.com/list/ai-short-play" {
|
|
t.Fatalf("detail breadcrumb middle item = %v", middle)
|
|
}
|
|
}
|
|
|
|
func TestPageJSONLD_SkipNoindex(t *testing.T) {
|
|
raw := PageJSONLD("https://www.example.com", "", "/setting", map[string]any{"title": "账号设置"}, configser.AppConfigVM{})
|
|
if string(raw) != "" {
|
|
t.Fatalf("noindex page should not emit json-ld, got %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestIsIndexablePath(t *testing.T) {
|
|
if !IsIndexablePath("/") || !IsIndexablePath("/detail/example-show") || !IsIndexablePath("/topic/editor-picks") || !IsIndexablePath("/help-center") {
|
|
t.Fatal("public pages should be indexable")
|
|
}
|
|
if IsIndexablePath("/setting") || IsIndexablePath("/user-center") || IsIndexablePath("/search") || IsIndexablePath("/richtext/custom") || IsIndexablePath("/privacy-policy") || IsIndexablePath("/user-agreement") {
|
|
t.Fatal("private pages should not be indexable")
|
|
}
|
|
}
|
|
|
|
func parseGraph(t *testing.T, raw template.JS) []any {
|
|
t.Helper()
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
t.Fatalf("json-ld invalid: %v\n%s", err, raw)
|
|
}
|
|
graph, _ := parsed["@graph"].([]any)
|
|
return graph
|
|
}
|
|
|
|
func findNodeByType(t *testing.T, graph []any, typ string) map[string]any {
|
|
t.Helper()
|
|
for _, node := range graph {
|
|
m, _ := node.(map[string]any)
|
|
if m["@type"] == typ {
|
|
return m
|
|
}
|
|
}
|
|
t.Fatalf("missing %s node in %v", typ, graph)
|
|
return nil
|
|
}
|