//go:build browser
package check
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"sort"
"strings"
"testing"
"time"
"github.com/chromedp/chromedp"
"amadan.net/rastrillo/fiesta"
"amadan.net/rastrillo/fiesta/internal/build"
"amadan.net/rastrillo/fiesta/internal/harness"
)
// The release is held to the tree it was built from, in a browser:
// the one-file themes paint the same pixels as the source stylesheets,
// the stripped scripts drive the same journeys, and a page on another
// origin can use all of it from one directory.
// releaseDist is build.Dist over the embedded tree at the shipped
// version, once per test.
func releaseDist(t *testing.T) map[string][]byte {
t.Helper()
d, err := build.Dist(fiesta.FS(), fiesta.Version)
if err != nil {
t.Fatalf("building the release: %v", err)
}
return d
}
// distScripts serves the release's scripts and i18n/*.js by the name
// the fixture mux asks for, in place of the tree's, so a drive written
// against the source runs against what ships.
func distScripts(t *testing.T) func(name string) ([]byte, bool) {
t.Helper()
d := releaseDist(t)
return func(name string) ([]byte, bool) {
if !strings.HasSuffix(name, ".js") {
return nil, false
}
b, ok := d[name]
return b, ok
}
}
// oneFile is the release's one-file theme for a theme.
func oneFile(theme string) string {
if theme == "day" {
return "fiesta.css"
}
return "fiesta-" + theme + ".css"
}
// TestTheReleaseRendersAsTheSource: for every theme, the matrix page
// under the source tokens.css and theme paints exactly the pixels it
// paints under the release's one-file theme served alone, and again
// under the release's separate tokens.css and theme. Exactly: the
// strip removes comments and nothing else, so there is no corner
// case to allow for, and samePixelsWithin's cap is 0.
func TestTheReleaseRendersAsTheSource(t *testing.T) {
d := releaseDist(t)
for _, theme := range fiesta.ThemeNames() {
oldTheme, ok := fiesta.ThemeCSS(theme)
if !ok {
t.Fatalf("no theme %s", theme)
}
for _, c := range []struct {
name string
tokens, theme []byte
}{
{"one file", d[oneFile(theme)], []byte{}},
{"separate files", d["tokens.css"], d[theme+".css"]},
} {
t.Run(theme+", "+c.name, func(t *testing.T) {
if len(c.tokens) == 0 {
t.Fatalf("the release has no stylesheet for %s", c.name)
}
rig := legacyRig(t, fiesta.TokensCSS(), oldTheme, c.tokens, c.theme, false)
ctx, cancel := context.WithTimeout(rig.Context(), 300*time.Second)
defer cancel()
for _, scheme := range []string{"light", "dark"} {
old := matrixShots(t, ctx, rig.Origin, "/old/", scheme)
now := matrixShots(t, ctx, rig.Origin, "/new/", scheme)
if len(old) == 0 {
t.Fatal("the matrix photographed nothing")
}
keys := make([]string, 0, len(old))
for key := range old {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if ok, why := samePixelsWithin(t, old[key], now[key], 0); !ok {
t.Errorf("%s renders differently from the release: %s", key, why)
}
}
}
})
}
}
}
// TestTheReleaseScriptsDriveTheJourneys runs five drives, each on a
// rig whose scripts are the release's: the switch drag, the
// remembered scheme, the select filter, the calendar and the
// snippet's copy. Each drive's pages put their own document at /, so
// each has a rig of its own.
func TestTheReleaseScriptsDriveTheJourneys(t *testing.T) {
scripts := distScripts(t)
if b, ok := scripts("fiesta.js"); !ok || !bytes.HasPrefix(b, []byte(build.Banner(fiesta.Version))) {
t.Fatal("distScripts does not serve the release's fiesta.js")
}
for _, drive := range []struct {
name string
pages map[string]string
run func(*testing.T, *harness.Rig)
}{
{"switch drag", switchDragPages, driveSwitchDrag},
{"scheme remember", schemePages(), driveSchemeRemember},
{"select filter", selectFilterPages(t), driveSelectFilter},
{"calendar open", calendarPages(t), driveCalendarOpen},
{"snippet copy", copyPages(), func(t *testing.T, rig *harness.Rig) {
ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
defer cancel()
driveCopy(t, ctx, rig.Origin, "/")
}},
} {
t.Run(drive.name, func(t *testing.T) {
pages := drive.pages
rig := harness.New(t, func(string) http.Handler { return sizingMuxWith(t, pages, scripts) })
drive.run(t, rig)
})
}
}
// contentTypes is what the release server says each file is.
var contentTypes = map[string]string{".css": "text/css", ".js": "text/javascript", ".svg": "image/svg+xml", ".json": "application/json"}
// releaseServer serves the dist on 127.0.0.1, which is a different
// origin from the rig's localhost, as a CDN or a site's static host
// would be.
func releaseServer(t *testing.T, d map[string][]byte) string {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, ok := d[strings.TrimPrefix(r.URL.Path, "/")]
if !ok {
http.NotFound(w, r)
return
}
for ext, ct := range contentTypes {
if strings.HasSuffix(r.URL.Path, ext) {
w.Header().Set("Content-Type", ct)
}
}
w.Write(b)
}))
t.Cleanup(srv.Close)
if !strings.HasPrefix(srv.URL, "http://127.0.0.1:") {
t.Fatalf("the release server is at %s, not on 127.0.0.1", srv.URL)
}
return srv.URL
}
// anotherOriginDoc is a Spanish page on the rig's origin that takes
// everything from the release server: fiesta.css, the scripts the
// date field needs, es.js, and every icon as an image.
func anotherOriginDoc(files string, slugs []string) string {
var icons strings.Builder
for _, slug := range slugs {
fmt.Fprintf(&icons, ``, slug, files, slug)
}
return `
` + icons.String() + `
` + `