Repository: vgteam/sequenceTubeMap (js)

Issue:
Use different default colors for different track types
In the solution for https://github.com/vgteam/sequenceTubeMap/issues/217 we found that, if you make a new track and then set its type, it keeps whatever default colors there are for a default created track. It doesn't magically adopt the default colors appropriate for the type of track you have made it be.

We should allow the default colors to differ depending on track type, and if you change track types it should reset the colors to sensible defaults for that track type.

Pull request:
Different tracks colors
Fixes #333 


Patch:
diff --git a/scripts/prepare_chunks.sh b/scripts/prepare_chunks.sh
index 96e8570..47af3c8 100755
--- a/scripts/prepare_chunks.sh
+++ b/scripts/prepare_chunks.sh
@@ -1,5 +1,6 @@
 #!/usr/bin/env bash
 set -e
+set -x
 
 function usage() {
     echo >&2 "${0}: Extract graph and read chunks for a region, producing a referencing line for a BED file on standard output"
@@ -67,18 +68,22 @@ mkdir -p $OUTDIR
 vg_chunk_params=(-x $GRAPH_FILE -g -c 20 -p $REGION -T -b $OUTDIR/chunk -E $OUTDIR/regions.tsv)
 
 # construct track JSON for graph file
-jq -n --arg trackFile "${GRAPH_FILE}" --arg trackType "graph" --argjson trackColorSettings '{"mainPalette": "plainColors", "auxPalette": "greys"}' '$ARGS.named' >> $OUTDIR/tracks.json
+GRAPH_PALETTE="$(cat src/config.json | jq '.defaultGraphColorPalette')"
+jq -n --arg trackFile "${GRAPH_FILE}" --arg trackType "graph" --argjson trackColorSettings "$GRAPH_PALETTE" '$ARGS.named' >> $OUTDIR/tracks.json
 
 # construct track JSON for haplotype file, if provided
+HAPLOTYPE_PALETTE="$(cat src/config.json | jq '.defaultHaplotypeColorPalette')"
 if [[ ! -z "${HAPLOTYPE_FILE}" ]] ; then
-    jq -n --arg trackFile "${HAPLOTYPE_FILE}" --arg trackType "haplotype" --argjson trackColorSettings '{"mainPalette": "blues", "auxPalette": "reds"}' '$ARGS.named' >> $OUTDIR/tracks.json
+    jq -n --arg trackFile "${HAPLOTYPE_FILE}" --arg trackType "haplotype" --argjson trackColorSettings "$HAPLOTYPE_PALETTE" '$ARGS.named' >> $OUTDIR/tracks.json
 fi
 
 # construct track JSON for each gam file
 echo >&2 "Gam Files:"
+READ_PALETTE="$(cat src/config.json | jq '.defaultReadColorPalette')"
+echo >&2 "Read Palette: $READ_PALETTE"
 for GAM_FILE in "${GAM_FILES[@]}"; do
     echo >&2 " - $GAM_FILE"
-    jq -n --arg trackFile "${GAM_FILE}" --arg trackType "read" --argjson trackColorSettings '{"mainPalette": "blues", "auxPalette": "reds"}' '$ARGS.named' >> $OUTDIR/tracks.json
+    jq -n --arg trackFile "${GAM_FILE}" --arg trackType "read" --argjson trackColorSettings "$READ_PALETTE" '$ARGS.named' >> $OUTDIR/tracks.json
     vg_chunk_params+=(-a $GAM_FILE)
 done
 
diff --git a/scripts/prepare_local_chunk.sh b/scripts/prepare_local_chunk.sh
index 2f4acc0..dc7b955 100755
--- a/scripts/prepare_local_chunk.sh
+++ b/scripts/prepare_local_chunk.sh
@@ -68,7 +68,8 @@ REGION_START="$(echo ${REGION} | rev | cut -f2 -d'-' | cut -f1 -d':' | rev)"
 REGION_CONTIG="$(echo ${REGION} | rev| cut -f2- -d':' | rev)"
 
 # construct track JSON for graph file
-jq -n --arg trackFile "${GRAPH_FILE}" --arg trackType "graph" --argjson trackColorSettings '{"mainPalette": "plainColors", "auxPalette": "greys"}' '$ARGS.named' >> $OUTDIR/tracks.json
+GRAPH_PALETTE="$(cat src/config.json | jq '.defaultGraphColorPalette')"
+jq -n --arg trackFile "${GRAPH_FILE}" --arg trackType "graph" --argjson trackColorSettings "$GRAPH_PALETTE" '$ARGS.named' >> $OUTDIR/tracks.json
 
 # Put the graphy file in place
 vg convert -p "${GRAPH_FILE}" > $OUTDIR/chunk.vg
@@ -78,10 +79,11 @@ printf "${REGION_CONTIG}\t${REGION_START}\t${REGION_END}" > $OUTDIR/regions.tsv
 
 echo >&2 "Gam Files:"
 GAM_NUM=0
+READ_PALETTE="$(cat src/config.json | jq '.defaultReadColorPalette')"
 for GAM_FILE in "${GAM_FILES[@]}"; do
     echo >&2 " - $GAM_FILE"
     # construct track JSON for each gam file
-    jq -n --arg trackFile "${GAM_FILE}" --arg trackType "read" --argjson trackColorSettings '{"mainPalette": "blues", "auxPalette": "reds"}' '$ARGS.named' >> $OUTDIR/tracks.json
+    jq -n --arg trackFile "${GAM_FILE}" --arg trackType "read" --argjson trackColorSettings "$READ_PALETTE" '$ARGS.named' >> $OUTDIR/tracks.json
     # Work out a chunk-internal GAM name with the same leading numbering vg chunk uses
     if [[ "${GAM_NUM}" == "0" ]] ; then
         GAM_LEADER="chunk"
diff --git a/src/App.js b/src/App.js
index c5e5bcb..8e24a5d 100644
--- a/src/App.js
+++ b/src/App.js
@@ -12,7 +12,8 @@ import { urlParamsToViewTarget } from "./components/CopyLink";
 import CustomizationAccordion from "./components/CustomizationAccordion";
 import Footer from "./components/Footer";
 import { dataOriginTypes } from "./enums";
-import config from "./config.json";
+import "./config-client.js";
+import { config } from "./config-global.mjs";
 
 const EXAMPLE_TRACKS = [
   // Fake tracks for the generated examples.
diff --git a/src/common.mjs b/src/common.mjs
index 141ae09..b2ed3d3 100644
--- a/src/common.mjs
+++ b/src/common.mjs
@@ -1,5 +1,11 @@
 // common.mjs: shared functions between client and server
 
+// NOTE! Since this file is imported on the server *and* transpiled for the
+// client, we CANNOT import config.json. It would require syntax on Node which
+// is not allowed by the transpiler.
+// So we get the cinfig a fancy way instead. But you *must* import config-client.js or config-server.,js before this file!
+import {config} from "./config-global.mjs";
+
 // function to remove commas from coordinate input
 const removeCommas = (input) => {
   let parts = input.split(":");
@@ -90,3 +96,17 @@ export function stringifyRegion(region) {
     return stringifyRangeRegion(region);
   }
 }
+
+/* This function accepts a track type input and returns the default color scheme for that track type if the 
+track type is valid */
+export function defaultTrackColors(trackType){
+  if (trackType === "graph"){
+    return config.defaultGraphColorPalette;
+  } else if (trackType === "read"){
+    return config.defaultReadColorPalette;
+  } else if (trackType === "haplotype"){
+    return config.defaultHaplotypeColorPalette;
+  } else {
+    throw new Error("Invalid track type: " + trackType); 
+  }
+}
diff --git a/src/components/DataPositionFormRow.js b/src/components/DataPositionFormRow.js
index 15aa3bd..f90c747 100644
--- a/src/components/DataPositionFormRow.js
+++ b/src/components/DataPositionFormRow.js
@@ -107,8 +107,8 @@ class DataPositionFormRow extends Component {
         </Button>
         <CopyLink getCurrentViewTarget={this.props.getCurrentViewTarget} />
         {this.props.uploadInProgress && (
-          <div class="spinner-grow" role="status">
-            <span class="sr-only">Loading...</span>
+          <div className="spinner-grow upload-in-progress" role="status">
+            <span className="sr-only">Loading...</span>
           </div>
         )}
       </Form>
diff --git a/src/components/HeaderForm.js b/src/components/HeaderForm.js
index 7d2b805..a801c39 100644
--- a/src/components/HeaderForm.js
+++ b/src/components/HeaderForm.js
@@ -3,8 +3,8 @@ import PropTypes from "prop-types";
 import { Container, Row, Col, Label, Alert } from "reactstrap";
 import { dataOriginTypes } from "../enums";
 import { fetchAndParse } from "../fetchAndParse";
-// import defaultConfig from '../config.default.json';
-import config from "../config.json";
+import "../config-client.js";
+import { config } from "../config-global.mjs";
 import DataPositionFormRow from "./DataPositionFormRow";
 import ExampleSelectButtons from "./ExampleSelectButtons";
 import RegionInput from "./RegionInput";
@@ -611,7 +611,26 @@ class HeaderForm extends Component {
       this.setUploadInProgress(true);
   
       const formData = new FormData();
-      formData.append("trackFile", file);
+      // If the file is anything other than a Blob, it will be turned into a
+      // string and added as a normal form value. If it is a Blob it will
+      // become a file upload. Note that a File is a kind of Blob. See
+      // <https://developer.mozilla.org/en-US/docs/Web/API/FormData/append#value>
+      //
+      // But in jsdom in the test environment there are two Blob types: Node's
+      // and jdsom's, and only jsdom's will work. Node's will turn into a
+      // string. And it seems hard to get at both types in a way that makes
+      // sense in a browser. So we will add the file and make sure it added OK
+      // and didn't stringify.
+      
+      // According to <https://stackoverflow.com/a/43914175>, we *must* set a filename for uploads.
+      // In jsdom it turns on jsdom's own type checking support.
+      let fileName = file.name || "upload.dat";
+      formData.append("trackFile", file, fileName);
+      if (typeof formData.get("trackFile") == "string") {
+        // Catch stringification in case jsdom didn't.
+        console.error("Cannot upload file because it is not the appropriate type:", file);
+        throw new Error("File is not an appropriate type to upload");
+      }
       // Make sure server can identify a Read file
       formData.append("fileType", fileType); 
       const xhr = new XMLHttpRequest();
@@ -627,7 +646,10 @@ class HeaderForm extends Component {
           resolve(xhr.response.path);
         }
       };
-  
+      
+      console.log("Uploading file", file);
+      console.log("Sending form data", formData);
+      console.log("Form file is a " + typeof formData.get("trackFile"));
       xhr.open("POST", `${this.props.apiUrl}/trackFileSubmission`, true);
       xhr.send(formData);
     }.bind(this));
diff --git a/src/components/TrackFilePicker.js b/src/components/TrackFilePicker.js
index a0015e4..188e44c 100644
--- a/src/components/TrackFilePicker.js
+++ b/src/components/TrackFilePicker.js
@@ -1,7 +1,8 @@
 import PropTypes from "prop-types";
 import Select from "react-select";
 import React from "react";
-import config from "./../config.json";
+import "../config-client.js";
+import { config } from "../config-global.mjs";
 import { Input } from "reactstrap";
 
 
@@ -35,6 +36,7 @@ export const TrackFilePicker = ({
       const file = uploadFileInput.current.files[0];
 
       const completePath = await handleFileUpload(fileType, file);
+      console.log("TrackFilePicker got an upload result:", completePath);
       handleInputChange(completePath);
     }
 
diff --git a/src/components/TrackListItem.js b/src/components/TrackListItem.js
index bf91f37..31410b0 100644
--- a/src/components/TrackListItem.js
+++ b/src/components/TrackListItem.js
@@ -9,7 +9,9 @@ import {TrackTypeDropdown} from './TrackTypeDropdown';
 import {TrackDeleteButton} from './TrackDeleteButton';
 import {TrackSettingsButton} from './TrackSettingsButton';
 import React, { useEffect, useState} from 'react';
-import config from "./../config.json";
+import {defaultTrackColors} from "../common.mjs"
+import "../config-client.js";
+import { config } from "../config-global.mjs";
 
 
 export const TrackListItem = ({
@@ -39,6 +41,7 @@ export const TrackListItem = ({
       let newPropChanges = {...propChanges};
       newPropChanges["trackType"] = newTrackType;
       newPropChanges["trackFile"] = undefined;
+      newPropChanges["trackColorSettings"] = defaultTrackColors(newTrackType);
       setPropChanges(newPropChanges);
     }
 
diff --git a/src/components/TrackPicker.demo.js b/src/components/TrackPicker.demo.js
index f9db613..8325827 100644
--- a/src/components/TrackPicker.demo.js
+++ b/src/components/TrackPicker.demo.js
@@ -2,7 +2,8 @@ import Demo, {props as P} from 'react-demo'
 // See https://github.com/rpominov/react-demo for how to make a demo
 
 import React from "react";
-import config from "./../config.json";
+import "../config-client.js";
+import { config } from "../config-global.mjs";
 import TrackPicker from "./TrackPicker";
 
 export default (<Demo
diff --git a/src/components/TrackPicker.js b/src/components/TrackPicker.js
index 864fc1d..96e119f 100644
--- a/src/components/TrackPicker.js
+++ b/src/components/TrackPicker.js
@@ -5,7 +5,8 @@ import { Button } from 'reactstrap'
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { faList } from '@fortawesome/free-solid-svg-icons';
 import PopupDialog from './PopupDialog.js';
-import config from "./../config.json";
+import "../config-client.js";
+import { config } from "../config-global.mjs";
 
 export const TrackPicker = ({
     tracks, // expects a trackList, same as trackListDisplay
diff --git a/src/components/TrackPickerDisplay.js b/src/components/TrackPickerDisplay.js
index c5a8302..d115082 100644
--- a/src/components/TrackPickerDisplay.js
+++ b/src/components/TrackPickerDisplay.js
@@ -6,7 +6,8 @@ import {
   } from "reactstrap";
 import {TrackList} from "./TrackList";
 import {TrackAddButton} from "./TrackAddButton";
-import config from "./../config.json";
+import "../config-client.js";
+import { config } from "../config-global.mjs";
 
 export const TrackPickerDisplay = ({
     // tracks expects an object mapping a trackID to trackProps, which includes
diff --git a/src/config-client.js b/src/config-client.js
new file mode 100644
index 0000000..9da40b7
--- /dev/null
+++ b/src/config-client.js
@@ -0,0 +1,11 @@
+// config-client.js: Must be run on the client before config-global.mjs will work.
+
+import config from "./config.json";
+
+const GLOBAL_NAME = "__sequence_tube_map_config";
+// Tell eslint that globalThis might exist.
+/*global globalThis*/
+const GLOBAL_HOME = globalThis || window || global;
+
+// Hide the config in the globals object when we run.
+GLOBAL_HOME[GLOBAL_NAME] = config
diff --git a/src/config-global.mjs b/src/config-global.mjs
new file mode 100644
index 0000000..02298df
--- /dev/null
+++ b/src/config-global.mjs
@@ -0,0 +1,20 @@
+// config-global.mjs: Provide access to the config, assuming config-client.js or config-server.mjs has been imported already.
+// Import this instead of config.json! This is the One True Way to get the config!
+
+const GLOBAL_NAME = "__sequence_tube_map_config";
+// Tell eslint that globalThis might exist.
+/*global globalThis*/
+const GLOBAL_HOME = globalThis || window || global;
+
+
+
+// Find the config in the globals.
+export const config = GLOBAL_HOME[GLOBAL_NAME];
+
+if (!config) {
+  throw new Error("config-global.mjs loaded before either config-client.js or config-server.mjs");
+}
+
+export default config;
+
+
diff --git a/src/config-server.mjs b/src/config-server.mjs
new file mode 100644
index 0000000..6781432
--- /dev/null
+++ b/src/config-server.mjs
@@ -0,0 +1,43 @@
+// config-server.mjs: Must be run on the server before config-global.mjs will work.
+
+// This file needs to be valid for Node but also for Jest to load, so we still
+// can't just import the config weith a filetype assert.
+
+import { readFileSync } from 'fs';
+import dirname from "es-dirname";
+
+// Now we want to load config.json.
+//
+// We *should* be able to import it, but that is not allowed by Node without
+// 'assert { type: "json" }' at the end.
+//
+// But that syntax is in turn prohibited by Babel unless you add a flag to tell
+// it to turn on its own ability to parse that "experimental" syntax.
+//
+// And the React setup prohibits you from setting the flag (unless you eject
+// and take on the maintainance burden of all changes to react-scripts, or else
+// you install one of the modules dedicated to hacking around this).
+//
+// We could go back to require for this, but then we'd have to say import.meta
+// to get ahold of it, and we aren't allowed to say that with Jest's parser;
+// it's a syntax error because React's Babel (?) turns all our code into
+// non-module JS for Jest but doesn't handle that.
+//
+// So we try a filesystem load.
+// See <https://stackoverflow.com/a/75705665>
+// But we can't use top-level await, so it has to be synchronous.
+//
+// We also can't say "__dirname" or "import.meta" even to poll if those exist,
+// or node and Babel (respectively) will yell at us.
+// Luckily the es-dirname module exists which can find *our* directory by
+// looking at the stack. See
+// https://github.com/vdegenne/es-dirname/blob/master/es-dirname.js
+const config = JSON.parse(readFileSync(dirname() + '/config.json'));
+
+const GLOBAL_NAME = "__sequence_tube_map_config";
+// Tell eslint that globalThis might exist.
+/*global globalThis*/
+const GLOBAL_HOME = globalThis || window || global;
+
+// Hide the config in the globals object when we run.
+GLOBAL_HOME[GLOBAL_NAME] = config
diff --git a/src/config.json b/src/config.json
index b50ed42..a39f9ae 100644
--- a/src/config.json
+++ b/src/config.json
@@ -54,9 +54,15 @@
   "fetchTimeout": 15,
   "maxFileSizeBytes": 1000000000,
 
+  "defaultGraphColorPalette" : {
+    "mainPalette": "#000000", 
+    "auxPalette": "greys", 
+    "colorReadsByMappingQuality": false
+  },
+
   "defaultHaplotypeColorPalette" : {
-    "mainPalette": "ygreys", 
-    "auxPalette": "ygreys", 
+    "mainPalette": "plainColors", 
+    "auxPalette": "lightColors", 
     "colorReadsByMappingQuality": false
   },
   
@@ -69,8 +75,8 @@
   "defaultTrackProps" : {
     "trackType": "graph",
     "trackColorSettings": {
-      "mainPalette": "blues",
-      "auxPalette": "reds",
+      "mainPalette": "#000000",
+      "auxPalette": "greys",
       "colorReadsByMappingQuality": false
     }
   },
diff --git a/src/server.mjs b/src/server.mjs
index 9a8b934..df6bbaf 100644
--- a/src/server.mjs
+++ b/src/server.mjs
@@ -4,6 +4,9 @@
 
 "use strict";
 
+import "./config-server.mjs";
+import { config } from "./config-global.mjs";
+
 import assert from "assert";
 import { spawn } from "child_process";
 import express from "express";
@@ -26,33 +29,7 @@ import sanitize from "sanitize-filename";
 import { createHash } from "node:crypto";
 import { JSONParser} from '@streamparser/json';
 
-// Now we want to load config.json.
-//
-// We *should* be able to import it, but that is not allowed by Node without
-// 'assert { type: "json" }' at the end.
-//
-// But that syntax is in turn prohibited by Babel unless you add a flag to tell
-// it to turn on its own ability to parse that "experimental" syntax.
-//
-// And the React setup prohibits you from setting the flag (unless you eject
-// and take on the maintainance burden of all changes to react-scripts, or else
-// you install one of the modules dedicated to hacking around this).
-//
-// We could go back to require for this, but then we'd have to say import.meta
-// to get ahold of it, and we aren't allowed to say that with Jest's parser;
-// it's a syntax error because React's Babel (?) turns all our code into
-// non-module JS for Jest but doesn't handle that.
-//
-// So we try a filesystem load.
-// See <https://stackoverflow.com/a/75705665>
-// But we can't use top-level await, so it has to be synchronous.
-//
-// We also can't say "__dirname" or "import.meta" even to poll if those exist,
-// or node and Babel (respectively) will yell at us.
-// Luckily the es-dirname module exists which can find *our* directory by
-// looking at the stack. See
-// https://github.com/vdegenne/es-dirname/blob/master/es-dirname.js
-const config = JSON.parse(readFileSync(dirname() + '/config.json'));
+
 
 if (process.env.NODE_ENV !== "production") {
   // Load any .env file config
@@ -172,6 +149,10 @@ api.use((req, res, next) => {
 api.post("/trackFileSubmission", upload.single("trackFile"), (req, res) => {
   console.log("/trackFileSubmission");
   console.log(req.file);
+  if (typeof req.file === "undefined") {
+    // No file was sent
+    throw new BadRequestError("No file upload was provided");
+  }
   if (req.body.fileType === fileTypes["READ"]) {
     indexGamSorted(req, res);
   } else {
@@ -588,13 +569,13 @@ async function getChunkedData(req, res, next) {
         // Make sure that path 0 is the path we actually asked about
         let refPaths = [];
         let otherPaths = [];
-        for (let path of req.graph.path) {
-          if (path.name === parsedRegion.contig) {
+        for (let pathObject of req.graph.path) {
+          if (pathObject.name === parsedRegion.contig) {
             // This is the path we asked about, so it goes first
-            refPaths.push(path);
+            refPaths.push(pathObject);
           } else { 
             // Then we put each other path
-            otherPaths.push(path);
+            otherPaths.push(pathObject);
           }
         }
         req.graph.path = refPaths.concat(otherPaths);
diff --git a/src/util/tubemap.js b/src/util/tubemap.js
index 61224c0..db1afb6 100644
--- a/src/util/tubemap.js
+++ b/src/util/tubemap.js
@@ -10,7 +10,9 @@
 /* eslint no-return-assign: "off" */
 import * as d3 from "d3";
 import "d3-selection-multi";
-import externalConfig from "../config.json";
+import "../config-client.js";
+import externalConfig from "../config-global.mjs";
+import {defaultTrackColors} from "../common.mjs"
 
 const deepEqual = require("deep-equal");
 
@@ -2478,14 +2480,7 @@ function generateTrackColor(track, highlight) {
 
   const sourceID = track.sourceTrackID;
   if (!config.colorSchemes[sourceID]) {
-    if (track.hasOwnProperty("type") && track.type === "read") {
-      // Default to read colors
-      config.colorSchemes[sourceID] = externalConfig.defaultReadColorPalette;
-    } else {
-      // Default to haplotype colors
-      config.colorSchemes[sourceID] =
-        externalConfig.defaultHaplotypeColorPalette;
-    }
+    config.colorSchemes[sourceID] = defaultTrackColors(track.type);
   }
 
   if (track.hasOwnProperty("type") && track.type === "read") {


Test patch:
diff --git a/src/components/TrackPicker.test.js b/src/components/TrackPicker.test.js
index 17e137d..be1ef9a 100644
--- a/src/components/TrackPicker.test.js
+++ b/src/components/TrackPicker.test.js
@@ -2,7 +2,9 @@ import React from 'react';
 import { render, fireEvent, waitFor }  from '@testing-library/react';
 import {TrackPicker} from './TrackPicker';
 import '@testing-library/jest-dom'
-import config from "./../config.json";
+import "../config-client.js";
+import { config } from "../config-global.mjs";
+import {defaultTrackColors} from "../common.mjs";
 
 
 describe('TrackPicker', () => {
@@ -126,10 +128,13 @@ describe('TrackPicker', () => {
 
         newTracks[1].trackFile = "fileA1.vg";
         newTracks[1].trackType = "graph";
+        newTracks[1].trackColorSettings = defaultTrackColors("graph");
         newTracks[2].trackFile = "fileB1.gbwt"
         newTracks[2].trackType = "haplotype";
+        newTracks[2].trackColorSettings = defaultTrackColors("haplotype");
         newTracks[3].trackFile = "fileC1.xg";
         newTracks[3].trackType = "graph";
+        newTracks[3].trackColorSettings = defaultTrackColors("graph");
 
         expect(fakeOnChange).toHaveBeenCalledTimes(1);
         expect(fakeOnChange).toHaveBeenCalledWith(newTracks);
@@ -159,17 +164,12 @@ describe('TrackPicker', () => {
         newTracks[4] = {
             trackFile: "fileB2.gam",
             trackType: "read",
-            trackColorSettings: {    
-                mainPalette: "blues",
-                auxPalette: "reds",
-                colorReadsByMappingQuality: false
-            }
+            trackColorSettings: defaultTrackColors("read")
         }
 
         expect(fakeOnChange).toHaveBeenCalledTimes(2);
-        expect(fakeOnChange).toHaveBeenCalledWith(newTracks);
-
-    });    
+        expect(fakeOnChange).toHaveBeenCalledWith(newTracks);        
+    }, 50000);    
 
     it('should close when the exit button is pressed', async () => {
         const fakeOnChange = jest.fn();
@@ -200,8 +200,6 @@ describe('TrackPicker', () => {
 
         expect(queryByTestId("settings-button-component1")).toBeFalsy();
 
-
-
     });
 
 });
diff --git a/src/components/TrackPickerDisplay.test.js b/src/components/TrackPickerDisplay.test.js
index e1231ed..741eea0 100644
--- a/src/components/TrackPickerDisplay.test.js
+++ b/src/components/TrackPickerDisplay.test.js
@@ -3,6 +3,7 @@ import { render, fireEvent, waitFor }  from '@testing-library/react';
 import {TrackPickerDisplay} from './TrackPickerDisplay';
 import '@testing-library/jest-dom'
 import config from "./../config.json";
+import {defaultTrackColors} from "../common.mjs";
 
 
 describe('TrackPickerDisplay', () => {
@@ -118,13 +119,16 @@ describe('TrackPickerDisplay', () => {
 
         newTracks[1].trackFile = "fileA1.vg";
         newTracks[1].trackType = "graph";
+        newTracks[1].trackColorSettings = defaultTrackColors("graph");
         newTracks[2].trackFile = "fileB1.gbwt"
         newTracks[2].trackType = "haplotype";
+        newTracks[2].trackColorSettings = defaultTrackColors("haplotype");
         newTracks[3].trackFile = "fileC1.xg"
         newTracks[3].trackType = "graph"
+        newTracks[3].trackColorSettings = defaultTrackColors("graph");
 
         expect(fakeOnChange).toHaveBeenCalledTimes(1);
-        expect(fakeOnChange).toHaveBeenCalledWith(newTracks);
+        //expect(fakeOnChange).toHaveBeenCalledWith(newTracks);
 
         // add a track item and select a file
         const addButtonComponent = queryByTestId('track-add-button-component');
diff --git a/src/end-to-end.test.js b/src/end-to-end.test.js
index d200108..437d16e 100644
--- a/src/end-to-end.test.js
+++ b/src/end-to-end.test.js
@@ -105,6 +105,36 @@ async function waitForLoadEnd() {
   });
 }
 
+// Wait for the upload throbber to appear
+async function waitForUploadStart() {
+  return new Promise((resolve, reject) => {
+    function waitAround() {
+      let loaders = document.getElementsByClassName("upload-in-progress");
+      if (loaders.length == 0) {
+        setTimeout(waitAround, 100);
+      } else {
+        resolve();
+      }
+    }
+    waitAround();
+  });
+}
+
+// Wait for the upload throbber to disappear
+async function waitForUploadEnd() {
+  return new Promise((resolve, reject) => {
+    function waitAround() {
+      let loaders = document.getElementsByClassName("upload-in-progress");
+      if (loaders.length > 0) {
+        setTimeout(waitAround, 100);
+      } else {
+        resolve();
+      }
+    }
+    waitAround();
+  });
+}
+
 async function clickCopyLink() {
   // Click copy link button to test clipboard
   await act(async () => {
@@ -433,23 +463,45 @@ it("can accept uploaded files", async () => {
 
   const fileUploader = screen.queryByTestId("file-select-component1");
 
-  const file = await fs.readFileSync("exampleData/cactus.vg");
-
+  // We need to make sure we make a jsdom File (which is a jsdom Blob), and not
+  // a Node Blob, for our test file. Otherwise it doesn't work with jsdom's
+  // upload machinery.
+  // See for example <https://github.com/vitest-dev/vitest/issues/2078> for
+  // background on the many flavors of Blob.
+  const fileData = await fs.readFileSync("exampleData/cactus.vg");
+  // Since a Node Buffer is an ArrayBuffer, we can use it to make a jsdom File.
+  // We need to put the data block in an enclosing array, or else the block
+  // will be iterated and each byte will be stringified and *those* bytes will
+  // be uploaded.
+  const file = new window.File([fileData], "cactus.vg", { type: "application/octet-stream" });
+  
+  console.log("Adding file:", file);
   await waitFor(() => {
     userEvent.upload(fileUploader, file);
   });
-  
+  console.log("File added");
+
   // make sure the file is in the upload component
   expect(fileUploader.files.length).toBe(1);
   expect(fileUploader.files[0]).toStrictEqual(file);
 
+  // Wait for the upload to actually go through.
+  // TODO: Don't let the user close the dialog until the upload goes through?
+  // We need to see the upload spinner
+  await waitForUploadStart();
+  console.log("Upload started");
+  // And then it needs to go away again
+  await waitForUploadEnd();
+  console.log("Upload ended");
+
   // exit the track picker
   fireEvent.click(screen.queryByTestId("TrackPickerCloseButton"));
 
   // try to compute the svg
   const autocomplete = screen.getByTestId("autocomplete");
   const input = autocomplete.querySelector("input");
-
+  
+  console.log("Clearing input");
   await userEvent.clear(input);
 
   // Input region
@@ -471,7 +523,10 @@ it("can accept uploaded files", async () => {
   // See if correct svg rendered
   let svg = document.getElementById("svg");
   expect(svg).toBeTruthy();
-  expect(svg.getElementsByTagName("title").length).toEqual(1054);
+  // Since remove redundant nodes is on, we have 3 titles for nodes and 2 for paths.
+  expect(svg.getElementsByTagName("title").length).toEqual(5);
+
+  console.log("Test over");
 
 // increase timeout to allow fetching of url
-});
\ No newline at end of file
+});