package check import ( "net/url" "path" "regexp" "strings" "testing" ) // parityLinks rewrites a document's asset links to the flat names the // parity rig serves beside the page: a layout's ../../css/tokens.css, // ../../css/themes/.css, ../../css/shell.css and ../../js/ // become tokens.css, theme.css, shell.css and , and the depth // fixture's /tokens.css, /theme.css and /.js the same. Only the // stylesheet links and script tags are touched: an anchor in the body // is the page's own, and stays. // // Its contract is the tree's own links. A path, relative or from the // root, is flattened to its last segment, and a path with a /themes/ // segment is the theme, whichever theme it names. A URL with a scheme // or a host (https://fonts.example/x.css, //cdn.example/y.js) is left // as it is: the rig has nothing to serve for it, and a layout that // gains one wants it fetched from where it points. Two files that // share a base name under different directories would collide; the // tree has none. func parityLinks(doc string) string { return assetLinks(doc, "") } // assetLinks is parityLinks with the flat names under dir: "" for the // names beside the page, "/" for the root paths the sizing mux serves // (shellPage), so a page served below the root still finds them. func assetLinks(doc, dir string) string { return assetLink.ReplaceAllStringFunc(doc, func(tag string) string { return assetURL.ReplaceAllStringFunc(tag, func(attr string) string { m := assetURL.FindStringSubmatch(attr) if u, err := url.Parse(m[2]); err != nil || u.Scheme != "" || u.Host != "" { return attr } name := path.Base(m[2]) if strings.Contains(m[2], "/themes/") { name = "theme.css" } return m[1] + dir + name + `"` }) }) } // assetURL is the href or src attribute inside one asset link. var assetURL = regexp.MustCompile(`((?:href|src)=")([^"]*)"`) // TestParityLinksAreRewritten holds parityLinks to one layout document: // every stylesheet and script it links comes out as a flat name beside // the page, the theme under whichever name the layout linked it as // theme.css, the body's own links untouched, and nothing else in the // document moved. A rewrite that missed a link would leave the parity // rig serving a 404 the harness reports, which is a failure for the // wrong reason; one that touched an anchor would compare a page the // layout never wrote. func TestParityLinksAreRewritten(t *testing.T) { m := manifest(t) state, ok := m.Default("layout-sidebar") if !ok || state == "" { t.Fatal("examples.json names no default state for layout-sidebar") } doc := defaultMarkup(t, "layout-sidebar") for _, want := range []string{`href="../../css/tokens.css"`, `href="../../css/themes/day.css"`, `href="../../css/shell.css"`, `src="../../js/shell.js"`, `href="/"`} { if !strings.Contains(doc, want) { t.Fatalf("layout-sidebar/%s no longer links %s; the control has nothing to rewrite", state, want) } } got := parityLinks(doc) for _, want := range []string{ ``, ``, ``, ``, ``, } { if !strings.Contains(got, want) { t.Errorf("the rewritten layout does not link %s", want) } } if strings.Contains(got, "../../") { t.Errorf("a relative link survived the rewrite:\n%s", linkLines(got)) } if n := strings.Count(got, `href="theme.css"`); n != 1 { t.Errorf("the rewritten layout links theme.css %d times, want once", n) } if strings.Count(got, `href="/"`) != strings.Count(doc, `href="/"`) { t.Errorf("the rewrite touched the body's own links: %d href=\"/\" before, %d after", strings.Count(doc, `href="/"`), strings.Count(got, `href="/"`)) } // The link tags are the only lines that change: strip every asset // link from both and what is left is byte for byte the same. strip := func(s string) string { return assetLink.ReplaceAllString(s, "") } if strip(got) != strip(doc) { t.Errorf("the rewrite changed more than the asset links") } if strip(doc) == doc { t.Fatal("the control stripped nothing; assetLink matches no link in the layout") } // The limits: a URL with a scheme or a host is not the rig's to // serve and stays as written, beside the tree's own links, which // still flatten; and the depth fixture's root paths flatten too. const foreign = `` mixed := `` + foreign + `` if got, want := parityLinks(mixed), ``+foreign+``; got != want { t.Errorf("the rewrite of foreign and root links:\n got %s\n want %s", got, want) } // Under a directory, for the shell drives: the same names from the // root, the foreign URLs still untouched. if got, want := assetLinks(mixed, "/"), ``+foreign+``; got != want { t.Errorf("the rewrite under /:\n got %s\n want %s", got, want) } } // assetLink is a stylesheet link or a script tag, the tags parityLinks // rewrites; the test above uses it to prove nothing else changed. var assetLink = regexp.MustCompile(`]*rel="stylesheet"[^>]*>|]*src="[^"]*"[^>]*>`) // linkLines is the asset links of a document, one per line, for a // failure message. func linkLines(doc string) string { return strings.Join(assetLink.FindAllString(doc, -1), "\n") }