From 1264b5a619df6687104bd095e571a7d37adb6eee Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 19 Jun 2025 08:20:03 +0200 Subject: [PATCH] Actions: Introduce A11y test (#106806) --- .github/workflows/pr-e2e-tests.yml | 40 ++- e2e/internal/cmd/a11y/cmd.go | 135 ++++++++++ e2e/internal/cmd/cypress/cmd.go | 251 ++++++++++++++++++ e2e/internal/cmd/root.go | 25 ++ e2e/internal/fpaths/root.go | 35 +++ e2e/internal/outs/wrapping.go | 65 +++++ e2e/main.go | 334 +----------------------- package.json | 1 + pkg/build/a11y/main.go | 152 +++++++++++ pkg/build/a11y/run.go | 25 ++ pkg/build/a11y/service.go | 97 +++++++ pkg/build/e2e/run.go | 2 +- scripts/grafana-server/kill-server | 2 +- scripts/grafana-server/start-server | 2 +- scripts/grafana-server/variables | 2 +- scripts/grafana-server/wait-for-grafana | 2 +- yarn.lock | 334 +++++++++++++++++++++++- 17 files changed, 1153 insertions(+), 351 deletions(-) create mode 100644 e2e/internal/cmd/a11y/cmd.go create mode 100644 e2e/internal/cmd/cypress/cmd.go create mode 100644 e2e/internal/cmd/root.go create mode 100644 e2e/internal/fpaths/root.go create mode 100644 e2e/internal/outs/wrapping.go create mode 100644 pkg/build/a11y/main.go create mode 100644 pkg/build/a11y/run.go create mode 100644 pkg/build/a11y/service.go diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index 778f0c532eb..3c988749ed0 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -28,7 +28,6 @@ jobs: persist-credentials: false - uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e with: - version: "8.0.0" verb: run args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt - run: mv "$(cat out.txt)" grafana.tar.gz @@ -119,7 +118,6 @@ jobs: - name: Run E2E tests uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e with: - version: "8.0.0" verb: run args: go run ./pkg/build/e2e --package=grafana.tar.gz --suite=${{ matrix.path }} @@ -139,12 +137,50 @@ jobs: path: videos retention-days: 1 + run-a11y-test: + needs: + - build-grafana + - build-e2e-runner + name: A11y test + runs-on: ubuntu-latest-8-cores + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.build-grafana.outputs.artifact }} + - uses: actions/download-artifact@v4 + with: + name: ${{ needs.build-e2e-runner.outputs.artifact }} + - name: chmod +x + run: chmod +x ./e2e-runner + - name: Run PR a11y test + if: github.event_name == 'pull_request' + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + with: + verb: run + args: go run ./pkg/build/a11y --package=grafana.tar.gz + --flags="--json --config ./.pa11yci-pr.conf.js" + - name: Run non-PR a11y test + if: github.event_name != 'pull_request' + uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e + with: + verb: run + args: go run ./pkg/build/a11y --package=grafana.tar.gz + --flags="--json --config ./.pa11yci.conf.js" + # This is the job that is actually required by rulesets. # We want to only require one job instead of all the individual tests. # Future work also allows us to start skipping some tests based on changed files. required-e2e-tests: needs: - run-e2e-tests + # a11y test is not listed on purpose: it is not an important E2E test. + # It is also totally fine to fail right now. # always() is the best function here. # success() || failure() will skip this function if any need is also skipped. # That means conditional test suites will fail the entire requirement check. diff --git a/e2e/internal/cmd/a11y/cmd.go b/e2e/internal/cmd/a11y/cmd.go new file mode 100644 index 00000000000..3015183890b --- /dev/null +++ b/e2e/internal/cmd/a11y/cmd.go @@ -0,0 +1,135 @@ +package a11y + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "path" + + "github.com/grafana/grafana/e2e/internal/fpaths" + "github.com/grafana/grafana/e2e/internal/outs" + "github.com/urfave/cli/v3" +) + +func NewCmd() *cli.Command { + return &cli.Command{ + Name: "a11y", + Usage: "Run accessibility tests on the Grafana frontend", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Usage: "Path to the accessibility test configuration file", + Required: true, + TakesFile: true, + }, + &cli.BoolFlag{ + Name: "json", + Usage: "Output results in JSON format", + Value: false, + }, + + &cli.StringFlag{ + Name: "grafana-host", + Usage: "Host for the Grafana server", + Value: "localhost", + }, + &cli.Uint16Flag{ + Name: "grafana-port", + Usage: "Port for the Grafana server", + Value: 3001, + }, + + &cli.BoolFlag{ + Name: "start-grafana", + Usage: "Start and wait for Grafana before running the tests", + Value: true, + Category: "Grafana Server", + }, + &cli.StringFlag{ + Name: "license-path", + Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)", + Value: "", + TakesFile: true, + Category: "Grafana Server", + }, + }, + Action: runAction, + } +} + +func runAction(ctx context.Context, c *cli.Command) error { + cfgPath, err := fpaths.NormalisePath(c.String("config")) + if err != nil { + return fmt.Errorf("failed to normalise config path %q: %w", c.String("config"), err) + } + + repoRoot, err := fpaths.RepoRoot(ctx, ".") + if err != nil { + return fmt.Errorf("failed to get repository root: %w", err) + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if c.Bool("start-grafana") { + startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server") + waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana") + go func() { + defer cancel() + var args []string + if c.String("license-path") != "" { + args = append(args, c.String("license-path")) + } + //nolint:gosec + cmd := exec.CommandContext(ctx, startServerPath, args...) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + cmd.Stdout = prefixGrafana(os.Stdout) + cmd.Stderr = prefixGrafana(os.Stderr) + cmd.Stdin = nil + + if err := cmd.Run(); err != nil { + fmt.Println("Error running Grafana:", err) + } + }() + + //nolint:gosec + cmd := exec.CommandContext(ctx, waitForGrafanaPath) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + cmd.Stdout = prefixGrafana(os.Stdout) + cmd.Stderr = prefixGrafana(os.Stderr) + cmd.Stdin = nil + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to wait for Grafana: %w", err) + } + } + + args := []string{"run", "pa11y-ci", "--config", cfgPath} + if c.Bool("json") { + args = append(args, "--json") + } + //nolint:gosec + cmd := exec.CommandContext(ctx, "yarn", args...) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, + fmt.Sprintf("HOST=%s", c.String("grafana-host")), + fmt.Sprintf("PORT=%d", c.Uint16("grafana-port"))) + cmd.Stdout = prefixA11y(os.Stdout) + cmd.Stderr = prefixA11y(os.Stderr) + cmd.Stdin = os.Stdin + return cmd.Run() +} + +func prefixA11y(w io.Writer) io.Writer { + return outs.Prefix(w, "A11y", outs.CyanColor) +} + +func prefixGrafana(w io.Writer) io.Writer { + return outs.Prefix(w, "Grafana", outs.YellowColor) +} diff --git a/e2e/internal/cmd/cypress/cmd.go b/e2e/internal/cmd/cypress/cmd.go new file mode 100644 index 00000000000..ec4d836cd15 --- /dev/null +++ b/e2e/internal/cmd/cypress/cmd.go @@ -0,0 +1,251 @@ +package cypress + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "path" + "regexp" + "strings" + "time" + + "github.com/grafana/grafana/e2e/internal/fpaths" + "github.com/grafana/grafana/e2e/internal/outs" + "github.com/urfave/cli/v3" +) + +func NewCmd() *cli.Command { + return &cli.Command{ + Name: "cypress", + Usage: "Run a Cypress test suite", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "command", + Usage: "Cypress command to run. 'open' can be useful for development (enum: run, open)", + Value: "run", + Validator: func(s string) error { + if s != "run" && s != "open" { + return fmt.Errorf("invalid command: %s, must be 'run' or 'open'", s) + } + return nil + }, + }, + &cli.StringFlag{ + Name: "browser", + Usage: "Browser to run tests with (e.g.: chrome, electron)", + Value: "chrome", + }, + &cli.StringFlag{ + Name: "grafana-base-url", + Usage: "Base URL for Grafana", + Value: "http://localhost:3001", + }, + &cli.BoolFlag{ + Name: "cypress-video", + Usage: "Enable Cypress video recordings", + Value: false, + }, + &cli.BoolFlag{ + Name: "smtp-plugin", + Usage: "Enable SMTP plugin", + Value: false, + }, + &cli.BoolFlag{ + Name: "benchmark-plugin", + Usage: "Enable Benchmark plugin", + Value: false, + }, + &cli.BoolFlag{ + Name: "slowmo", + Usage: "Slow down the test run", + Value: false, + }, + &cli.StringSliceFlag{ + Name: "env", + Usage: "Additional Cypress environment variables to set (format: KEY=VALUE)", + Validator: func(s []string) error { + pattern := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*`) + for _, v := range s { + if !pattern.MatchString(v) { + return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v) + } + } + return nil + }, + }, + &cli.StringSliceFlag{ + Name: "parameters", + Usage: "Additional parameters to pass to the Cypress command (e.g. --headed)", + }, + &cli.DurationFlag{ + Name: "timeout", + Usage: "Timeout for the Cypress command (precision: milliseconds)", + Value: time.Second * 30, + Validator: func(d time.Duration) error { + if d < 0 { + return fmt.Errorf("timeout must be a positive duration") + } + if d.Round(time.Millisecond) != d { + return fmt.Errorf("timeout must be a whole number of milliseconds") + } + return nil + }, + }, + + &cli.BoolFlag{ + Name: "start-grafana", + Usage: "Start and wait for Grafana before running the tests", + Value: true, + Category: "Grafana Server", + }, + &cli.StringFlag{ + Name: "license-path", + Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)", + Value: "", + TakesFile: true, + Category: "Grafana Server", + }, + &cli.BoolFlag{ + Name: "image-renderer", + Usage: "Install the image renderer plugin (requires --start-grafana)", + Category: "Grafana Server", + }, + + &cli.StringFlag{ + Name: "suite", + Usage: "Path to the suite to run (e.g. './e2e/dashboards-suite')", + TakesFile: true, + Required: true, + }, + }, + Action: runAction, + } +} + +func runAction(ctx context.Context, c *cli.Command) error { + suitePath := c.String("suite") + suitePath, err := fpaths.NormalisePath(suitePath) + if err != nil { + return fmt.Errorf("failed to normalise suite path: %w", err) + } + + repoRoot, err := fpaths.RepoRoot(ctx, suitePath) + if err != nil { + return fmt.Errorf("failed to get git repo root: %w", err) + } + + screenshotsFolder := path.Join(suitePath, "screenshots") + videosFolder := path.Join(suitePath, "videos") + fileServerFolder := path.Join(repoRoot, "e2e", "cypress") + fixturesFolder := path.Join(fileServerFolder, "fixtures") + downloadsFolder := path.Join(fileServerFolder, "downloads") + benchmarkPluginResultsFolder := path.Join(suitePath, "benchmark-results") + reporter := path.Join(repoRoot, "e2e", "log-reporter.js") + + env := map[string]string{ + "BENCHMARK_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("benchmark-plugin")), + "SMTP_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("smtp-plugin")), + "BENCHMARK_PLUGIN_RESULTS_FOLDER": benchmarkPluginResultsFolder, + "SLOWMO": "0", + "BASE_URL": c.String("grafana-base-url"), + } + for _, v := range c.StringSlice("env") { + parts := strings.SplitN(v, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v) + } + env[parts[0]] = parts[1] + } + + cypressConfig := map[string]string{ + "screenshotsFolder": screenshotsFolder, + "fixturesFolder": fixturesFolder, + "videosFolder": videosFolder, + "downloadsFolder": downloadsFolder, + "fileServerFolder": fileServerFolder, + "reporter": reporter, + + "specPattern": path.Join(suitePath, "*.spec.ts"), + "defaultCommandTimeout": fmt.Sprintf("%d", c.Duration("timeout").Milliseconds()), + "viewportWidth": "1920", + "viewportHeight": "1080", + "trashAssetsBeforeRuns": "false", + "baseUrl": c.String("grafana-base-url"), + "video": fmt.Sprintf("%t", c.Bool("cypress-video")), + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if c.Bool("start-grafana") { + startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server") + waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana") + go func() { + defer cancel() + var args []string + if c.String("license-path") != "" { + args = append(args, c.String("license-path")) + } + //nolint:gosec + cmd := exec.CommandContext(ctx, startServerPath, args...) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + if c.Bool("image-renderer") { + cmd.Env = append(cmd.Env, "INSTALL_IMAGE_RENDERER=true") + } + cmd.Stdout = prefixGrafana(os.Stdout) + cmd.Stderr = prefixGrafana(os.Stderr) + cmd.Stdin = nil + + if err := cmd.Run(); err != nil { + fmt.Println("Error running Grafana:", err) + } + }() + + //nolint:gosec + cmd := exec.CommandContext(ctx, waitForGrafanaPath) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + cmd.Stdout = prefixGrafana(os.Stdout) + cmd.Stderr = prefixGrafana(os.Stderr) + cmd.Stdin = nil + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to wait for Grafana: %w", err) + } + } + + args := []string{"run", "cypress", c.String("command"), + "--env", joinCypressCfg(env), + "--config", joinCypressCfg(cypressConfig), + "--browser", c.String("browser")} + args = append(args, c.StringSlice("parameters")...) + //nolint:gosec + cmd := exec.CommandContext(ctx, "yarn", args...) + cmd.Dir = repoRoot + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) + cmd.Stdout = prefixCypress(os.Stdout) + cmd.Stderr = prefixCypress(os.Stderr) + cmd.Stdin = os.Stdin + return cmd.Run() +} + +func joinCypressCfg(cfg map[string]string) string { + config := make([]string, 0, len(cfg)) + for k, v := range cfg { + config = append(config, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(config, ",") +} + +func prefixCypress(w io.Writer) io.Writer { + return outs.Prefix(w, "Cypress", outs.CyanColor) +} + +func prefixGrafana(w io.Writer) io.Writer { + return outs.Prefix(w, "Grafana", outs.YellowColor) +} diff --git a/e2e/internal/cmd/root.go b/e2e/internal/cmd/root.go new file mode 100644 index 00000000000..d1d679e0a0f --- /dev/null +++ b/e2e/internal/cmd/root.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/grafana/grafana/e2e/internal/cmd/a11y" + "github.com/grafana/grafana/e2e/internal/cmd/cypress" + "github.com/urfave/cli/v3" +) + +func Root() *cli.Command { + return &cli.Command{ + Name: "e2e", + Usage: "Run an end-to-end test suite", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "timezone", + Usage: "Timezone to set for all containers (e.g. 'America/New_York')", + Value: "Pacific/Honolulu", + }, + }, + Commands: []*cli.Command{ + a11y.NewCmd(), + cypress.NewCmd(), + }, + } +} diff --git a/e2e/internal/fpaths/root.go b/e2e/internal/fpaths/root.go new file mode 100644 index 00000000000..7f01c46a903 --- /dev/null +++ b/e2e/internal/fpaths/root.go @@ -0,0 +1,35 @@ +package fpaths + +import ( + "context" + "fmt" + "os/exec" + "path" + "path/filepath" + "strings" +) + +// RepoRoot finds the root directory of the git repository. +func RepoRoot(ctx context.Context, dir string) (string, error) { + cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel") + cmd.Dir = dir + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get git repo root: %w", err) + } + p := strings.TrimSpace(string(out)) + p, err = NormalisePath(p) + if err != nil { + return "", fmt.Errorf("failed to normalise git repo root path: %w", err) + } + return p, nil +} + +// NormalisePath converts a path to an absolute path, cleans it, and converts it to a forward-slash format. +func NormalisePath(p string) (string, error) { + absPath, err := filepath.Abs(p) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + return path.Clean(filepath.ToSlash(absPath)), nil +} diff --git a/e2e/internal/outs/wrapping.go b/e2e/internal/outs/wrapping.go new file mode 100644 index 00000000000..347226f841e --- /dev/null +++ b/e2e/internal/outs/wrapping.go @@ -0,0 +1,65 @@ +package outs + +import ( + "bytes" + "io" + "os" + "sync" +) + +const ( + ResetColor = "\033[0m" + YellowColor = "\033[0;33m" + CyanColor = "\033[0;36m" +) + +func Prefix(w io.Writer, name, colour string) io.Writer { + if _, ok := os.LookupEnv("CI"); ok { + return newWrappingOutput(name+": ", "", w) + } + + return newWrappingOutput(colour+name+": ", ResetColor, w) +} + +var _ io.Writer = (*wrappingOutput)(nil) + +type wrappingOutput struct { + prefix string + suffix string + mu *sync.Mutex + inner io.Writer + writtenPrefix bool +} + +func newWrappingOutput(prefix, suffix string, inner io.Writer) *wrappingOutput { + return &wrappingOutput{ + prefix: prefix, + suffix: suffix, + mu: &sync.Mutex{}, + inner: inner, + } +} + +func (p *wrappingOutput) Write(b []byte) (int, error) { + p.mu.Lock() + defer p.mu.Unlock() + + for line := range bytes.Lines(b) { + if !p.writtenPrefix { + if _, err := p.inner.Write([]byte(p.prefix)); err != nil { + return 0, err + } + p.writtenPrefix = true + } + if _, err := p.inner.Write(line); err != nil { + return 0, err + } + if bytes.HasSuffix(line, []byte("\n")) { + p.writtenPrefix = false + if _, err := p.inner.Write([]byte(p.suffix)); err != nil { + return 0, err + } + } + } + return len(b), nil +} diff --git a/e2e/main.go b/e2e/main.go index 130d8d85426..95185485b34 100644 --- a/e2e/main.go +++ b/e2e/main.go @@ -1,351 +1,21 @@ package main import ( - "bytes" "context" "fmt" - "io" "os" - "os/exec" "os/signal" - "path" - "path/filepath" - "regexp" - "strings" - "sync" - "time" - "github.com/urfave/cli/v3" + "github.com/grafana/grafana/e2e/internal/cmd" ) func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() - if err := Run().Run(ctx, os.Args); err != nil { + if err := cmd.Root().Run(ctx, os.Args); err != nil { cancel() fmt.Println(err) os.Exit(1) } } - -func Run() *cli.Command { - var suitePath string - - return &cli.Command{ - Name: "e2e", - Usage: "Run the test suite", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "command", - Usage: "Cypress command to run. 'open' can be useful for development (enum: run, open)", - Value: "run", - Validator: func(s string) error { - if s != "run" && s != "open" { - return fmt.Errorf("invalid command: %s, must be 'run' or 'open'", s) - } - return nil - }, - }, - &cli.StringFlag{ - Name: "browser", - Usage: "Browser to run tests with (e.g.: chrome, electron)", - Value: "chrome", - }, - &cli.StringFlag{ - Name: "grafana-base-url", - Usage: "Base URL for Grafana", - Value: "http://localhost:3001", - }, - &cli.BoolFlag{ - Name: "cypress-video", - Usage: "Enable Cypress video recordings", - Value: false, - }, - &cli.BoolFlag{ - Name: "smtp-plugin", - Usage: "Enable SMTP plugin", - Value: false, - }, - &cli.BoolFlag{ - Name: "benchmark-plugin", - Usage: "Enable Benchmark plugin", - Value: false, - }, - &cli.BoolFlag{ - Name: "slowmo", - Usage: "Slow down the test run", - Value: false, - }, - &cli.StringSliceFlag{ - Name: "env", - Usage: "Additional Cypress environment variables to set (format: KEY=VALUE)", - Validator: func(s []string) error { - pattern := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*`) - for _, v := range s { - if !pattern.MatchString(v) { - return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v) - } - } - return nil - }, - }, - &cli.StringSliceFlag{ - Name: "parameters", - Usage: "Additional parameters to pass to the Cypress command (e.g. --headed)", - }, - &cli.DurationFlag{ - Name: "timeout", - Usage: "Timeout for the Cypress command (precision: milliseconds)", - Value: time.Second * 30, - Validator: func(d time.Duration) error { - if d < 0 { - return fmt.Errorf("timeout must be a positive duration") - } - if d.Round(time.Millisecond) != d { - return fmt.Errorf("timeout must be a whole number of milliseconds") - } - return nil - }, - }, - &cli.StringFlag{ - Name: "timezone", - Usage: "Timezone to set for the Cypress run (e.g. 'America/New_York')", - Value: "Pacific/Honolulu", - }, - - &cli.BoolFlag{ - Name: "start-grafana", - Usage: "Start and wait for Grafana before running the tests", - Value: true, - Category: "Grafana Server", - }, - &cli.StringFlag{ - Name: "license-path", - Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)", - Value: "", - TakesFile: true, - Category: "Grafana Server", - }, - &cli.BoolFlag{ - Name: "image-renderer", - Usage: "Install the image renderer plugin (requires --start-grafana)", - Category: "Grafana Server", - }, - - &cli.StringFlag{ - Name: "suite", - Usage: "Path to the suite to run (e.g. './e2e/dashboards-suite')", - TakesFile: true, - Required: true, - Destination: &suitePath, - }, - }, - Action: runAction, - } -} - -func runAction(ctx context.Context, c *cli.Command) error { - suitePath := c.String("suite") - suitePath, err := normalisePath(suitePath) - if err != nil { - return fmt.Errorf("failed to normalise suite path: %w", err) - } - - repoRoot, err := gitRepoRoot(ctx, suitePath) - if err != nil { - return fmt.Errorf("failed to get git repo root: %w", err) - } - - screenshotsFolder := path.Join(suitePath, "screenshots") - videosFolder := path.Join(suitePath, "videos") - fileServerFolder := path.Join(repoRoot, "e2e", "cypress") - fixturesFolder := path.Join(fileServerFolder, "fixtures") - downloadsFolder := path.Join(fileServerFolder, "downloads") - benchmarkPluginResultsFolder := path.Join(suitePath, "benchmark-results") - reporter := path.Join(repoRoot, "e2e", "log-reporter.js") - - env := map[string]string{ - "BENCHMARK_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("benchmark-plugin")), - "SMTP_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("smtp-plugin")), - "BENCHMARK_PLUGIN_RESULTS_FOLDER": benchmarkPluginResultsFolder, - "SLOWMO": "0", - "BASE_URL": c.String("grafana-base-url"), - } - for _, v := range c.StringSlice("env") { - parts := strings.SplitN(v, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v) - } - env[parts[0]] = parts[1] - } - - cypressConfig := map[string]string{ - "screenshotsFolder": screenshotsFolder, - "fixturesFolder": fixturesFolder, - "videosFolder": videosFolder, - "downloadsFolder": downloadsFolder, - "fileServerFolder": fileServerFolder, - "reporter": reporter, - - "specPattern": path.Join(suitePath, "*.spec.ts"), - "defaultCommandTimeout": fmt.Sprintf("%d", c.Duration("timeout").Milliseconds()), - "viewportWidth": "1920", - "viewportHeight": "1080", - "trashAssetsBeforeRuns": "false", - "baseUrl": c.String("grafana-base-url"), - "video": fmt.Sprintf("%t", c.Bool("cypress-video")), - } - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - if c.Bool("start-grafana") { - startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server") - waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana") - go func() { - var args []string - if c.String("license-path") != "" { - args = append(args, c.String("license-path")) - } - //nolint:gosec - cmd := exec.CommandContext(ctx, startServerPath, args...) - cmd.Dir = repoRoot - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) - if c.Bool("image-renderer") { - cmd.Env = append(cmd.Env, "INSTALL_IMAGE_RENDERER=true") - } - cmd.Stdout = prefixGrafana(os.Stdout) - cmd.Stderr = prefixGrafana(os.Stderr) - cmd.Stdin = nil - - if err := cmd.Run(); err != nil { - fmt.Println("Error running Grafana:", err) - } - }() - - //nolint:gosec - cmd := exec.CommandContext(ctx, waitForGrafanaPath) - cmd.Dir = repoRoot - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) - cmd.Stdout = prefixGrafana(os.Stdout) - cmd.Stderr = prefixGrafana(os.Stderr) - cmd.Stdin = nil - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to wait for Grafana: %w", err) - } - } - - args := []string{"run", "cypress", c.String("command"), - "--env", joinCypressCfg(env), - "--config", joinCypressCfg(cypressConfig), - "--browser", c.String("browser")} - args = append(args, c.StringSlice("parameters")...) - //nolint:gosec - cmd := exec.CommandContext(ctx, "yarn", args...) - cmd.Dir = repoRoot - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone"))) - cmd.Stdout = prefixCypress(os.Stdout) - cmd.Stderr = prefixCypress(os.Stderr) - cmd.Stdin = os.Stdin - return cmd.Run() -} - -func gitRepoRoot(ctx context.Context, dir string) (string, error) { - cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel") - cmd.Dir = dir - out, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to get git repo root: %w", err) - } - p := strings.TrimSpace(string(out)) - p, err = normalisePath(p) - if err != nil { - return "", fmt.Errorf("failed to normalise git repo root path: %w", err) - } - return p, nil -} - -func normalisePath(p string) (string, error) { - absPath, err := filepath.Abs(p) - if err != nil { - return "", fmt.Errorf("failed to get absolute path: %w", err) - } - return path.Clean(filepath.ToSlash(absPath)), nil -} - -func joinCypressCfg(cfg map[string]string) string { - config := make([]string, 0, len(cfg)) - for k, v := range cfg { - config = append(config, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(config, ",") -} - -const ( - resetColor = "\033[0m" - yellowColor = "\033[0;33m" - cyanColor = "\033[0;36m" -) - -func prefixCypress(w io.Writer) io.Writer { - if _, ok := os.LookupEnv("CI"); ok { - return w - } - - return newWrappingOutput(cyanColor+"Cypress: ", resetColor, w) -} - -func prefixGrafana(w io.Writer) io.Writer { - if _, ok := os.LookupEnv("CI"); ok { - return w - } - - return newWrappingOutput(yellowColor+"Grafana: ", resetColor, w) -} - -var _ io.Writer = (*wrappingOutput)(nil) - -type wrappingOutput struct { - prefix string - suffix string - mu *sync.Mutex - inner io.Writer - writtenPrefix bool -} - -func newWrappingOutput(prefix, suffix string, inner io.Writer) *wrappingOutput { - return &wrappingOutput{ - prefix: prefix, - suffix: suffix, - mu: &sync.Mutex{}, - inner: inner, - } -} - -func (p *wrappingOutput) Write(b []byte) (int, error) { - p.mu.Lock() - defer p.mu.Unlock() - - for line := range bytes.Lines(b) { - if !p.writtenPrefix { - if _, err := p.inner.Write([]byte(p.prefix)); err != nil { - return 0, err - } - p.writtenPrefix = true - } - if _, err := p.inner.Write(line); err != nil { - return 0, err - } - if bytes.HasSuffix(line, []byte("\n")) { - p.writtenPrefix = false - if _, err := p.inner.Write([]byte(p.suffix)); err != nil { - return 0, err - } - } - } - return len(b), nil -} diff --git a/package.json b/package.json index 87122173c41..146176a19f1 100644 --- a/package.json +++ b/package.json @@ -224,6 +224,7 @@ "node-notifier": "10.0.1", "nx": "20.7.1", "openapi-types": "^12.1.3", + "pa11y-ci": "^3.1.0", "pdf-parse": "^1.1.1", "plop": "^4.0.1", "postcss": "8.5.1", diff --git a/pkg/build/a11y/main.go b/pkg/build/a11y/main.go new file mode 100644 index 00000000000..acc74e26248 --- /dev/null +++ b/pkg/build/a11y/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "path/filepath" + + "dagger.io/dagger" + "github.com/urfave/cli/v3" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := NewApp().Run(ctx, os.Args); err != nil { + cancel() + fmt.Println(err) + os.Exit(1) + } +} + +func NewApp() *cli.Command { + return &cli.Command{ + Name: "a11y", + Usage: "Run Grafana accessibility tests", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "grafana-dir", + Usage: "Path to the grafana/grafana clone directory", + Value: ".", + Validator: mustBeDir("grafana-dir"), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "package", + Usage: "Path to the grafana tar.gz package", + Value: "grafana.tar.gz", + Validator: mustBeFile("package", false), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "license", + Usage: "Path to the Grafana Enterprise license file (optional)", + Validator: mustBeFile("license", true), + TakesFile: true, + }, + &cli.StringFlag{ + Name: "flags", + Usage: "Flags to pass through to the e2e runner", + }, + }, + Action: run, + } +} + +func run(ctx context.Context, cmd *cli.Command) error { + grafanaDir := cmd.String("grafana-dir") + targzPath := cmd.String("package") + licensePath := cmd.String("license") + runnerFlags := cmd.String("flags") + + d, err := dagger.Connect(ctx) + if err != nil { + return fmt.Errorf("failed to connect to Dagger: %w", err) + } + + yarnCache := d.CacheVolume("yarn") + + //nolint:gosec + nvmrcContents, err := os.ReadFile(filepath.Join(grafanaDir, ".nvmrc")) + if err != nil { + return fmt.Errorf("failed to read .nvmrc file: %w", err) + } + nodeVersion := string(nvmrcContents) + + grafana := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{ + Exclude: []string{"node_modules", "*.tar.gz"}, + }) + targz := d.Host().File(targzPath) + + var license *dagger.File + if licensePath != "" { + license = d.Host().File(licensePath) + } + + svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{ + GrafanaDir: grafana, + GrafanaTarGz: targz, + License: license, + YarnCache: yarnCache, + NodeVersion: nodeVersion, + }) + if err != nil { + return fmt.Errorf("failed to create Grafana service: %w", err) + } + + c := RunTest(d, svc, grafana, yarnCache, nodeVersion, runnerFlags) + c, err = c.Sync(ctx) + if err != nil { + return fmt.Errorf("failed to run a11y test suite: %w", err) + } + + code, err := c.ExitCode(ctx) + if err != nil { + return fmt.Errorf("failed to get exit code of a11y test suite: %w", err) + } + if code != 0 { + return fmt.Errorf("a11y tests failed with exit code %d", code) + } + + log.Println("a11y tests completed successfully") + return nil +} + +func mustBeFile(arg string, emptyOk bool) func(string) error { + return func(s string) error { + if s == "" { + if emptyOk { + return nil + } + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if stat.IsDir() { + return cli.Exit(arg+" must be a file, not a directory: "+s, 1) + } + return nil + } +} + +func mustBeDir(arg string) func(string) error { + return func(s string) error { + if s == "" { + return cli.Exit(arg+" cannot be empty", 1) + } + stat, err := os.Stat(s) + if err != nil { + return cli.Exit(arg+" does not exist or cannot be read: "+s, 1) + } + if !stat.IsDir() { + return cli.Exit(arg+" must be a directory: "+s, 1) + } + return nil + } +} diff --git a/pkg/build/a11y/run.go b/pkg/build/a11y/run.go new file mode 100644 index 00000000000..4fbe2910018 --- /dev/null +++ b/pkg/build/a11y/run.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + + "dagger.io/dagger" +) + +func RunTest( + d *dagger.Client, + svc *dagger.Service, + src *dagger.Directory, cache *dagger.CacheVolume, + nodeVersion, runnerFlags string) *dagger.Container { + command := fmt.Sprintf( + "./e2e-runner a11y --start-grafana=false"+ + " --grafana-host grafana --grafana-port 3001 %s", runnerFlags) + + return GrafanaFrontend(d, cache, nodeVersion, src). + WithExec([]string{"/bin/sh", "-c", "apt-get update && apt-get install -y git curl"}). + WithExec([]string{"curl", "-LO", "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb"}). + WithExec([]string{"apt-get", "install", "-y", "./google-chrome-stable_current_amd64.deb"}). + WithWorkdir("/src"). + WithServiceBinding("grafana", svc). + WithExec([]string{"/bin/bash", "-c", command}, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny}) +} diff --git a/pkg/build/a11y/service.go b/pkg/build/a11y/service.go new file mode 100644 index 00000000000..72e13f61297 --- /dev/null +++ b/pkg/build/a11y/service.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "dagger.io/dagger" +) + +func NodeImage(version string) string { + return fmt.Sprintf("node:%s-slim", strings.TrimPrefix(strings.TrimSpace(version), "v")) +} + +type GrafanaServiceOpts struct { + GrafanaDir *dagger.Directory + GrafanaTarGz *dagger.File + License *dagger.File + YarnCache *dagger.CacheVolume + NodeVersion string +} + +func Frontend(src *dagger.Directory) *dagger.Directory { + return src. + WithoutFile("go.mod"). + WithoutFile("go.sum"). + WithoutFile("go.work"). + WithoutFile("go.work.sum"). + WithoutDirectory(".github"). + WithoutDirectory("docs"). + WithoutDirectory("pkg"). + WithoutDirectory("apps"). + WithoutDirectory("videos") +} + +func WithGrafanaFrontend(c *dagger.Container, src *dagger.Directory) *dagger.Container { + return c.WithDirectory("/src", Frontend(src), dagger.ContainerWithDirectoryOpts{ + Exclude: []string{ + "*drone*", + "*.go", + "*.md", + }, + }) +} + +func WithYarnCache(c *dagger.Container, cache *dagger.CacheVolume) *dagger.Container { + return c. + WithWorkdir("/src"). + WithMountedCache("/yarn/cache", cache) +} + +func GrafanaFrontend(d *dagger.Client, yarnCache *dagger.CacheVolume, nodeVersion string, grafanaDir *dagger.Directory) *dagger.Container { + container := d.Container().From(NodeImage(nodeVersion)) + container = WithGrafanaFrontend(container, grafanaDir) + return WithYarnCache(container, yarnCache). + WithEnvVariable("YARN_CACHE_FOLDER", "/yarn/cache"). + WithExec([]string{"yarn", "install", "--immutable"}) +} + +func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOpts) (*dagger.Service, error) { + src := GrafanaFrontend(d, opts.YarnCache, opts.NodeVersion, opts.GrafanaDir) + + container := d.Container().From("alpine:3"). + WithExec([]string{"apk", "add", "--no-cache", "bash", "tar", "netcat-openbsd"}). + WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz). + WithExec([]string{"mkdir", "-p", "/src/grafana"}). + WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}). + WithDirectory("/src/grafana/devenv", src.Directory("/src/devenv")). + WithDirectory("/src/grafana/e2e", src.Directory("/src/e2e")). + WithDirectory("/src/grafana/scripts", src.Directory("/src/scripts")). + WithDirectory("/src/grafana/tools", src.Directory("/src/tools")). + WithWorkdir("/src/grafana"). + WithEnvVariable("GF_APP_MODE", "development"). + WithEnvVariable("GF_SERVER_HTTP_PORT", "3001"). + WithEnvVariable("GF_SERVER_ROUTER_LOGGING", "1"). + WithExposedPort(3001) + + var licenseArg string + if opts.License != nil { + container = container.WithMountedFile("/src/license.jwt", opts.License) + licenseArg = "/src/license.jwt" + } + + // We add all GF_ environment variables to allow for overriding Grafana configuration. + // It is unlikely the runner has any such otherwise. + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GF_") { + parts := strings.SplitN(env, "=", 2) + container = container.WithEnvVariable(parts[0], parts[1]) + } + } + + svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "-x", "scripts/grafana-server/start-server", licenseArg}}) + + return svc, nil +} diff --git a/pkg/build/e2e/run.go b/pkg/build/e2e/run.go index db42d397c38..e5d36d34b7b 100644 --- a/pkg/build/e2e/run.go +++ b/pkg/build/e2e/run.go @@ -8,7 +8,7 @@ import ( func RunSuite(d *dagger.Client, svc *dagger.Service, src *dagger.Directory, cache *dagger.CacheVolume, suite, runnerFlags string) *dagger.Container { command := fmt.Sprintf( - "./e2e-runner --start-grafana=false --cypress-video"+ + "./e2e-runner cypress --start-grafana=false --cypress-video"+ " --grafana-base-url http://grafana:3001 --suite %s %s", suite, runnerFlags) return WithYarnCache(WithGrafanaFrontend(d.Container().From("cypress/included:13.1.0"), src), cache). diff --git a/scripts/grafana-server/kill-server b/scripts/grafana-server/kill-server index 7b9e38cda78..702626ecb3c 100755 --- a/scripts/grafana-server/kill-server +++ b/scripts/grafana-server/kill-server @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash . scripts/grafana-server/variables diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server index fdb4499697e..6162439a299 100755 --- a/scripts/grafana-server/start-server +++ b/scripts/grafana-server/start-server @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eo pipefail . scripts/grafana-server/variables diff --git a/scripts/grafana-server/variables b/scripts/grafana-server/variables index 541c4f21f69..b61d316b41c 100644 --- a/scripts/grafana-server/variables +++ b/scripts/grafana-server/variables @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash DEFAULT_RUNDIR=scripts/grafana-server/tmp RUNDIR=${RUNDIR:-$DEFAULT_RUNDIR} diff --git a/scripts/grafana-server/wait-for-grafana b/scripts/grafana-server/wait-for-grafana index a77972fb167..54ff393b7a8 100755 --- a/scripts/grafana-server/wait-for-grafana +++ b/scripts/grafana-server/wait-for-grafana @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eo pipefail . scripts/grafana-server/variables diff --git a/yarn.lock b/yarn.lock index 41a0405f51a..fbd3fdceeb5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11487,6 +11487,15 @@ __metadata: languageName: node linkType: hard +"array-union@npm:^1.0.1": + version: 1.0.2 + resolution: "array-union@npm:1.0.2" + dependencies: + array-uniq: "npm:^1.0.1" + checksum: 10/82cec6421b6e6766556c484835a6d476a873f1b71cace5ab2b4f1b15b1e3162dc4da0d16f7a2b04d4aec18146c6638fe8f661340b31ba8e469fd811a1b45dc8d + languageName: node + linkType: hard + "array-union@npm:^2.1.0": version: 2.1.0 resolution: "array-union@npm:2.1.0" @@ -11494,6 +11503,13 @@ __metadata: languageName: node linkType: hard +"array-uniq@npm:^1.0.1": + version: 1.0.3 + resolution: "array-uniq@npm:1.0.3" + checksum: 10/1625f06b093d8bf279b81adfec6e72951c0857d65b5e3f65f053fffe9f9dd61c2fc52cff57e38a4700817e7e3f01a4faa433d505ea9e33cdae4514c334e0bf9e + languageName: node + linkType: hard + "array.prototype.findlast@npm:^1.2.5": version: 1.2.5 resolution: "array.prototype.findlast@npm:1.2.5" @@ -11655,7 +11671,7 @@ __metadata: languageName: node linkType: hard -"async@npm:^2.6.4": +"async@npm:^2.6.4, async@npm:~2.6.4": version: 2.6.4 resolution: "async@npm:2.6.4" dependencies: @@ -11765,6 +11781,13 @@ __metadata: languageName: node linkType: hard +"axe-core@npm:~4.2.1": + version: 4.2.4 + resolution: "axe-core@npm:4.2.4" + checksum: 10/fc45c087241da298be1e35ea898a1d7189903ddc52ad6f6eb46b27fb47955cb4e097eb854da5b60bedb5eab2df66fabcf334f6bb09105d404e89323cf82e8773 + languageName: node + linkType: hard + "axios@npm:^1, axios@npm:^1.7.9, axios@npm:^1.8.2, axios@npm:^1.8.3": version: 1.8.4 resolution: "axios@npm:1.8.4" @@ -12021,6 +12044,18 @@ __metadata: languageName: node linkType: hard +"bfj@npm:~7.0.2": + version: 7.0.2 + resolution: "bfj@npm:7.0.2" + dependencies: + bluebird: "npm:^3.5.5" + check-types: "npm:^11.1.1" + hoopy: "npm:^0.1.4" + tryer: "npm:^1.0.1" + checksum: 10/e1040fe6aec2afeb6f6c5231bbbc055616fa99c23c5249c7d20a2919507a69d8fd4d82d2245eca5ee08cbfcd3e70ce817328b8a20acda69af4638f1c11343bc7 + languageName: node + linkType: hard + "big.js@npm:^5.2.2": version: 5.2.2 resolution: "big.js@npm:5.2.2" @@ -12090,7 +12125,7 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:^3.7.2": +"bluebird@npm:^3.5.5, bluebird@npm:^3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" checksum: 10/007c7bad22c5d799c8dd49c85b47d012a1fe3045be57447721e6afbd1d5be43237af1db62e26cb9b0d9ba812d2e4ca3bac82f6d7e016b6b88de06ee25ceb96e7 @@ -12308,7 +12343,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.5.0, buffer@npm:^5.7.1": +"buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.7.1": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -12725,6 +12760,13 @@ __metadata: languageName: node linkType: hard +"check-types@npm:^11.1.1": + version: 11.2.3 + resolution: "check-types@npm:11.2.3" + checksum: 10/557e119fa018d7de4e873ada0a6c8917a0f6e0955dc19293396405f5292cfcfe190457557f4cc422e6845d715ef6bbb1d0ab9198ff6735dd96ac50e3ef1e2424 + languageName: node + linkType: hard + "cheerio-select@npm:^2.1.0": version: 2.1.0 resolution: "cheerio-select@npm:2.1.0" @@ -12739,7 +12781,7 @@ __metadata: languageName: node linkType: hard -"cheerio@npm:^1.0.0": +"cheerio@npm:^1.0.0, cheerio@npm:~1.0.0-rc.10": version: 1.0.0 resolution: "cheerio@npm:1.0.0" dependencies: @@ -12793,6 +12835,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d + languageName: node + linkType: hard + "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -13232,7 +13281,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^6.2.0, commander@npm:^6.2.1": +"commander@npm:^6.2.0, commander@npm:^6.2.1, commander@npm:~6.2.1": version: 6.2.1 resolution: "commander@npm:6.2.1" checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e @@ -13246,6 +13295,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:~8.0.0": + version: 8.0.0 + resolution: "commander@npm:8.0.0" + checksum: 10/fdae6767935000431360bc72eefb20e8f0e1c639dc5fac956dc5495877dfbc5d18fcdbd32a0abf59854e013efcb2533957d11dbdbdd42b1d76aa2b54328bb15e + languageName: node + linkType: hard + "comment-parser@npm:1.4.1": version: 1.4.1 resolution: "comment-parser@npm:1.4.1" @@ -14983,6 +15039,13 @@ __metadata: languageName: node linkType: hard +"devtools-protocol@npm:0.0.869402": + version: 0.0.869402 + resolution: "devtools-protocol@npm:0.0.869402" + checksum: 10/fd3d12947047c6d6ef597b86cad1e5cf956780dac0237f0ac5de0091a1f449e3ea1eef10c65eb7c416785fb7d9e588c2588995b91b4db2830d7f23cea421472e + languageName: node + linkType: hard + "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -15551,6 +15614,15 @@ __metadata: languageName: node linkType: hard +"envinfo@npm:~7.8.1": + version: 7.8.1 + resolution: "envinfo@npm:7.8.1" + bin: + envinfo: dist/cli.js + checksum: 10/e7a2d71c7dfe398a4ffda0e844e242d2183ef2627f98e74e4cd71edd2af691c8707a2b34aacef92538c27b3daf9a360d32202f33c0a9f27f767c4e1c6ba8b522 + languageName: node + linkType: hard + "environment@npm:^1.0.0": version: 1.1.0 resolution: "environment@npm:1.1.0" @@ -16647,7 +16719,7 @@ __metadata: languageName: node linkType: hard -"extract-zip@npm:2.0.1": +"extract-zip@npm:2.0.1, extract-zip@npm:^2.0.0": version: 2.0.1 resolution: "extract-zip@npm:2.0.1" dependencies: @@ -16904,6 +16976,13 @@ __metadata: languageName: node linkType: hard +"file-url@npm:^3.0.0": + version: 3.0.0 + resolution: "file-url@npm:3.0.0" + checksum: 10/f15c1bdd81df1a09238f3411f877274d7849703df837ec327c4d1df631314f60036cb700a59d826d8c96b79ff66429d3c758480005e1899c00961541b98d5bfe + languageName: node + linkType: hard + "filelist@npm:^1.0.1": version: 1.0.4 resolution: "filelist@npm:1.0.4" @@ -17814,7 +17893,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": +"glob@npm:^7.0.3, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -17981,6 +18060,19 @@ __metadata: languageName: node linkType: hard +"globby@npm:~6.1.0": + version: 6.1.0 + resolution: "globby@npm:6.1.0" + dependencies: + array-union: "npm:^1.0.1" + glob: "npm:^7.0.3" + object-assign: "npm:^4.0.1" + pify: "npm:^2.0.0" + pinkie-promise: "npm:^2.0.0" + checksum: 10/18109d6b9d55643d2b98b59c3cfae7073ccfe39829632f353d516cc124d836c2ddebe48a23f04af63d66a621b6d86dd4cbd7e6af906f2458a7fe510ffc4bd424 + languageName: node + linkType: hard + "globjoin@npm:^0.1.4": version: 0.1.4 resolution: "globjoin@npm:0.1.4" @@ -18262,6 +18354,7 @@ __metadata: ol: "npm:7.4.0" ol-ext: "npm:4.0.33" openapi-types: "npm:^12.1.3" + pa11y-ci: "npm:^3.1.0" pdf-parse: "npm:^1.1.1" plop: "npm:^4.0.1" pluralize: "npm:^8.0.0" @@ -18655,6 +18748,13 @@ __metadata: languageName: node linkType: hard +"hoopy@npm:^0.1.4": + version: 0.1.4 + resolution: "hoopy@npm:0.1.4" + checksum: 10/7a73f1839a7fd6b953356770dff2c3cff813d97d899cddd75b348926c4df36059d987c06bedb57b1b7711504dba83d3b7b986f979a08b1e415da73a51fefa767 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -18818,6 +18918,13 @@ __metadata: languageName: node linkType: hard +"html_codesniffer@npm:~2.5.1": + version: 2.5.1 + resolution: "html_codesniffer@npm:2.5.1" + checksum: 10/baeb07f54f7a514f7c0abb68318f6f1216266ef853059dbe6fc182d9c49fcc610e331de76599d51d10b2415cd1c0a137a22d656047e002cf6aa8815b9a8434cc + languageName: node + linkType: hard + "htmlparser2@npm:^6.1.0": version: 6.1.0 resolution: "htmlparser2@npm:6.1.0" @@ -20161,6 +20268,13 @@ __metadata: languageName: node linkType: hard +"is@npm:^3.3.0": + version: 3.3.0 + resolution: "is@npm:3.3.0" + checksum: 10/f77dc5a05a1e8fd1f1de282add9bb01c44dae27af72b883bf0ce342151dec48f125b0b8923efa78c1e93c4fb866095629b2c7de3e5e3853aea4ed17c82c5cd8d + languageName: node + linkType: hard + "isarray@npm:0.0.1": version: 0.0.1 resolution: "isarray@npm:0.0.1" @@ -21382,6 +21496,13 @@ __metadata: languageName: node linkType: hard +"kleur@npm:~4.1.4": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 10/44d84cc4eedd4311099402ef6d4acd9b2d16e08e499d6ef3bb92389bd4692d7ef09e35248c26e27f98acac532122acb12a1bfee645994ae3af4f0a37996da7df + languageName: node + linkType: hard + "known-css-properties@npm:^0.29.0": version: 0.29.0 resolution: "known-css-properties@npm:0.29.0" @@ -21937,7 +22058,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4, lodash@npm:^4.1.1, lodash@npm:^4.15.0, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4": +"lodash@npm:4.17.21, lodash@npm:^4, lodash@npm:^4.1.1, lodash@npm:^4.15.0, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:~4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 @@ -22806,6 +22927,13 @@ __metadata: languageName: node linkType: hard +"mkdirp-classic@npm:^0.5.2": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac + languageName: node + linkType: hard + "mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -23072,6 +23200,15 @@ __metadata: languageName: node linkType: hard +"mustache@npm:~4.2.0": + version: 4.2.0 + resolution: "mustache@npm:4.2.0" + bin: + mustache: bin/mustache + checksum: 10/6e668bd5803255ab0779c3983b9412b5c4f4f90e822230e0e8f414f5449ed7a137eed29430e835aa689886f663385cfe05f808eb34b16e1f3a95525889b05cd3 + languageName: node + linkType: hard + "mutationobserver-shim@npm:0.3.7": version: 0.3.7 resolution: "mutationobserver-shim@npm:0.3.7" @@ -23291,6 +23428,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:~2.6.1": + version: 2.6.13 + resolution: "node-fetch@npm:2.6.13" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10/72f94498d547e322207575b2778e7b3d969b0d73f35a19f258c7fb982bf0ae96e5a3a518477300ba1dd2bd22e6b05be074648ed88448c5cf1a9b7d23c6529d1a + languageName: node + linkType: hard + "node-forge@npm:^1, node-forge@npm:^1.3.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" @@ -23414,6 +23565,16 @@ __metadata: languageName: node linkType: hard +"node.extend@npm:~2.0.2": + version: 2.0.3 + resolution: "node.extend@npm:2.0.3" + dependencies: + hasown: "npm:^2.0.0" + is: "npm:^3.3.0" + checksum: 10/f500ace16d0b90e9db3919676de593eb37e7b82d8d9b67d95a40e5856ef5842592df3364b4d01fc2c3f4c0dea6dd9d627444dd85fe18581b7a22caad5ffab249 + languageName: node + linkType: hard + "nodemailer@npm:6.9.13": version: 6.9.13 resolution: "nodemailer@npm:6.9.13" @@ -24333,6 +24494,13 @@ __metadata: languageName: node linkType: hard +"p-timeout@npm:~4.1.0": + version: 4.1.0 + resolution: "p-timeout@npm:4.1.0" + checksum: 10/321fec524c23a754e3f1487f2b0a5516fd32aba960d5610490eac56f8a0114b549a93f9919ffc05aa68956dc52e8330e0519f3ddf951d208d19c845f9cd778de + languageName: node + linkType: hard + "p-try@npm:^1.0.0": version: 1.0.0 resolution: "p-try@npm:1.0.0" @@ -24356,6 +24524,48 @@ __metadata: languageName: node linkType: hard +"pa11y-ci@npm:^3.1.0": + version: 3.1.0 + resolution: "pa11y-ci@npm:3.1.0" + dependencies: + async: "npm:~2.6.4" + cheerio: "npm:~1.0.0-rc.10" + commander: "npm:~6.2.1" + globby: "npm:~6.1.0" + kleur: "npm:~4.1.4" + lodash: "npm:~4.17.21" + node-fetch: "npm:~2.6.1" + pa11y: "npm:^6.2.3" + protocolify: "npm:~3.0.0" + puppeteer: "npm:~9.1.1" + wordwrap: "npm:~1.0.0" + bin: + pa11y-ci: bin/pa11y-ci.js + checksum: 10/26f8a9e5aa2a7e1114090f4d2ff9392b2c0fa52cf7282343ce81816a7c0c017e2860d546623b6a869b86f1e597ac2f36415056ae9ccd783e8fc4bc91d76c0e5d + languageName: node + linkType: hard + +"pa11y@npm:^6.2.3": + version: 6.2.3 + resolution: "pa11y@npm:6.2.3" + dependencies: + axe-core: "npm:~4.2.1" + bfj: "npm:~7.0.2" + commander: "npm:~8.0.0" + envinfo: "npm:~7.8.1" + html_codesniffer: "npm:~2.5.1" + kleur: "npm:~4.1.4" + mustache: "npm:~4.2.0" + node.extend: "npm:~2.0.2" + p-timeout: "npm:~4.1.0" + puppeteer: "npm:~9.1.1" + semver: "npm:~7.3.5" + bin: + pa11y: bin/pa11y.js + checksum: 10/3903f10475aa6132279c8af21862dbcc3133e78d3c3ab6273094964093d73f60e312e9bc6eebcdcdc5c733d9229c1e63e3890103f0e860897ffd4ec2bf0f62b3 + languageName: node + linkType: hard + "package-json-from-dist@npm:^1.0.0": version: 1.0.0 resolution: "package-json-from-dist@npm:1.0.0" @@ -24895,7 +25105,7 @@ __metadata: languageName: node linkType: hard -"pify@npm:^2.2.0, pify@npm:^2.3.0": +"pify@npm:^2.0.0, pify@npm:^2.2.0, pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" checksum: 10/9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba @@ -24916,6 +25126,22 @@ __metadata: languageName: node linkType: hard +"pinkie-promise@npm:^2.0.0": + version: 2.0.1 + resolution: "pinkie-promise@npm:2.0.1" + dependencies: + pinkie: "npm:^2.0.0" + checksum: 10/b53a4a2e73bf56b6f421eef711e7bdcb693d6abb474d57c5c413b809f654ba5ee750c6a96dd7225052d4b96c4d053cdcb34b708a86fceed4663303abee52fcca + languageName: node + linkType: hard + +"pinkie@npm:^2.0.0": + version: 2.0.4 + resolution: "pinkie@npm:2.0.4" + checksum: 10/11d207257a044d1047c3755374d36d84dda883a44d030fe98216bf0ea97da05a5c9d64e82495387edeb9ee4f52c455bca97cdb97629932be65e6f54b29f5aec8 + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -25516,6 +25742,13 @@ __metadata: languageName: node linkType: hard +"prepend-http@npm:^3.0.0": + version: 3.0.1 + resolution: "prepend-http@npm:3.0.1" + checksum: 10/8f4ea0c73e7fc9d42b33a0487a458504dd1098b64ac7832ed4b0c401d292f1cf312c631efe078d9d92b0aa898be57f63459206793882061a8f507a52f0045cc7 + languageName: node + linkType: hard + "prettier@npm:3.4.2, prettier@npm:^3.1.1, prettier@npm:^3.2.5": version: 3.4.2 resolution: "prettier@npm:3.4.2" @@ -25622,6 +25855,13 @@ __metadata: languageName: node linkType: hard +"progress@npm:^2.0.1": + version: 2.0.3 + resolution: "progress@npm:2.0.3" + checksum: 10/e6f0bcb71f716eee9dfac0fe8a2606e3704d6a64dd93baaf49fbadbc8499989a610fe14cf1bc6f61b6d6653c49408d94f4a94e124538084efd8e4cf525e0293d + languageName: node + linkType: hard + "promise-all-reject-late@npm:^1.0.0": version: 1.0.1 resolution: "promise-all-reject-late@npm:1.0.1" @@ -25735,6 +25975,16 @@ __metadata: languageName: node linkType: hard +"protocolify@npm:~3.0.0": + version: 3.0.0 + resolution: "protocolify@npm:3.0.0" + dependencies: + file-url: "npm:^3.0.0" + prepend-http: "npm:^3.0.0" + checksum: 10/6517361b577d42ef80b975e6ad03673f1f3ea5aedae003bed7a1b6fffb06bc9c7542302f36c3517c82fb3271ba0c7b592e7b80830fa96f397ee261cba258c396 + languageName: node + linkType: hard + "protocols@npm:^2.0.0, protocols@npm:^2.0.1": version: 2.0.1 resolution: "protocols@npm:2.0.1" @@ -25818,6 +26068,26 @@ __metadata: languageName: node linkType: hard +"puppeteer@npm:~9.1.1": + version: 9.1.1 + resolution: "puppeteer@npm:9.1.1" + dependencies: + debug: "npm:^4.1.0" + devtools-protocol: "npm:0.0.869402" + extract-zip: "npm:^2.0.0" + https-proxy-agent: "npm:^5.0.0" + node-fetch: "npm:^2.6.1" + pkg-dir: "npm:^4.2.0" + progress: "npm:^2.0.1" + proxy-from-env: "npm:^1.1.0" + rimraf: "npm:^3.0.2" + tar-fs: "npm:^2.0.0" + unbzip2-stream: "npm:^1.3.3" + ws: "npm:^7.2.3" + checksum: 10/eab8e4982ea2e6007adbe7b1028504fe55dac39476e5f6f4d93b1b5abc5276ee0b398149e1824510e51c8b9a2a15048b31518563bdbe10135e2c6ced915b8018 + languageName: node + linkType: hard + "pure-rand@npm:^6.0.0": version: 6.0.3 resolution: "pure-rand@npm:6.0.3" @@ -28283,6 +28553,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:~7.3.5": + version: 7.3.8 + resolution: "semver@npm:7.3.8" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 10/c8c04a4d41d30cffa7277904e0ad6998623dd61e36bca9578b0128d8c683b705a3924beada55eae7fa004fb30a9359a53a4ead2b68468d778b602f3b1a28f8e3 + languageName: node + linkType: hard + "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" @@ -30079,7 +30360,19 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:~2.2.0": +"tar-fs@npm:^2.0.0": + version: 2.1.3 + resolution: "tar-fs@npm:2.1.3" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10/37fdfd3aa73f4f49c0821ef75f67647ecafd5370d2e311d9ace6ff3825ff4355014055c3d43407c6a655adf6c5bfb0cbcf93412161dad5af7110eb7d7a0c2eae + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4, tar-stream@npm:~2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" dependencies: @@ -30547,6 +30840,13 @@ __metadata: languageName: node linkType: hard +"tryer@npm:^1.0.1": + version: 1.0.1 + resolution: "tryer@npm:1.0.1" + checksum: 10/4d869d187bd715136903b349f39d1cc3e5c19f742689a348190aff92408ee8dd3d7d9adc26dc9265c35d722731184c979ed316109b6c1239249a8707bb92cc49 + languageName: node + linkType: hard + "ts-api-utils@npm:^2.0.0, ts-api-utils@npm:^2.1.0": version: 2.1.0 resolution: "ts-api-utils@npm:2.1.0" @@ -31062,6 +31362,16 @@ __metadata: languageName: node linkType: hard +"unbzip2-stream@npm:^1.3.3": + version: 1.4.3 + resolution: "unbzip2-stream@npm:1.4.3" + dependencies: + buffer: "npm:^5.2.1" + through: "npm:^2.3.8" + checksum: 10/4ffc0e14f4af97400ed0f37be83b112b25309af21dd08fa55c4513e7cb4367333f63712aec010925dbe491ef6e92db1248e1e306e589f9f6a8da8b3a9c4db90b + languageName: node + linkType: hard + "unc-path-regex@npm:^0.1.2": version: 0.1.2 resolution: "unc-path-regex@npm:0.1.2" @@ -32280,7 +32590,7 @@ __metadata: languageName: node linkType: hard -"wordwrap@npm:^1.0.0": +"wordwrap@npm:^1.0.0, wordwrap@npm:~1.0.0": version: 1.0.0 resolution: "wordwrap@npm:1.0.0" checksum: 10/497d40beb2bdb08e6d38754faa17ce20b0bf1306327f80cb777927edb23f461ee1f6bc659b3c3c93f26b08e1cf4b46acc5bae8fda1f0be3b5ab9a1a0211034cd @@ -32383,7 +32693,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.2.0, ws@npm:^7.3.1": +"ws@npm:^7.2.0, ws@npm:^7.2.3, ws@npm:^7.3.1": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: