test(11.2): cover site_url and site_label and gate the framework, app and site stages
- TestCheckSiteURL (21 accepted and rejected values), TestSiteLabel and TestSiteURLPrecedence (option over site.yaml, label-without-URL and invalid option errors); new TestParseSite rows for blank and two-line labels - TestSiteLink: escaped label, section page and 404 page, exact unset header bytes - TestDocsSiteFlagsInHelp and the --site-label-without-URL error - check-phase11.2.sh --framework, --app and --site
This commit is contained in:
@@ -104,6 +104,38 @@ func TestDocsBuildSiteFlags(t *testing.T) {
|
|||||||
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-url") {
|
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-url") {
|
||||||
t.Fatalf("docs:build --site-url javascript:alert(1): err = %v, want a --site-url error", err)
|
t.Fatalf("docs:build --site-url javascript:alert(1): err = %v, want a --site-url error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The framework's own site.yaml sets no site_url, so a label alone fails.
|
||||||
|
buf.Reset()
|
||||||
|
root, err = bonfire.NewRoot("summer", toolCommands(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root.SetArgs([]string{"docs:build", "--root", repoRoot, "--out", filepath.Join(t.TempDir(), "nolabel"), "--site-label", "x"})
|
||||||
|
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-label needs --site-url") {
|
||||||
|
t.Fatalf("docs:build --site-label x: err = %v, want a --site-label needs --site-url error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDocsSiteFlagsInHelp checks that docs:build and docs:serve list the
|
||||||
|
// --site-url and --site-label flags in their help.
|
||||||
|
func TestDocsSiteFlagsInHelp(t *testing.T) {
|
||||||
|
for _, cmd := range []string{"docs:build", "docs:serve"} {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
root, err := bonfire.NewRoot("summer", toolCommands(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root.SetArgs([]string{cmd, "--help"})
|
||||||
|
if err := root.Execute(); err != nil {
|
||||||
|
t.Fatalf("%s --help: %v", cmd, err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"--site-url", "--site-label", "overrides site.yaml site_url", "overrides site.yaml site_label"} {
|
||||||
|
if !strings.Contains(buf.String(), want) {
|
||||||
|
t.Errorf("%s help lacks %q:\n%s", cmd, want, buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// requiredPages lists the guide pages the docs must keep. Each content plan
|
// requiredPages lists the guide pages the docs must keep. Each content plan
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package docsite
|
package docsite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -30,6 +31,9 @@ func TestParseSite(t *testing.T) {
|
|||||||
{"site_url javascript", valid + "site_url: javascript:alert(1)\n", "site_url must be an http(s) URL with a host or a path starting with a single /"},
|
{"site_url javascript", valid + "site_url: javascript:alert(1)\n", "site_url must be an http(s) URL with a host or a path starting with a single /"},
|
||||||
{"site_url protocol-relative", valid + "site_url: //acme.example\n", "site_url must be an http(s) URL with a host or a path starting with a single /"},
|
{"site_url protocol-relative", valid + "site_url: //acme.example\n", "site_url must be an http(s) URL with a host or a path starting with a single /"},
|
||||||
{"site_label without site_url", valid + "site_label: Acme\n", "site_label needs site_url"},
|
{"site_label without site_url", valid + "site_label: Acme\n", "site_label needs site_url"},
|
||||||
|
{"blank site_label", valid + "site_url: /\nsite_label: \" \"\n", "site_label must be one non-empty line"},
|
||||||
|
{"two-line site_label", valid + "site_url: /\nsite_label: \"a\\nb\"\n", "site_label must be one non-empty line"},
|
||||||
|
{"site_url with a newline", valid + "site_url: \"/a\\nb\"\n", "site_url must be an http(s) URL"},
|
||||||
} {
|
} {
|
||||||
if _, err := ParseSite([]byte(tc.raw)); err == nil || !strings.Contains(err.Error(), tc.want) {
|
if _, err := ParseSite([]byte(tc.raw)); err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||||
t.Errorf("%s: err = %v, want %q", tc.name, err, tc.want)
|
t.Errorf("%s: err = %v, want %q", tc.name, err, tc.want)
|
||||||
@@ -41,6 +45,9 @@ func TestParseSite(t *testing.T) {
|
|||||||
t.Errorf("site_url %q: ParseSite = %+v, %v", u, got, err)
|
t.Errorf("site_url %q: ParseSite = %+v, %v", u, got, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if got, err := ParseSite([]byte(valid + "site_url: /\nsite_label: Acme\n")); err != nil || got.SiteURL != "/" || got.SiteLabel != "Acme" {
|
||||||
|
t.Errorf("site_url with site_label: ParseSite = %+v, %v", got, err)
|
||||||
|
}
|
||||||
if strings.Contains(func() string { _, err := ParseSite([]byte(valid + "colour: red\n")); return err.Error() }(), "\n") {
|
if strings.Contains(func() string { _, err := ParseSite([]byte(valid + "colour: red\n")); return err.Error() }(), "\n") {
|
||||||
t.Error("a decode error must be reported on one line")
|
t.Error("a decode error must be reported on one line")
|
||||||
}
|
}
|
||||||
@@ -319,3 +326,108 @@ func TestNormalizeDefaults(t *testing.T) {
|
|||||||
t.Fatalf("normalize(relative) = %+v", o)
|
t.Fatalf("normalize(relative) = %+v", o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCheckSiteURL pins the site_url policy (T-11.2-09): an http(s) URL
|
||||||
|
// with a host and no user info, or a path starting with exactly one /.
|
||||||
|
func TestCheckSiteURL(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, raw string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"https host", "https://acme.example", true},
|
||||||
|
{"https host slash", "https://acme.example/", true},
|
||||||
|
{"http port path", "http://acme.example:8080/x", true},
|
||||||
|
{"root path", "/", true},
|
||||||
|
{"path", "/home", true},
|
||||||
|
{"empty", "", false},
|
||||||
|
{"javascript", "javascript:alert(1)", false},
|
||||||
|
{"javascript mixed case", "JavaScript:alert(1)", false},
|
||||||
|
{"data", "data:text/html,x", false},
|
||||||
|
{"protocol-relative", "//acme.example", false},
|
||||||
|
{"bare host", "acme.example", false},
|
||||||
|
{"relative path", "docs/x", false},
|
||||||
|
{"scheme without host", "https://", false},
|
||||||
|
{"user info", "https://user:pw@acme.example", false},
|
||||||
|
{"ftp", "ftp://acme.example", false},
|
||||||
|
{"space in path", "/ x", false},
|
||||||
|
{"newline", "https://acme.example/\nx", false},
|
||||||
|
{"leading tab", "\t/home", false},
|
||||||
|
{"backslash host", "/\\acme.example", false},
|
||||||
|
{"mailto opaque", "mailto:docs@acme.example", false},
|
||||||
|
{"control character", "/home\x00", false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := checkSiteURL(tc.raw)
|
||||||
|
if tc.ok && err != nil {
|
||||||
|
t.Fatalf("checkSiteURL(%q) = %v, want accepted", tc.raw, err)
|
||||||
|
}
|
||||||
|
if !tc.ok && !errors.Is(err, errSiteURL) {
|
||||||
|
t.Fatalf("checkSiteURL(%q) = %v, want errSiteURL", tc.raw, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSiteLabel(t *testing.T) {
|
||||||
|
for _, tc := range []struct{ url, label, want string }{
|
||||||
|
{"/", " acme.example ", "acme.example"},
|
||||||
|
{"https://acme.example/docs", "Acme", "Acme"},
|
||||||
|
{"https://acme.example/docs", "", "acme.example"},
|
||||||
|
{"https://acme.example/docs", " ", "acme.example"},
|
||||||
|
{"http://acme.example:8080/", "", "acme.example:8080"},
|
||||||
|
{"/", "", "Home"},
|
||||||
|
{"/home", "", "Home"},
|
||||||
|
} {
|
||||||
|
if got := siteLabel(tc.url, tc.label); got != tc.want {
|
||||||
|
t.Errorf("siteLabel(%q, %q) = %q, want %q", tc.url, tc.label, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSiteURLPrecedence checks that the --site-url and --site-label options
|
||||||
|
// override site.yaml the way --base-url overrides base_url.
|
||||||
|
func TestSiteURLPrecedence(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
yaml string
|
||||||
|
opts Options
|
||||||
|
wantURL string
|
||||||
|
wantLabel string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "nothing set"},
|
||||||
|
{name: "yaml url, derived label", yaml: "site_url: https://acme.example:8443/x\n", wantURL: "https://acme.example:8443/x", wantLabel: "acme.example:8443"},
|
||||||
|
{name: "yaml url and label", yaml: "site_url: /\nsite_label: Yaml\n", wantURL: "/", wantLabel: "Yaml"},
|
||||||
|
{name: "option url wins", yaml: "site_url: https://yaml.example\n", opts: Options{SiteURL: "/"}, wantURL: "/", wantLabel: "Home"},
|
||||||
|
{name: "option label wins", yaml: "site_url: /\nsite_label: Yaml\n", opts: Options{SiteLabel: "Opt"}, wantURL: "/", wantLabel: "Opt"},
|
||||||
|
{name: "option url keeps yaml label", yaml: "site_url: /\nsite_label: Yaml\n", opts: Options{SiteURL: "https://opt.example"}, wantURL: "https://opt.example", wantLabel: "Yaml"},
|
||||||
|
{name: "options only", opts: Options{SiteURL: "https://opt.example/", SiteLabel: "Opt"}, wantURL: "https://opt.example/", wantLabel: "Opt"},
|
||||||
|
{name: "option label without url", opts: Options{SiteLabel: "x"}, wantErr: "docsite: --site-label needs --site-url or site.yaml site_url"},
|
||||||
|
{name: "invalid option url", yaml: "site_url: /\n", opts: Options{SiteURL: "javascript:alert(1)"}, wantErr: "docsite: --site-url: must be an http(s) URL"},
|
||||||
|
{name: "protocol-relative option url", opts: Options{SiteURL: "//acme.example"}, wantErr: "docsite: --site-url: "},
|
||||||
|
{name: "two-line option label", opts: Options{SiteURL: "/", SiteLabel: "a\nb"}, wantErr: "docsite: --site-label: must be one non-empty line"},
|
||||||
|
{name: "blank option label", opts: Options{SiteURL: "/", SiteLabel: " "}, wantErr: "docsite: --site-label: must be one non-empty line"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := themeTree(t)
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "docs", "site.yaml"), []byte(fixtureSite+tc.yaml), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
opts := tc.opts
|
||||||
|
opts.Root, opts.Commands, opts.Out = root, fixtureCommands, filepath.Join(t.TempDir(), "site")
|
||||||
|
s, problems, err := load(opts)
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||||
|
t.Fatalf("load error = %v, want %q", err, tc.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil || len(problems) > 0 {
|
||||||
|
t.Fatalf("load: %v %q", err, problemLines(problems))
|
||||||
|
}
|
||||||
|
if s.siteURL != tc.wantURL || s.siteLabel != tc.wantLabel {
|
||||||
|
t.Fatalf("site link = %q %q, want %q %q", s.siteURL, s.siteLabel, tc.wantURL, tc.wantLabel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,35 +77,44 @@ func assertCSPSafe(t *testing.T, name, html string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSiteLink checks the optional link back to the main site on a page
|
// TestSiteLink checks the optional link back to the main site on the home
|
||||||
// and on the 404 page: absent without a site URL, labelled explicitly, by
|
// page, a section page and the 404 page: absent without a site URL (the
|
||||||
// the URL host, or Home.
|
// header bytes unchanged), labelled explicitly, by the URL host, or Home,
|
||||||
|
// and always HTML-escaped.
|
||||||
func TestSiteLink(t *testing.T) {
|
func TestSiteLink(t *testing.T) {
|
||||||
|
// unsetHeader is the wordmark's closing tag followed directly by the
|
||||||
|
// spacer: the header bytes from before site_url existed.
|
||||||
|
const unsetHeader = `<span class="wordmark-cms">CMS</span></span></a>` + "\n" + `<div class="header-spacer"></div>`
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
opts Options
|
opts Options
|
||||||
want []string
|
want []string
|
||||||
|
deny []string
|
||||||
}{
|
}{
|
||||||
{"unset", Options{}, nil},
|
{"unset", Options{}, []string{unsetHeader}, []string{"site-link"}},
|
||||||
{"explicit label", Options{SiteURL: "/", SiteLabel: "acme.example"}, []string{`<a class="site-link" href="/"`, `<span class="site-link-label">acme.example</span>`}},
|
{"explicit label", Options{SiteURL: "/", SiteLabel: "acme.example"}, []string{`<a class="site-link" href="/" aria-label="acme.example">`, `<span class="site-link-label">acme.example</span>`}, []string{unsetHeader}},
|
||||||
{"host label", Options{SiteURL: "https://acme.example/docs"}, []string{`<a class="site-link" href="https://acme.example/docs"`, `<span class="site-link-label">acme.example</span>`}},
|
{"host label", Options{SiteURL: "https://acme.example/docs"}, []string{`<a class="site-link" href="https://acme.example/docs"`, `<span class="site-link-label">acme.example</span>`}, nil},
|
||||||
{"home label", Options{SiteURL: "/"}, []string{`<a class="site-link" href="/"`, `<span class="site-link-label">Home</span>`}},
|
{"host and port label", Options{SiteURL: "http://acme.example:8080/"}, []string{`<a class="site-link" href="http://acme.example:8080/"`, `<span class="site-link-label">acme.example:8080</span>`}, nil},
|
||||||
|
{"home label", Options{SiteURL: "/"}, []string{`<a class="site-link" href="/"`, `<span class="site-link-label">Home</span>`}, nil},
|
||||||
|
{"escaped label", Options{SiteURL: "/", SiteLabel: "<b>&"}, []string{`aria-label="<b>&"`, `<span class="site-link-label"><b>&</span>`}, []string{"<b>&"}},
|
||||||
} {
|
} {
|
||||||
opts := tc.opts
|
opts := tc.opts
|
||||||
opts.Root, opts.Commands, opts.Out = themeTree(t), fixtureCommands, filepath.Join(t.TempDir(), "site")
|
opts.Root, opts.Commands, opts.Out = themeTree(t), fixtureCommands, filepath.Join(t.TempDir(), "site")
|
||||||
if _, problems, err := Build(opts); err != nil || len(problems) > 0 {
|
if _, problems, err := Build(opts); err != nil || len(problems) > 0 {
|
||||||
t.Fatalf("%s: Build: %v %q", tc.name, err, problemLines(problems))
|
t.Fatalf("%s: Build: %v %q", tc.name, err, problemLines(problems))
|
||||||
}
|
}
|
||||||
for _, name := range []string{"index.html", "404.html"} {
|
for _, name := range []string{"index.html", "setup/start.html", "404.html"} {
|
||||||
html := readOut(t, opts.Out, name)
|
html := readOut(t, opts.Out, name)
|
||||||
if tc.want == nil && strings.Contains(html, "site-link") {
|
|
||||||
t.Errorf("%s: %s has a site link", tc.name, name)
|
|
||||||
}
|
|
||||||
for _, w := range tc.want {
|
for _, w := range tc.want {
|
||||||
if !strings.Contains(html, w) {
|
if !strings.Contains(html, w) {
|
||||||
t.Errorf("%s: %s lacks %s", tc.name, name, w)
|
t.Errorf("%s: %s lacks %s", tc.name, name, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, d := range tc.deny {
|
||||||
|
if strings.Contains(html, d) {
|
||||||
|
t.Errorf("%s: %s contains %s", tc.name, name, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,18 @@ ROOT="${PHASE11_2_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|||||||
APP="${PHASE11_2_APP:-$ROOT/../sm-summercmsio-app}"
|
APP="${PHASE11_2_APP:-$ROOT/../sm-summercmsio-app}"
|
||||||
PLUG="$APP/plugins/golem15/summercms"
|
PLUG="$APP/plugins/golem15/summercms"
|
||||||
SITE="$APP/vue-summercmsio-app"
|
SITE="$APP/vue-summercmsio-app"
|
||||||
|
APP_PKG="git.golem15.com/golem15/sm-summercmsio-app"
|
||||||
# Statement coverage floors (SC5).
|
# Statement coverage floors (SC5).
|
||||||
PLUGIN_COVERAGE_MIN=90.0
|
PLUGIN_COVERAGE_MIN=90.0
|
||||||
|
DOCSITE_COVERAGE_MIN=85.0
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
usage:
|
usage:
|
||||||
|
check-phase11.2.sh --framework
|
||||||
check-phase11.2.sh --plugin
|
check-phase11.2.sh --plugin
|
||||||
|
check-phase11.2.sh --app
|
||||||
|
check-phase11.2.sh --site
|
||||||
EOF
|
EOF
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
@@ -125,6 +130,20 @@ coverage_at_least() {
|
|||||||
awk -v t="$total" -v m="$min" 'BEGIN { exit !(t + 0 >= m + 0) }' || refuse "coverage $total% is below $min% in $dir"
|
awk -v t="$total" -v m="$min" 'BEGIN { exit !(t + 0 >= m + 0) }' || refuse "coverage $total% is below $min% in $dir"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Framework tests of the D-41/D-46 site link, plus the docs checker.
|
||||||
|
DOCSITE_TESTS=(TestParseSite TestCheckSiteURL TestSiteLabel TestSiteURLPrecedence TestSiteLink)
|
||||||
|
SUMMER_TESTS=(TestDocsTree TestDocsBuildRealTree TestDocsBuildSiteFlags TestDocsSiteFlagsInHelp)
|
||||||
|
|
||||||
|
run_framework() {
|
||||||
|
(cd "$ROOT" && go vet ./...)
|
||||||
|
named_tests "$ROOT" ./internal/docsite "${DOCSITE_TESTS[@]}" || refuse "framework: internal/docsite named tests"
|
||||||
|
named_tests "$ROOT" ./cmd/summer "${SUMMER_TESTS[@]}" || refuse "framework: cmd/summer named tests"
|
||||||
|
coverage_at_least "$ROOT" "$DOCSITE_COVERAGE_MIN" ./internal/docsite
|
||||||
|
"$ROOT/scripts/check-phase11.1.sh" --docs
|
||||||
|
"$ROOT/scripts/check-phase11.1.sh" --forbidden
|
||||||
|
echo "phase11.2 framework passed"
|
||||||
|
}
|
||||||
|
|
||||||
PLUGIN_TESTS=(TestStaticSmoke TestStaticSite TestStaticDocs TestStaticConditionalAndRange TestStaticNoBlockingHeaders
|
PLUGIN_TESTS=(TestStaticSmoke TestStaticSite TestStaticDocs TestStaticConditionalAndRange TestStaticNoBlockingHeaders
|
||||||
TestStaticRedirectLocations TestStaticMissing404Page TestNewHandlersMissingIndex TestContentType TestSiteImmutable
|
TestStaticRedirectLocations TestStaticMissing404Page TestNewHandlersMissingIndex TestContentType TestSiteImmutable
|
||||||
TestRoutesAssemble TestRoutesFailClosed TestRoutesCoexistWithAdminPatterns TestPluginIdentity TestPluginEmbeddedTree
|
TestRoutesAssemble TestRoutesFailClosed TestRoutesCoexistWithAdminPatterns TestPluginIdentity TestPluginEmbeddedTree
|
||||||
@@ -138,7 +157,46 @@ run_plugin() {
|
|||||||
echo "phase11.2 plugin passed"
|
echo "phase11.2 plugin passed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The D-40 terminal-check helpers (the gated TestTerminalCommands runs in
|
||||||
|
# the terminal stage).
|
||||||
|
APP_TESTS=(TestLoadTerminal TestTerminalScript TestTerminalEnv)
|
||||||
|
|
||||||
|
run_app() {
|
||||||
|
local pkgs
|
||||||
|
need_dir "$APP"
|
||||||
|
go -C "$APP" vet ./...
|
||||||
|
pkgs="$(go -C "$APP" list ./...)"
|
||||||
|
[ "$pkgs" = "$APP_PKG" ] || refuse "app: go list ./... printed '$pkgs', want only $APP_PKG"
|
||||||
|
(unset SUMMERCMS_TERMINAL_CHECK && named_tests "$APP" . "${APP_TESTS[@]}") || refuse "app: named tests"
|
||||||
|
echo "phase11.2 app passed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# run_site installs from the lockfile, generates the static site and runs
|
||||||
|
# the node:test suites (output, terminal and scroll-spy tests).
|
||||||
|
run_site() {
|
||||||
|
local log
|
||||||
|
need_dir "$SITE"
|
||||||
|
command -v pnpm >/dev/null || refuse "site: pnpm not found"
|
||||||
|
pnpm -C "$SITE" install --frozen-lockfile
|
||||||
|
pnpm -C "$SITE" run generate
|
||||||
|
log="$(mktemp)"
|
||||||
|
SCRATCH+=("$log")
|
||||||
|
pnpm -C "$SITE" test >"$log" 2>&1 || {
|
||||||
|
cat "$log" >&2
|
||||||
|
refuse "site: pnpm test failed"
|
||||||
|
}
|
||||||
|
grep -E '^# (tests|pass|fail|skipped|todo|cancelled) ' "$log"
|
||||||
|
grep -qx '# fail 0' "$log" || refuse "site: no '# fail 0' line"
|
||||||
|
grep -qE '^# pass [1-9][0-9]*$' "$log" || refuse "site: no passing tests"
|
||||||
|
grep -qx '# skipped 0' "$log" || refuse "site: skipped tests"
|
||||||
|
grep -qx '# todo 0' "$log" || refuse "site: todo tests"
|
||||||
|
echo "phase11.2 site passed"
|
||||||
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
|
--framework) run_framework ;;
|
||||||
--plugin) run_plugin ;;
|
--plugin) run_plugin ;;
|
||||||
|
--app) run_app ;;
|
||||||
|
--site) run_site ;;
|
||||||
*) usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Reference in New Issue
Block a user