Allow setup files in ts_web_test_suite

Closes #266

PiperOrigin-RevId: 221465479
diff --git a/examples/testing/BUILD.bazel b/examples/testing/BUILD.bazel
index b802c0e..ca8dd3f 100644
--- a/examples/testing/BUILD.bazel
+++ b/examples/testing/BUILD.bazel
@@ -17,6 +17,17 @@
     ],
 )
 
+ts_library(
+    name = "tests_setup",
+    testonly = 1,
+    srcs = ["setup_script.ts"],
+    tsconfig = "//examples:tsconfig-test",
+    deps = [
+        "@npm//@types/jasmine",
+        "@npm//@types/node",
+    ],    
+)
+
 ts_web_test_suite(
     name = "testing",
     browsers = [
@@ -26,6 +37,9 @@
     static_files = [
         "static_script.js",
     ],
+    runtime_deps = [
+        ":tests_setup",
+    ],
     deps = [
         ":tests",
     ],
diff --git a/examples/testing/setup_script.spec.ts b/examples/testing/setup_script.spec.ts
new file mode 100644
index 0000000..1b39eff
--- /dev/null
+++ b/examples/testing/setup_script.spec.ts
@@ -0,0 +1,9 @@
+describe('setup script', () => {
+  it('should load before the spec', async () => {
+    expect((window as any).setupGlobal).toBe("setupGlobalValue");
+  });
+});
+
+// at least one import or export is needed for this file to
+// be compiled into an named-UMD module by typescript
+export {};
diff --git a/examples/testing/setup_script.ts b/examples/testing/setup_script.ts
new file mode 100644
index 0000000..7bca6f8
--- /dev/null
+++ b/examples/testing/setup_script.ts
@@ -0,0 +1,6 @@
+// Setup global value that the test expect to be present.
+(window as any).setupGlobal = "setupGlobalValue";
+
+// at least one import or export is needed for this file to
+// be compiled into an named-UMD module by typescript
+export { };
diff --git a/internal/karma/karma.conf.js b/internal/karma/karma.conf.js
index 09f05ab..70dc9bf 100644
--- a/internal/karma/karma.conf.js
+++ b/internal/karma/karma.conf.js
@@ -186,6 +186,7 @@
 // A simplified version of Karma's requirejs.config.tpl.js for use with Karma under Bazel.
 // This does an explicit \`require\` on each test script in the files, otherwise nothing will be loaded.
 (function(){
+  var runtimeFiles = [TMPL_runtime_files].map(function(file) { return file.replace(/\\.js$/, ''); });
   var allFiles = [TMPL_user_files];
   var allTestFiles = [];
   allFiles.forEach(function (file) {
@@ -193,7 +194,7 @@
       allTestFiles.push(file.replace(/\\.js$/, ''))
     }
   });
-  require(allTestFiles, window.__karma__.start);
+  require(runtimeFiles, function() { return require(allTestFiles, window.__karma__.start); });
 })();
 `;
 
diff --git a/internal/karma/ts_web_test.bzl b/internal/karma/ts_web_test.bzl
index b2798fa..d76703a 100644
--- a/internal/karma/ts_web_test.bzl
+++ b/internal/karma/ts_web_test.bzl
@@ -38,7 +38,7 @@
     )
 
     files = depset(ctx.files.srcs)
-    for d in ctx.attr.deps:
+    for d in ctx.attr.deps + ctx.attr.runtime_deps:
         if hasattr(d, "node_sources"):
             files = depset(transitive = [files, d.node_sources])
         elif hasattr(d, "files"):
@@ -79,6 +79,18 @@
         "/".join([ctx.workspace_name, amd_names_shim.short_path]),
     ]
 
+    # Next we load the "runtime_deps" which we expect to contain named AMD modules
+    # Thus they should come after the require.js script, but before any srcs or deps
+    runtime_files = []
+    for d in ctx.attr.runtime_deps:
+        if not hasattr(d, "typescript"):
+            # Workaround https://github.com/bazelbuild/rules_nodejs/issues/57
+            # We should allow any JS source as long as it yields something that
+            # can be loaded by require.js
+            fail("labels in runtime_deps must be created by ts_library")
+        for src in d.typescript.es5_sources.to_list():
+            runtime_files.append(expand_path_into_runfiles(ctx, src.short_path))
+
     # Finally we load the user's srcs and deps
     user_entries = [
         expand_path_into_runfiles(ctx, f.short_path)
@@ -100,6 +112,7 @@
             "TMPL_bootstrap_files": "\n".join(["      '%s'," % e for e in bootstrap_entries]),
             "TMPL_user_files": "\n".join(["      '%s'," % e for e in user_entries]),
             "TMPL_static_files": "\n".join(["      '%s'," % e for e in static_files]),
+            "TMPL_runtime_files": "\n".join(["      '%s'," % e for e in runtime_files]),
             "TMPL_workspace_name": ctx.workspace_name,
         },
     )
@@ -110,6 +123,7 @@
     ]
     karma_runfiles += ctx.files.srcs
     karma_runfiles += ctx.files.deps
+    karma_runfiles += ctx.files.runtime_deps
     karma_runfiles += ctx.files.bootstrap
     karma_runfiles += ctx.files.static_files
 
@@ -181,12 +195,20 @@
             or UMD bundles for third-party libraries.""",
             allow_files = [".js"],
         ),
+        "runtime_deps": attr.label_list(
+            doc = """Dependencies which should be loaded after the module loader but before the srcs and deps.
+            These should be a list of targets which produce JavaScript such as `ts_library`.
+            The files will be loaded in the same order they are declared by that rule.""",
+            allow_files = True,
+            aspects = [sources_aspect],
+        ),
         "data": attr.label_list(
             doc = "Runtime dependencies",
         ),
         "static_files": attr.label_list(
-            doc = """Arbitrary files which to be served. Files are served at:
-            `/base/<WORKSPACE_NAME>/<path-to-file>`, e.g. 
+            doc = """Arbitrary files which are available to be served on request.
+            Files are served at:
+            `/base/<WORKSPACE_NAME>/<path-to-file>`, e.g.
             `/base/build_bazel_rules_typescript/examples/testing/static_script.js`""",
             allow_files = True,
         ),