ui: Make all unitests to jsdom-based

Today UI tests are a mixture of nodejs-based tests
and jsdom-based tests (without counting headless, which
I am going to rewrite next).
That creates unnecessary confusion and makes tests harder
to write. The real issue is that some parts of the codebase
(the code under tests) sometimes depends on DOM elements
like window.localstorage and the like. Running them in a
clean Node environment adds unnecessary boilerplate.
Today we have a mixture of tests that do hacks to run in
both DOM and Node environment (e.g., string_utils.ts)
and tests that give up and use the jsdom environment.
This CL gives up on all of them and uses jsdom everywhere
for consistency.
For now I am leaving the _unittest.ts vs _jsdom.ts suffix
and I am merging everything at the jest.config level.
If this CL sticks, will rename jsdom -> unittests in a
follow-up CL.

This CL also fixes the build.js script when using --watch:
today the HTTP server and the tests are started straight
away after the end of the tasks, but in --watch mode that's
too early because tsc and rollup happen asynchronously.

Bug: 190075400
Change-Id: I6ef8060e32924417b612c1010ae206cf4b6671c1
diff --git a/ui/build.js b/ui/build.js
index d0aa3d1..9867ad2 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -82,7 +82,6 @@
   debug: false,
   startHttpServer: false,
   wasmModules: ['trace_processor', 'trace_to_text'],
-  testConfigs: ['jest.unit.config.js', 'jest.jsdom.config.js'],
 
   // The fields below will be changed by main() after cmdline parsing.
   // Directory structure:
@@ -117,12 +116,11 @@
 
 let tasks = [];
 let tasksTot = 0, tasksRan = 0;
-let serverStarted = false;
 let httpWatches = [];
 let tStart = Date.now();
 let subprocesses = [];
 
-function main() {
+async function main() {
   const parser = new argparse.ArgumentParser();
   parser.addArgument('--out', {help: 'Output directory'});
   parser.addArgument(['--watch', '-w'], {action: 'storeTrue'});
@@ -130,8 +128,9 @@
   parser.addArgument(['--verbose', '-v'], {action: 'storeTrue'});
   parser.addArgument(['--no-build', '-n'], {action: 'storeTrue'});
   parser.addArgument(['--no-wasm', '-W'], {action: 'storeTrue'});
-  parser.addArgument(['--run-tests', '-t'], {action: 'storeTrue'});
+  parser.addArgument(['--run-unittests', '-t'], {action: 'storeTrue'});
   parser.addArgument(['--debug', '-d'], {action: 'storeTrue'});
+  parser.addArgument(['--interactive', '-i'], {action: 'storeTrue'});
 
   const args = parser.parseArgs();
   const clean = !args.no_build;
@@ -148,6 +147,9 @@
   cfg.verbose = !!args.verbose;
   cfg.debug = !!args.debug;
   cfg.startHttpServer = args.serve;
+  if (args.interactive) {
+    process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1';
+  }
 
   process.on('SIGINT', () => {
     console.log('\nSIGINT received. Killing all child processes and exiting');
@@ -192,8 +194,22 @@
     scanDir(cfg.outDistRootDir);
   }
 
-  if (args.run_tests) {
-    runTests();
+
+  // We should enter the loop only in watch mode, where tsc and rollup are
+  // asynchronous because they run in watch mode.
+  const tStart = Date.now();
+  while (!isDistComplete()) {
+    const secs = Math.ceil((Date.now() - tStart) / 1000);
+    process.stdout.write(`Waiting for first build to complete... ${secs} s\r`);
+    await new Promise(r => setTimeout(r, 500));
+  }
+  if (cfg.watch) console.log('\nFirst build completed!');
+
+  if (cfg.startHttpServer) {
+    startServer();
+  }
+  if (args.run_unittests) {
+    runTests('jest.unittest.config.js');
   }
 }
 
@@ -201,12 +217,17 @@
 // Build rules
 // -----------
 
-function runTests() {
-  const args =
-      ['--rootDir', cfg.outTscDir, '--verbose', '--runInBand', '--forceExit'];
-  for (const cfgFile of cfg.testConfigs) {
-    args.push('--projects', pjoin(ROOT_DIR, 'ui/config', cfgFile));
-  }
+function runTests(cfgFile) {
+  const args = [
+    '--rootDir',
+    cfg.outTscDir,
+    '--verbose',
+    '--runInBand',
+    '--detectOpenHandles',
+    '--forceExit',
+    '--projects',
+    pjoin(ROOT_DIR, 'ui/config', cfgFile)
+  ];
   if (cfg.watch) {
     args.push('--watchAll');
     addTask(execNode, ['jest', args, {async: true}]);
@@ -284,8 +305,14 @@
 }
 
 function updateSymlinks() {
+  // /ui/out -> /out/ui.
   mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out'));
 
+  // /out/ui/test/data -> /test/data (For UI tests).
+  mklink(
+      pjoin(ROOT_DIR, 'test/data'),
+      pjoin(ensureDir(pjoin(cfg.outDir, 'test')), 'data'));
+
   // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config
   // can point to that without having to know the current version number.
   mklink(
@@ -427,6 +454,24 @@
       .listen(port, '127.0.0.1');
 }
 
+function isDistComplete() {
+  const requiredArtifacts = [
+    'controller_bundle.js',
+    'frontend_bundle.js',
+    'engine_bundle.js',
+    'trace_processor.wasm',
+    'perfetto.css',
+  ];
+  const relPaths = new Set();
+  walk(cfg.outDistDir, absPath => {
+    relPaths.add(path.relative(cfg.outDistDir, absPath));
+  });
+  for (const fName of requiredArtifacts) {
+    if (!relPaths.has(fName)) return false;
+  }
+  return true;
+}
+
 // Called whenever a change in the out/dist directory is detected. It sends a
 // Server-Side-Event to the live_reload.ts script.
 function notifyLiveServer(changedFile) {
@@ -476,11 +521,6 @@
     console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`);
     task.func.apply(/*this=*/ undefined, task.args);
   }
-  // Start the web server once reaching quiescence.
-  if (tasks.length === 0 && !serverStarted && cfg.startHttpServer) {
-    serverStarted = true;
-    startServer();
-  }
 }
 
 // Executes all the RULES that match the given |absPath|.