Create a reusable cypress_test() macro

I want to write a couple of tests for some plotting web pages. I keep
asking other folks to validate that I didn't break anything. It's time
to automate this.

This patch creates a new macro that will make it easier for everyone
to write such tests. We're also converting the one existing Cypress
test to use the new macro.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I8ec82dc8ebdcc22a394f3b66fbc5c39e937adc6e
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index f66d9c4..ceb67aa 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -12,6 +12,7 @@
 load("//tools/build_rules/js:ts.bzl", _ts_project = "ts_project")
 load("@aspect_rules_rollup//rollup:defs.bzl", upstream_rollup_bundle = "rollup_bundle")
 load("@aspect_rules_terser//terser:defs.bzl", "terser_minified")
+load("@aspect_rules_cypress//cypress:defs.bzl", "cypress_module_test")
 
 ts_project = _ts_project
 
@@ -359,3 +360,42 @@
         "suffix": attr.string(mandatory = True),
     },
 )
+
+def cypress_test(runner, data = None, **kwargs):
+    """Runs a cypress test with the specified runner.
+
+    Args:
+        runner: The runner that starts up any necessary servers and then
+            invokes Cypress itself. See the Module API documentation for more
+            information: https://docs.cypress.io/guides/guides/module-api
+        data: The spec files (*.cy.js) and the servers under test. Also any
+            other files needed at runtime.
+        kwargs: Arguments forwarded to the upstream cypress_module_test().
+    """
+
+    # Figure out how many directories deep this package is relative to the
+    # workspace root.
+    package_depth = len(native.package_name().split("/"))
+
+    # Chrome is located at the runfiles root. So we need to go up one more
+    # directory than the workspace root.
+    chrome_location = "../" * (package_depth + 1) + "chrome_linux/chrome"
+    config_location = "../" * package_depth + "tools/build_rules/js/cypress.config.js"
+
+    data = data or []
+    data.append("//tools/build_rules/js:cypress.config.js")
+    data.append("@xvfb_amd64//:wrapped_bin/Xvfb")
+
+    cypress_module_test(
+        args = [
+            "run",
+            "--config-file=" + config_location,
+            "--browser=" + chrome_location,
+        ],
+        browsers = ["@chrome_linux//:all"],
+        copy_data_to_bin = False,
+        cypress = "//:node_modules/cypress",
+        data = data,
+        runner = runner,
+        **kwargs
+    )
diff --git a/tools/build_rules/js/BUILD b/tools/build_rules/js/BUILD
index 2381fcb..1bbe769 100644
--- a/tools/build_rules/js/BUILD
+++ b/tools/build_rules/js/BUILD
@@ -1,6 +1,10 @@
 load("@npm//:@angular/compiler-cli/package_json.bzl", angular_compiler_cli = "bin")
 load(":ts.bzl", "ts_project")
 
+exports_files([
+    "cypress.config.js",
+])
+
 # Define the @angular/compiler-cli ngc bin binary as a target
 angular_compiler_cli.ngc_binary(
     name = "ngc",
diff --git a/tools/build_rules/js/cypress.config.js b/tools/build_rules/js/cypress.config.js
new file mode 100644
index 0000000..4eb1a82
--- /dev/null
+++ b/tools/build_rules/js/cypress.config.js
@@ -0,0 +1,22 @@
+const {defineConfig} = require('cypress');
+
+module.exports = defineConfig({
+  e2e: {
+    specPattern: ['*.cy.js'],
+    supportFile: false,
+    setupNodeEvents(on, config) {
+      on('before:browser:launch', (browser = {}, launchOptions) => {
+        launchOptions.args.push('--disable-gpu-shader-disk-cache');
+      });
+
+      // Lets users print to the console:
+      //    cy.task('log', 'message here');
+      on('task', {
+        log(message) {
+          console.log(message);
+          return null;
+        },
+      });
+    },
+  },
+});