Every link in the site resolves, and nothing leaves its origin
Human
Prompt
Task 6, Step 1 of Fiesta part 3: the whole site's link and origin tests. An untagged test that resolves every href, src and srcset on every page of the built site, the landing pages in all eleven languages, the design system's index, every example page and every frame, through serveSite's own rules, with fragments needing an id on their page, a bare # allowed, the site's own absolute URLs stripped of www.Origin and resolved as pages, and exactly the outside URLs the site links on purpose allowed; its control a scratch page linking /design-system/nope.html, seen red by assertion. And a browser drive that loads /, /ar/, /design-system/ and /design-system/day/dropdown.html through serveSite with no request leaving the origin and no 4xx, red by assertion first against a stub page with an outside image.
From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.
Summary
TestEveryLinkInTheSiteResolves (internal/www/links_test.go, untagged) reads every href, src and srcset on every HTML file of site.Build over the committed releases and www.Pages, plus the URL shown as text in a translate="no" code element, and resolves each against the page's own URL by serving the path through serveSite's handler, so a directory index, the /1/ rewrite and a real file count and nothing else does. A fragment needs an element with that id on the page it names; a bare # is left alone. A URL starting with www.Origin is the site's own, stripped and resolved root-relatively, so a wrong path under the origin is caught; any other outside URL must be one of the eleven listed in the test: rastrillo.org and its /docs/, the two amadan.net repositories, carlosframework.com and its /platform/, /start/, /letters/ and /trust/, carloku.com and 11factor.org. The check runs first over a control site whose scratch page links a missing page, a missing fragment, a wrong path under the origin and an unlisted outside URL, and must name all four. TestTheSiteServesFromItsOwnOrigin (internal/www/site_browser_test.go, browser) loads /, /ar/, /design-system/ and /design-system/day/dropdown.html on a tall viewport through the same handler, waits for the page and every frame in it to finish, and requires every request to stay on the rig's origin, every response short of a 4xx (the browser's own /favicon.ico probe excepted, as in the harness), and the traffic the page is there for: the landing pages' theme through /1/, the gallery's stylesheet, the example's frames. The real site passes both; no link needed fixing.
Agent notes
Red by assertion, both. The link test with the control's scratch page planted into the real tree: design-system/scratch.html: a href /design-system/nope.html resolves to /design-system/nope.html, which the edge answers with a 404; and the same for the wrong path under the origin (/nope/), the missing fragment (#nowhere on /design-system/index.html) and https://example.com/ leaving the site; nothing else on the real site was reported, then the plant removed and green. The browser drive against a stub landing page with an outside image and a missing one: a request left the origin: https://example.invalid/x.png, and the browser got a 404 for /nope.png, both from the drive's own lines before the harness's problem list; stub removed, green. The resolver is serveSite's handler through an httptest recorder, not a second copy of the rules. The install text's URL needs no allowance: it starts with www.Origin and resolves through the /1/ rewrite like the hreflang links. Nothing in the site links relatively today, every URL is root-relative, a fragment or absolute, but relative references resolve against the page's URL all the same. form action is not read, as the brief said href, src and srcset. go test ./internal/www, both www shards (17 and 16 of 33) and make ci in a clean /var/tmp worktree were green before this commit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+402
−0
internal/www/links_test.go+268 −0internal/www/site_browser_test.go+134 −0
CI passed — run details
CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok amadan.net/rastrillo/fiesta 0.097s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN TestTheOneLinkIsUnder25KiBGzipped
build_test.go:138: fiesta-plain.css: 152856 bytes, 22303 bytes gzipped (21.8 KiB of the 25.0 KiB limit)
build_test.go:138: fiesta-signal.css: 152951 bytes, 22414 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
build_test.go:138: fiesta.css: 155770 bytes, 23018 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok amadan.net/rastrillo/fiesta/internal/build 0.021s
=== suite ===
--- shard 0 of 12 ---
==> shard 0 of 12: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 go test -count=1 ./...
ok amadan.net/rastrillo/fiesta 1.175s
ok amadan.net/rastrillo/fiesta/check 4.799s
ok amadan.net/rastrillo/fiesta/cmd/fiesta 8.347s
ok amadan.net/rastrillo/fiesta/example 0.173s
ok amadan.net/rastrillo/fiesta/internal/axe 0.111s
ok amadan.net/rastrillo/fiesta/internal/build 1.051s
ok amadan.net/rastrillo/fiesta/internal/codeview 0.021s
ok amadan.net/rastrillo/fiesta/internal/gallery 5.120s
? amadan.net/rastrillo/fiesta/internal/harness [no test files]
ok amadan.net/rastrillo/fiesta/internal/markup 0.013s
ok amadan.net/rastrillo/fiesta/internal/nodetest 0.349s
ok amadan.net/rastrillo/fiesta/internal/site 3.061s
ok amadan.net/rastrillo/fiesta/internal/tokens 0.014s
ok amadan.net/rastrillo/fiesta/internal/www 9.768s
--- shard 1 of 12 ---
==> shard 1 of 12: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
--- shard 2 of 12 ---
==> shard 2 of 12: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
=== Symbol Results ===
No vulnerabilities found.
Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
--- shard 3 of 12 ---
==> shard 3 of 12: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
[90m8:25PM[0m [32mINF[0m [1mUnknown SCM platform. Use --platform to include links in findings.[0m [36mhost=[0mamadan.net
[90m8:26PM[0m [32mINF[0m [1m1 commits scanned.[0m
[90m8:26PM[0m [32mINF[0m [1mscanned ~5826711 bytes (5.83 MB) in 3.28s[0m
[90m8:26PM[0m [32mINF[0m [1mno leaks found[0m
--- shard 4 of 12 ---
==> shard 4 of 12: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
ok amadan.net/rastrillo/fiesta/internal/harness 10.865s
--- shard 5 of 12 ---
==> shard 5 of 12: make browser.check~1of3
./hack/gotest.sh browser 'check~1of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 3 of ./check/ runs 95 of 286 tests
ok amadan.net/rastrillo/fiesta/check 197.110s
--- shard 6 of 12 ---
==> shard 6 of 12: make browser.check~2of3
./hack/gotest.sh browser 'check~2of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 3 of ./check/ runs 95 of 286 tests
ok amadan.net/rastrillo/fiesta/check 208.955s
--- shard 7 of 12 ---
==> shard 7 of 12: make browser.check~3of3
./hack/gotest.sh browser 'check~3of3'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 3 of 3 of ./check/ runs 96 of 286 tests
ok amadan.net/rastrillo/fiesta/check 208.881s
--- shard 8 of 12 ---
==> shard 8 of 12: make browser.internal.gallery~1of2
./hack/gotest.sh browser 'internal.gallery~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/gallery/ runs 1 of 25 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 74.031s
--- shard 9 of 12 ---
==> shard 9 of 12: make browser.internal.gallery~2of2
./hack/gotest.sh browser 'internal.gallery~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/gallery/ runs 24 of 25 tests
ok amadan.net/rastrillo/fiesta/internal/gallery 34.940s
--- shard 10 of 12 ---
==> shard 10 of 12: make browser.internal.www~1of2
./hack/gotest.sh browser 'internal.www~1of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 1 of 2 of ./internal/www/ runs 17 of 33 tests
ok amadan.net/rastrillo/fiesta/internal/www 36.998s
--- shard 11 of 12 ---
==> shard 11 of 12: make browser.internal.www~2of2
./hack/gotest.sh browser 'internal.www~2of2'
gotest: on DOCS-T1, driving the harness's own choice (/usr/bin/chromium)
gotest: slice 2 of 2 of ./internal/www/ runs 16 of 33 tests
ok amadan.net/rastrillo/fiesta/internal/www 32.213s
Intent
diff --git a/internal/www/links_test.go b/internal/www/links_test.go| new file mode 100644 |
| index 0000000..b00fd9a |
| --- /dev/null |
| +++ b/internal/www/links_test.go |
| @@ -0,0 +1,268 @@ |
| +package www_test |
| + |
| +import ( |
| + "fmt" |
| + "net/http" |
| + "net/http/httptest" |
| + "net/url" |
| + "regexp" |
| + "sort" |
| + "strings" |
| + "testing" |
| + |
| + xhtml "golang.org/x/net/html" |
| + |
| + "amadan.net/rastrillo/fiesta/internal/www" |
| +) |
| + |
| +// outsideURLs are the URLs the site links on purpose that are not its |
| +// own: the top bar's and the footer's. Exactly these, as written, so |
| +// a new outside link is a decision someone takes here rather than a |
| +// line that slips in. The site's own absolute URLs, the hreflang |
| +// links and the install text, start with www.Origin and are resolved |
| +// as pages of the site instead, so a wrong path under the origin is |
| +// caught; the origin itself is never on this list. |
| +var outsideURLs = []string{ |
| + "https://rastrillo.org", |
| + "https://rastrillo.org/docs/", |
| + "https://amadan.net/rastrillo/fiesta", |
| + "https://amadan.net/rastrillo/rastrillo", |
| + "https://carlosframework.com", |
| + "https://carlosframework.com/platform/", |
| + "https://carlosframework.com/start/", |
| + "https://carlosframework.com/letters/", |
| + "https://carlosframework.com/trust/", |
| + "https://carloku.com", |
| + "https://11factor.org", |
| +} |
| + |
| +// textURL is an absolute URL inside text: the install box's <code> |
| +// shows a whole link tag as text, and the URL in it has to reach the |
| +// stylesheet like any other. |
| +var textURL = regexp.MustCompile(`https?://[^\s"'<>]+`) |
| + |
| +// reference is one URL a page carries: where it was found and what it |
| +// says. |
| +type reference struct { |
| + page string // the tree path of the page, index.html or ar/index.html |
| + where string // the element and attribute, "a href", or "code text" |
| + raw string // the URL as written |
| +} |
| + |
| +// referencesOf reads every href, src and srcset on a page, whatever |
| +// the element (a, link, script, img, iframe, use...), and every URL in |
| +// the text of a translate="no" code element, the one place a URL is |
| +// shown rather than linked. |
| +func referencesOf(page string, doc *xhtml.Node) []reference { |
| + var out []reference |
| + walk(doc, func(n *xhtml.Node) { |
| + for _, a := range n.Attr { |
| + switch a.Key { |
| + case "href", "src": |
| + out = append(out, reference{page, n.Data + " " + a.Key, a.Val}) |
| + case "srcset": |
| + // Comma-separated candidates, each a URL and an |
| + // optional descriptor. |
| + for _, c := range strings.Split(a.Val, ",") { |
| + if f := strings.Fields(c); len(f) > 0 { |
| + out = append(out, reference{page, n.Data + " srcset", f[0]}) |
| + } |
| + } |
| + } |
| + } |
| + if n.Data == "code" && attr(n, "translate") == "no" { |
| + for _, u := range textURL.FindAllString(text(n), -1) { |
| + out = append(out, reference{page, "code text", u}) |
| + } |
| + } |
| + }) |
| + return out |
| +} |
| + |
| +// served is what the edge answers for one path: the status, and the |
| +// ids on the page if it is one, read once. |
| +type served struct { |
| + status int |
| + html bool |
| + ids map[string]bool |
| +} |
| + |
| +// resolver answers for the built site the way the edge does, through |
| +// serveSite's own handler, so a directory index, the _redirects |
| +// rewrite and a real file count and nothing else does. |
| +type resolver struct { |
| + h *siteHandler |
| + cache map[string]*served |
| +} |
| + |
| +func newResolver(t *testing.T, tree map[string][]byte) *resolver { |
| + t.Helper() |
| + return &resolver{h: newSiteHandler(t, tree), cache: map[string]*served{}} |
| +} |
| + |
| +// get serves p through the handler, once per path. |
| +func (r *resolver) get(p string) *served { |
| + if s, ok := r.cache[p]; ok { |
| + return s |
| + } |
| + rec := httptest.NewRecorder() |
| + r.h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, p, nil)) |
| + s := &served{status: rec.Code} |
| + if s.status == http.StatusOK && strings.HasPrefix(rec.Header().Get("Content-Type"), "text/html") { |
| + s.html = true |
| + s.ids = map[string]bool{} |
| + if doc, err := xhtml.Parse(rec.Body); err == nil { |
| + walk(doc, func(n *xhtml.Node) { |
| + if id := attr(n, "id"); id != "" { |
| + s.ids[id] = true |
| + } |
| + }) |
| + } |
| + } |
| + r.cache[p] = s |
| + return s |
| +} |
| + |
| +// check reports what is wrong with one reference, or "" when it |
| +// resolves: an outside URL not on the list, a path the edge would not |
| +// serve, or a fragment with no id on the page it names. A bare # is a |
| +// link to nowhere on purpose, as in a sample, and is left alone. |
| +func (r *resolver) check(ref reference) string { |
| + if ref.raw == "#" { |
| + return "" |
| + } |
| + raw := ref.raw |
| + u, err := url.Parse(raw) |
| + if err != nil { |
| + return fmt.Sprintf("%s: %s %q does not parse: %v", ref.page, ref.where, raw, err) |
| + } |
| + if u.Scheme != "" || u.Host != "" { |
| + if raw == www.Origin || strings.HasPrefix(raw, www.Origin+"/") { |
| + // The site's own, written in full: resolve the rest as a |
| + // root-relative path of this very tree. |
| + raw = strings.TrimPrefix(raw, www.Origin) |
| + if raw == "" { |
| + raw = "/" |
| + } |
| + if u, err = url.Parse(raw); err != nil { |
| + return fmt.Sprintf("%s: %s %q does not parse: %v", ref.page, ref.where, ref.raw, err) |
| + } |
| + } else { |
| + for _, ok := range outsideURLs { |
| + if raw == ok { |
| + return "" |
| + } |
| + } |
| + return fmt.Sprintf("%s: %s links %s, which leaves the site and is not among the outside URLs the site links on purpose", ref.page, ref.where, raw) |
| + } |
| + } |
| + base := &url.URL{Path: "/" + ref.page} |
| + target := base.ResolveReference(u) |
| + s := r.get(target.Path) |
| + if s.status != http.StatusOK { |
| + return fmt.Sprintf("%s: %s %s resolves to %s, which the edge answers with a %d", ref.page, ref.where, ref.raw, target.Path, s.status) |
| + } |
| + if target.Fragment == "" { |
| + return "" |
| + } |
| + if !s.html { |
| + return fmt.Sprintf("%s: %s %s names a fragment on %s, which is not a page", ref.page, ref.where, ref.raw, target.Path) |
| + } |
| + if !s.ids[target.Fragment] { |
| + return fmt.Sprintf("%s: %s %s names #%s, and %s has no element with that id", ref.page, ref.where, ref.raw, target.Fragment, target.Path) |
| + } |
| + return "" |
| +} |
| + |
| +// linkProblems checks every reference on every HTML file of a built |
| +// site and returns what is wrong, one line each, sorted, with the |
| +// pages it read. |
| +func linkProblems(t *testing.T, tree map[string][]byte) (problems []string, pages []string) { |
| + t.Helper() |
| + r := newResolver(t, tree) |
| + for name, data := range tree { |
| + if !strings.HasSuffix(name, ".html") { |
| + continue |
| + } |
| + pages = append(pages, name) |
| + for _, ref := range referencesOf(name, parse(t, data)) { |
| + if p := r.check(ref); p != "" { |
| + problems = append(problems, p) |
| + } |
| + } |
| + } |
| + sort.Strings(problems) |
| + sort.Strings(pages) |
| + return problems, pages |
| +} |
| + |
| +// TestEveryLinkInTheSiteResolves: on every page of the built site, |
| +// the landing page in each language, the design system's index, every |
| +// example page and every frame, every href, src and srcset reaches a |
| +// file the edge would serve, resolved against the page's own URL, |
| +// through a directory index, the /1/ rewrite or a real file; every |
| +// fragment names an id on the page it points at; and nothing links |
| +// outside the site but the URLs listed above. The install text's URL |
| +// is held to the same rule as the hreflang links: the site's own, |
| +// through the rewrite to the newest release. |
| +// |
| +// The control comes first: a scratch site whose one page links a |
| +// missing page, a missing fragment, a wrong path under the site's own |
| +// origin and an outside URL that is not on the list. The check has to |
| +// name all four, or a clean run of the real site proves nothing. |
| +func TestEveryLinkInTheSiteResolves(t *testing.T) { |
| + control := map[string][]byte{ |
| + "_redirects": []byte("/1/* /1.0.0/:splat 200\n"), |
| + "1.0.0/fiesta.css": []byte("body{}"), |
| + "design-system/index.html": []byte(`<main id="main"><a href="../">up</a><link rel="stylesheet" href="/1/fiesta.css"></main>`), |
| + "design-system/scratch.html": []byte(`<a href="/design-system/nope.html">missing</a>` + |
| + `<a href="index.html#nowhere">no id</a>` + |
| + `<a href="` + www.Origin + `/nope/">wrong path</a>` + |
| + `<a href="https://example.com/">outside</a>` + |
| + `<a href="#">bare</a><a href="/#main">main</a>`), |
| + "index.html": []byte(`<main id="main"></main>`), |
| + } |
| + problems, _ := linkProblems(t, control) |
| + for _, want := range []string{"/design-system/nope.html", "#nowhere", www.Origin + "/nope/", "https://example.com/"} { |
| + found := false |
| + for _, p := range problems { |
| + found = found || strings.Contains(p, want) |
| + } |
| + if !found { |
| + t.Errorf("the control's link to %s was not reported; the check would prove nothing. Reported:\n %s", want, strings.Join(problems, "\n ")) |
| + } |
| + } |
| + if len(problems) != 4 { |
| + t.Errorf("the control has 4 broken links and %d were reported:\n %s", len(problems), strings.Join(problems, "\n ")) |
| + } |
| + if t.Failed() { |
| + return |
| + } |
| + |
| + problems, pages := linkProblems(t, builtSite(t)) |
| + for _, p := range problems { |
| + t.Error(p) |
| + } |
| + read := map[string]bool{} |
| + frames, examples := 0, 0 |
| + for _, p := range pages { |
| + read[p] = true |
| + switch { |
| + case strings.Contains(p, "/frames/"): |
| + frames++ |
| + case strings.HasPrefix(p, "design-system/") && p != "design-system/index.html": |
| + examples++ |
| + } |
| + } |
| + for _, l := range www.Languages { |
| + if name := strings.TrimPrefix(pageURL(l), "/") + "index.html"; !read[name] { |
| + t.Errorf("the landing page in %s, %s, was not read", l, name) |
| + } |
| + } |
| + if !read["design-system/index.html"] { |
| + t.Error("the design system's index was not read") |
| + } |
| + if examples == 0 || frames == 0 { |
| + t.Errorf("%d example pages and %d frames were read; the design system has both", examples, frames) |
| + } |
| +} |
diff --git a/internal/www/site_browser_test.go b/internal/www/site_browser_test.go| new file mode 100644 |
| index 0000000..a213c62 |
| --- /dev/null |
| +++ b/internal/www/site_browser_test.go |
| @@ -0,0 +1,134 @@ |
| +//go:build browser |
| + |
| +// The whole site in a real Chromium: a reader opens the landing page, |
| +// its Arabic twin, the design system and one example, and the browser |
| +// asks the site's own origin for everything it needs, and gets it. |
| +package www_test |
| + |
| +import ( |
| + "context" |
| + "fmt" |
| + "net/http" |
| + "strings" |
| + "sync" |
| + "testing" |
| + "time" |
| + |
| + "github.com/chromedp/cdproto/network" |
| + "github.com/chromedp/chromedp" |
| +) |
| + |
| +// traffic is what the browser asked for while a page loaded: every |
| +// request's URL, and every response's status with its URL, from the |
| +// network events of the page and its frames. |
| +type traffic struct { |
| + mu sync.Mutex |
| + requests []string |
| + responses []string |
| +} |
| + |
| +// listen records the browser's traffic from now on. |
| +func listen(ctx context.Context) *traffic { |
| + tr := &traffic{} |
| + chromedp.ListenTarget(ctx, func(ev any) { |
| + switch e := ev.(type) { |
| + case *network.EventRequestWillBeSent: |
| + tr.mu.Lock() |
| + tr.requests = append(tr.requests, e.Request.URL) |
| + tr.mu.Unlock() |
| + case *network.EventResponseReceived: |
| + tr.mu.Lock() |
| + tr.responses = append(tr.responses, fmt.Sprintf("%d %s", int(e.Response.Status), e.Response.URL)) |
| + tr.mu.Unlock() |
| + } |
| + }) |
| + return tr |
| +} |
| + |
| +// take hands back what was recorded and starts afresh, for the next |
| +// page. |
| +func (tr *traffic) take() (requests, responses []string) { |
| + tr.mu.Lock() |
| + defer tr.mu.Unlock() |
| + requests, responses = tr.requests, tr.responses |
| + tr.requests, tr.responses = nil, nil |
| + return requests, responses |
| +} |
| + |
| +// loadedJS is true once the page and every frame in it have loaded: a |
| +// gallery page's frames are same-origin, so their documents can be |
| +// read, and a frame that is still about:blank or loading is not done. |
| +const loadedJS = `document.readyState === "complete" && [...document.querySelectorAll("iframe")].every(f => f.contentDocument && f.contentDocument.location.href !== "about:blank" && f.contentDocument.readyState === "complete")` |
| + |
| +// TestTheSiteServesFromItsOwnOrigin: the landing page, the Arabic |
| +// landing page, the design system's index and one example page each |
| +// load through the edge's rules with every request going to the |
| +// site's own origin and every response short of a 4xx. The viewport |
| +// is tall so that lazily loaded frames are in view and load, and the |
| +// drive waits for every frame to finish before it reads the traffic. |
| +// The browser's own probe for /favicon.ico is the one 404 allowed, |
| +// as in the harness: the browser asks for it, not the page. |
| +func TestTheSiteServesFromItsOwnOrigin(t *testing.T) { |
| + rig, _ := siteRig(t) |
| + ctx, cancel := context.WithTimeout(rig.Context(), 2*time.Minute) |
| + defer cancel() |
| + tr := listen(ctx) |
| + mustRun(t, ctx, chromedp.EmulateViewport(1280, 6000)) |
| + // What each page must have asked for, so a page that loaded |
| + // nothing of the kind the drive is here for cannot pass: the |
| + // landing pages their theme through the /1/ rewrite, the example |
| + // its frames. |
| + expects := map[string]string{ |
| + "/": "/1/fiesta.css", |
| + "/ar/": "/1/fiesta.css", |
| + "/design-system/": "/design-system/assets/gallery.css", |
| + "/design-system/day/dropdown.html": "/design-system/day/frames/dropdown/", |
| + } |
| + for _, p := range []string{"/", "/ar/", "/design-system/", "/design-system/day/dropdown.html"} { |
| + tr.take() |
| + mustRun(t, ctx, chromedp.Navigate(rig.Origin+p), chromedp.WaitReady("body")) |
| + deadline := time.Now().Add(20 * time.Second) |
| + for { |
| + var done bool |
| + mustRun(t, ctx, chromedp.Evaluate(loadedJS, &done)) |
| + if done { |
| + break |
| + } |
| + if !time.Now().Before(deadline) { |
| + t.Fatalf("%s: the page or one of its frames never finished loading", p) |
| + } |
| + time.Sleep(50 * time.Millisecond) |
| + } |
| + // Give the browser a moment to report the last responses, |
| + // which arrive as events after the documents say complete. |
| + time.Sleep(200 * time.Millisecond) |
| + requests, responses := tr.take() |
| + if len(requests) < 2 { |
| + t.Errorf("%s: only %d requests were seen; the page alone loads a stylesheet, so the listener read nothing", p, len(requests)) |
| + } |
| + asked := false |
| + for _, u := range requests { |
| + if !strings.HasPrefix(u, rig.Origin+"/") && !strings.HasPrefix(u, "data:") { |
| + t.Errorf("%s: a request left the origin: %s", p, u) |
| + } |
| + asked = asked || strings.HasPrefix(u, rig.Origin+expects[p]) |
| + } |
| + if !asked { |
| + t.Errorf("%s: nothing under %s was requested; this is not the page the drive is for, or its frames never loaded", p, expects[p]) |
| + } |
| + for _, r := range responses { |
| + var status int |
| + var u string |
| + if _, err := fmt.Sscanf(r, "%d %s", &status, &u); err != nil { |
| + t.Fatalf("%s: reading a recorded response %q: %v", p, r, err) |
| + } |
| + if status == http.StatusNotFound && u == rig.Origin+"/favicon.ico" { |
| + continue |
| + } |
| + if status >= http.StatusBadRequest { |
| + t.Errorf("%s: the browser got a %d for %s", p, status, u) |
| + } |
| + } |
| + rig.Screen("body", "the page at "+p) |
| + } |
| +} |