Files
fish03andClaude Opus 5.5 bef9f8b4ed Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 14:43:16 +07:00

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
}