Repository: vgteam/sequenceTubeMap
Pull request #265 (2023-05-05 22:27:36)

Track list
- Add keys to radio row component
- Attach trackID to end of the testing tag to select specific items in the tracklist
- Rework TrackListItem to no longer rely on an event handler reference
    - Now keep a local version of uncommitted differences 
- Implement TrackList


Patch:
diff --git a/package-lock.json b/package-lock.json
index 676dd7e..6a96073 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5708,9 +5708,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001431",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz",
-      "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==",
+      "version": "1.0.30001482",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz",
+      "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ==",
       "funding": [
         {
           "type": "opencollective",
@@ -5719,6 +5719,10 @@
         {
           "type": "tidelift",
           "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
         }
       ]
     },
@@ -21102,9 +21106,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30001431",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz",
-      "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ=="
+      "version": "1.0.30001482",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001482.tgz",
+      "integrity": "sha512-F1ZInsg53cegyjroxLNW9DmrEQ1SuGRTO1QlpA0o2/6OpQ0gFeDRoq1yFmnr8Sakn9qwwt9DmbxHB6w167OSuQ=="
     },
     "case-sensitive-paths-webpack-plugin": {
       "version": "2.4.0",
diff --git a/src/components/RadioRow.js b/src/components/RadioRow.js
index 80b48d5..601286f 100644
--- a/src/components/RadioRow.js
+++ b/src/components/RadioRow.js
@@ -22,26 +22,32 @@ class RadioRow extends Component {
 
   };
 
+
   render() {
-    const colorRadios = Array.from(colorMap).map(([keyColor, valueColor]) => {
-      if(this.props.availableColors.includes(valueColor)){
-        return (
-          <Col xs="auto" key={keyColor}>
-            <FormGroup check>
-              <Label check>
-                <Input
-                  type="radio"
-                  value={keyColor}
-                  checked={this.props.color === valueColor}
-                  onChange={this.onChange}
-                />
-                {keyColor}
-              </Label>
-            </FormGroup>
-          </Col>
-        );
+    let currColorMap = new Map(colorMap);
+    for (const [keyColor, valueColor] of colorMap) {
+      if(!this.props.availableColors.includes(valueColor)){
+        currColorMap.delete(keyColor);
       }
-      return <></>;
+    }
+
+    const colorRadios = Array.from(currColorMap).map(([keyColor, valueColor]) => {
+      return (
+        <Col xs="auto" key={keyColor}>
+          <FormGroup check>
+            <Label check>
+              <Input
+                type="radio"
+                value={keyColor}
+                checked={this.props.color === valueColor}
+                onChange={this.onChange}
+                key={keyColor}
+              />
+              {keyColor}
+            </Label>
+          </FormGroup>
+        </Col>
+      );
     });
     return (
       <FormGroup row className="mb-1">
diff --git a/src/components/TrackDeleteButton.js b/src/components/TrackDeleteButton.js
index 3c324d9..161f8c6 100644
--- a/src/components/TrackDeleteButton.js
+++ b/src/components/TrackDeleteButton.js
@@ -12,12 +12,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
  */
 
  export function TrackDeleteButton (props) {
+    // https://stackoverflow.com/questions/49358560/react-wrapper-react-does-not-recognize-the-staticcontext-prop-on-a-dom-elemen
+    const {testID, ...rest} = props;
 
     // return button
     // upon click, call onClick function 
     
     return (
-        <Button data-testid="delete-button" {...props}><FontAwesomeIcon icon={faX} /></Button>
+        <Button data-testid={props.testID} {...rest}><FontAwesomeIcon icon={faX} /></Button>
     );
 }
 
@@ -30,11 +32,13 @@ TrackDeleteButton.propTypes = {
   id: PropTypes.string,
   className: PropTypes.string,
   onClick: PropTypes.func.isRequired,
+  testID: PropTypes.string
 };
 
 TrackDeleteButton.defaultProps = {
   id: undefined,
   className: undefined,
+  testID: "delete-button-component"
 };
 
 export default TrackDeleteButton;
diff --git a/src/components/TrackFilePicker.js b/src/components/TrackFilePicker.js
index 8f1b36f..9526d8f 100644
--- a/src/components/TrackFilePicker.js
+++ b/src/components/TrackFilePicker.js
@@ -18,7 +18,8 @@ export const TrackFilePicker = ({
   value, // input file
   handleInputChange,
   pickerType, // either "dropdown or upload" to determine which component we render
-  className 
+  className,
+  testID
 }) => {
 
 
@@ -49,7 +50,7 @@ export const TrackFilePicker = ({
     if (pickerType === "dropdown"){
       return(
       // wrap Select container in div to easily query in tests
-        <div data-testid="file-select-component">
+        <div data-testid={testID}>
             <Select 
               options={dropDownOptions}
               value={{label: value["name"], value: value}}
@@ -86,12 +87,14 @@ TrackFilePicker.propTypes = {
     handleInputChange: PropTypes.func.isRequired,
     pickerType: PropTypes.string,
     className: PropTypes.string,
+    testID: PropTypes.string
 }
   
 TrackFilePicker.defaultProps = {
   value: {"name": "Select a file", "type": undefined},
   pickerType: "dropdown",
-  className: undefined
+  className: undefined,
+  testID: "file-select-component"
 }
 
 export default TrackFilePicker;
diff --git a/src/components/TrackList.demo.js b/src/components/TrackList.demo.js
new file mode 100644
index 0000000..6d8483f
--- /dev/null
+++ b/src/components/TrackList.demo.js
@@ -0,0 +1,59 @@
+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 TrackList from "./TrackList";
+
+export default (<Demo
+  props={{
+    tracks:P.json({
+        1: {
+            trackFile: undefined,
+            trackType: "graph",
+            trackColorSettings: {    
+                mainPallete: "blues",
+                auxPallete: "reds",
+                colorReadsByMappingQuality: false
+            }
+        },
+        2: {
+            trackFile: undefined,
+            trackType: "graph",
+            trackColorSettings: {    
+                mainPallete: "blues",
+                auxPallete: "reds",
+                colorReadsByMappingQuality: false
+            }
+        },
+        3: {
+            trackFile: undefined,
+            trackType: "graph",
+            trackColorSettings: {    
+                mainPallete: "blues",
+                auxPallete: "reds",
+                colorReadsByMappingQuality: false
+            }
+        },
+    }),
+    availableTracks: P.json([
+        {"files": [{"name": "fileA1.vg", "type": "graph"},
+                   {"name": "fileA2.gbwt", "type": "haplotype"}]},
+        {"files": [{"name": "fileB1.gbwt", "type": "haplotype"},
+                   {"name": "fileB2.gam", "type": "read"}]},
+        {"files": [{"name": "fileC1.xg", "type": "graph"}]}
+      ])
+  }}
+>
+  {
+    (props, update) => {
+      return <TrackList {...props} 
+        onChange={(newTracks) => {
+          update({tracks: newTracks});
+        }} 
+        onDelete={() => {console.log("track deleted")}}/>
+    }
+  }
+</Demo>)
+
+
diff --git a/src/components/TrackList.js b/src/components/TrackList.js
new file mode 100644
index 0000000..3f0c7a5
--- /dev/null
+++ b/src/components/TrackList.js
@@ -0,0 +1,70 @@
+import PropTypes from "prop-types";
+import {
+    Container,
+    Row
+  } from "reactstrap";
+import {TrackListItem} from './TrackListItem';
+
+
+export const TrackList = ({
+  // tracks expects an object mapping an trackID to trackProps, which includes
+      // * trackType: string
+      // * trackFile: file object / undefined
+      // * trackColorSettings: object(aka. colorScheme)
+  tracks,
+  // availableTracks: array of tracks(see types.ts)
+  availableTracks,
+  // availableColors: array of ColorPalletes
+  availableColors,
+  onChange, // expects a new tracks object
+  onDelete,
+}) => {
+
+  function trackItemOnChange(trackID, trackProps) {
+    (function(callback){ console.log(callback.toString()); })(onChange);
+      let newTracks = {...tracks};
+      newTracks[trackID] = trackProps;
+      if (JSON.stringify(newTracks) !== JSON.stringify(tracks)) {
+          onChange(newTracks);
+      }
+
+  }
+
+
+  function renderTracks() {
+      let trackHTML = [];
+
+      Object.keys(tracks).forEach((trackID, index) => {
+          const trackProps = tracks[trackID]
+          trackHTML.push(          
+          <Row key={trackID}>
+              <TrackListItem 
+              trackProps={trackProps}
+              availableTracks={availableTracks}
+              availableColors={availableColors}
+              onChange={trackItemOnChange}
+              onDelete={onDelete}
+              trackID={trackID}/>
+          </Row>
+          );
+      })
+
+      return trackHTML;
+  }
+
+  return(<Container>
+            {renderTracks()}
+         </Container>);
+  
+}
+
+TrackListItem.propTypes = {
+  tracks: PropTypes.object.isRequired,
+  availableTracks: PropTypes.array.isRequired,
+  availableColors: PropTypes.array,
+  onChange: PropTypes.func.isRequired,
+  onDelete: PropTypes.func.isRequired,
+}
+  
+
+export default TrackList;
\ No newline at end of file
diff --git a/src/components/TrackListItem.demo.js b/src/components/TrackListItem.demo.js
index e43d434..83c4e36 100644
--- a/src/components/TrackListItem.demo.js
+++ b/src/components/TrackListItem.demo.js
@@ -28,8 +28,7 @@ export default (<Demo
   {
     (props, update) => {
       return <TrackListItem {...props} 
-        onChange={(newTrackProps) => {
-
+        onChange={(trackID, newTrackProps) => {
           update({trackProps: newTrackProps});
         }} 
         onDelete={() => {console.log("track deleted")}}/>
diff --git a/src/components/TrackListItem.js b/src/components/TrackListItem.js
index 8df096d..cd08292 100644
--- a/src/components/TrackListItem.js
+++ b/src/components/TrackListItem.js
@@ -8,7 +8,7 @@ import {TrackFilePicker} from './TrackFilePicker';
 import {TrackTypeDropdown} from './TrackTypeDropdown';
 import {TrackDeleteButton} from './TrackDeleteButton';
 import {TrackSettingsButton} from './TrackSettingsButton';
-import React, { useEffect, useReducer, useRef } from 'react';
+import React, { useEffect, useState} from 'react';
 
 
 export const TrackListItem = ({
@@ -23,77 +23,82 @@ export const TrackListItem = ({
     availableColors,
     onChange, // expects a new trackProps object
     onDelete,
+    trackID,
   }) => {
-    const [myTrackProps, dispatch] = useReducer(reducer, trackProps);
-    const _onChange = useRef(onChange);
+    // propChanges only store new trackType, trackFile, and trackColorSettings changes
+    // reset after onChange is called
+    const [propChanges, setPropChanges] = useState({});
 
-    // https://overreacted.io/a-complete-guide-to-useeffect/
-    function reducer(state, action) {
-      let newState = {...state};
-      switch (action.type){
-        case "setType":
-          newState["trackFile"] = undefined;
-          newState["trackType"] = action.payload;
-          break;
-        case "setFile":
-          newState["trackFile"] = action.payload;
-          break;
-        case "setColorSettings":
-          newState["trackColorSettings"] = action.payload;
-          break;
-        default:
-          throw new Error();
-      }
-      return newState;
-    }
   
     const trackTypeOnChange = async(newTrackType) => {
       // wait until file change to call onChange
-      dispatch({type: "setType", payload: newTrackType});
+      let newPropChanges = {...propChanges};
+      newPropChanges["trackType"] = newTrackType;
+      newPropChanges["trackFile"] = undefined;
+      setPropChanges(newPropChanges);
     }
 
     const trackFileOnChange = async(newFile) => {
-      dispatch({type: "setFile", payload: newFile});
+      let newPropChanges = {...propChanges};
+      newPropChanges["trackFile"] = newFile;
+      setPropChanges(newPropChanges);
     }
 
     const trackSettingsOnChange = async(key, value) => {
       let newTrackColorSettings = {...trackProps["trackColorSettings"]};
       newTrackColorSettings[key] = value;
-      dispatch({type: "setColorSettings", payload: newTrackColorSettings});
+      let newPropChanges = {...propChanges};
+      newPropChanges["trackColorSettings"] = newTrackColorSettings;
+      setPropChanges(newPropChanges);
     }
 
-    // https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook
     // useEffect hook to tell react to call onchange after state changes
+    // setPropChanges are async, useEffect acts as a callback to setPropChanges
+    // https://stackoverflow.com/questions/54954091/how-to-use-callback-with-usestate-hook-in-react/56394177#56394177
     useEffect(() => {
-      if (myTrackProps.trackFile !== undefined && JSON.stringify(trackProps) !== JSON.stringify(myTrackProps)) {
-        _onChange.current(myTrackProps);
+      // get an updated version of trackProps
+      let newTrackProps = {...trackProps};
+      for (const key in propChanges) {
+        newTrackProps[key] = propChanges[key];
       }
       
-    }, [myTrackProps.trackFile, myTrackProps.trackType, myTrackProps.trackColorSettings, myTrackProps, _onChange, trackProps]);
+      // push it if trackFile has been selected and changes are made
+      if (newTrackProps.trackFile !== undefined && JSON.stringify(trackProps) !== JSON.stringify(newTrackProps)) {
+        onChange(trackID, newTrackProps);
+        setPropChanges({});
+      }
+    }, [propChanges, onChange, trackProps, trackID]);
 
+    // displayed elements uses propChanges(local state) first, then uses trackProps
+    // Isn't just a || because propChanges can set the trackFile to be undefined
+    const displayedFile = "trackFile" in propChanges ? propChanges["trackFile"] : trackProps["trackFile"];
     return (
-      <Container>
+      <Container key={trackID}>
         <Row className="g-0">
           <Col className="tracklist-dropdown" sm="2">
-            <TrackTypeDropdown value={myTrackProps["trackType"]} 
+            <TrackTypeDropdown value={propChanges["trackType"] || trackProps["trackType"]} 
                               onChange={trackTypeOnChange}
+                              testID={"file-type-select-component".concat(trackID)}
                               />
           </Col>
           <Col className="tracklist-dropdown" sm="3">
             <TrackFilePicker tracks={availableTracks} 
-                            fileType={myTrackProps["trackType"]} 
-                            value={myTrackProps["trackFile"]}
+                            fileType={propChanges["trackType"] || trackProps["trackType"]} 
+                            value={displayedFile}
                             pickerType={"dropdown"} 
                             handleInputChange={trackFileOnChange}
+                            testID={"file-select-component".concat(trackID)}
                             />
           </Col>
           <Col className="tracklist-button" sm="1">
-            <TrackSettingsButton fileType={myTrackProps["trackType"]}
-                                trackColorSettings={myTrackProps["trackColorSettings"]}
+            <TrackSettingsButton fileType={propChanges["trackType"] || trackProps["trackType"]}
+                                trackColorSettings={propChanges["trackColorSettings"] || trackProps["trackColorSettings"]}
                                 setTrackColorSetting={trackSettingsOnChange}
                                 availableColors={availableColors}
+                                testID={"settings-button-component".concat(trackID)}
                                 />
             <TrackDeleteButton onClick={onDelete}
+                               testID={"delete-button-component".concat(trackID)}
                               />
           </Col>
 
@@ -109,6 +114,7 @@ export const TrackListItem = ({
     availableColors: PropTypes.array,
     onChange: PropTypes.func.isRequired,
     onDelete: PropTypes.func.isRequired,
+    trackID: PropTypes.number.isRequired,
   }
     
   
diff --git a/src/components/TrackSettingsButton.js b/src/components/TrackSettingsButton.js
index faa736b..26edcf7 100644
--- a/src/components/TrackSettingsButton.js
+++ b/src/components/TrackSettingsButton.js
@@ -17,7 +17,8 @@ export const TrackSettingsButton = ({
     trackColorSettings,
     setTrackColorSetting,
     label,
-    availableColors
+    availableColors,
+    testID
 }) => {
     // based off of https://react-popup.elazizi.com/controlled-popup/#using-open-prop
     const [open, setOpen] = useState(false);
@@ -25,7 +26,8 @@ export const TrackSettingsButton = ({
     return(
       <div>
         
-        <Button data-testid="button" aria-label="Settings" onClick={() => setOpen(!open)}><FontAwesomeIcon icon={faGear} /></Button>
+        <Button aria-label="Settings" onClick={() => setOpen(!open)}><FontAwesomeIcon icon={faGear} data-testid={testID}/></Button>
+
       
         {/* Popup has a trigger option, but passing in a button to trigger did not allow for the popup to be opened and closed
          with a Reactstrap button. We had to use onClick and onClose instead to open and close depending on the state of the popup */}
@@ -56,10 +58,12 @@ TrackSettingsButton.propTypes = {
     setTrackColorSetting: PropTypes.func.isRequired,
     label: PropTypes.string,
     availableColors: PropTypes.array,
+    testID: PropTypes.string
 }
 
 TrackSettingsButton.defaultProps = {
-    availableColors: ["greys", "ygreys", "blues", "reds", "plainColors", "lightColors"]
+    availableColors: ["greys", "ygreys", "blues", "reds", "plainColors", "lightColors"],
+    testID: "settings-button-component"
 }
 
 export default TrackSettingsButton;
diff --git a/src/components/TrackTypeDropdown.js b/src/components/TrackTypeDropdown.js
index 20c12b6..61c20b4 100644
--- a/src/components/TrackTypeDropdown.js
+++ b/src/components/TrackTypeDropdown.js
@@ -17,7 +17,7 @@ export function TrackTypeDropdown (props) {
     // dropdown and selections
     // upon selection of a dropdown option, call onChange function 
     let dropdown = (
-      <div data-testid="file-type-select-component">
+      <div data-testid={props.testID}>
         <Select {...props} onChange={o => {
           props.onChange(o.value)}
         }
@@ -44,12 +44,14 @@ TrackTypeDropdown.propTypes = {
   className: PropTypes.string,
   value: PropTypes.string,
   onChange: PropTypes.func.isRequired,
+  testID: PropTypes.string
 };
 
 TrackTypeDropdown.defaultProps = {
   id: undefined,
   className: undefined,
   value: undefined,
+  testID: "file-type-select-component"
 };
 
 export default TrackTypeDropdown;


Test patch:
diff --git a/src/components/TrackList.test.js b/src/components/TrackList.test.js
new file mode 100644
index 0000000..7882ea3
--- /dev/null
+++ b/src/components/TrackList.test.js
@@ -0,0 +1,162 @@
+import React from 'react';
+import { render, fireEvent, waitFor }  from '@testing-library/react';
+import {TrackList} from './TrackList';
+import '@testing-library/jest-dom'
+
+
+describe('TrackList', () => {
+    const tracks = {
+        1: {
+            trackFile: undefined,
+            trackType: "graph",
+            trackColorSettings: {    
+                mainPallete: "blues",
+                auxPallete: "reds",
+                colorReadsByMappingQuality: false
+            }
+        },
+        2: {
+            trackFile: undefined,
+            trackType: "graph",
+            trackColorSettings: {    
+                mainPallete: "blues",
+                auxPallete: "reds",
+                colorReadsByMappingQuality: false
+            }
+        },
+        3: {
+            trackFile: undefined,
+            trackType: "graph",
+            trackColorSettings: {    
+                mainPallete: "blues",
+                auxPallete: "reds",
+                colorReadsByMappingQuality: false
+            }
+        },
+    }
+    const availableColors = ["greys", "ygreys", "reds", "plainColors", "lightColors"];
+    const availableTracks = [{"files": [{"name": "fileA1.vg", "type": "graph"},
+                                        {"name": "fileA2.gbwt", "type": "haplotype"}]},
+                             {"files": [{"name": "fileB1.gbwt", "type": "haplotype"},
+                                        {"name": "fileB2.gam", "type": "read"}]},
+                             {"files": [{"name": "fileC1.xg", "type": "graph"}]}];
+
+    function rerenderTrackList(rerender, newTracks, fakeOnChange, fakeOnDelete) {
+        rerender(
+            <TrackList
+                tracks={newTracks}
+                availableTracks={availableTracks}
+                availableColors={availableColors}
+                onChange={fakeOnChange}
+                onDelete={fakeOnDelete}
+            />
+        );
+    }
+
+
+    it('should render without errors', async () => {
+        const fakeOnChange = jest.fn();
+        const fakeOnDelete = jest.fn();
+        const { queryByTestId } = render(
+            <TrackList
+                tracks={tracks}
+                availableTracks={availableTracks}
+                availableColors={availableColors}
+                onChange={fakeOnChange}
+                onDelete={fakeOnDelete}
+            />
+        );
+
+        expect(queryByTestId("file-type-select-component1")).toBeTruthy();
+
+        expect(queryByTestId("file-type-select-component2")).toBeTruthy();
+
+        expect(queryByTestId("file-select-component1")).toBeTruthy();
+
+        expect(queryByTestId("settings-button-component1")).toBeTruthy();
+
+        expect(queryByTestId("delete-button-component1")).toBeTruthy();
+        
+    });
+
+    it('should call onChange when a track is changed', async () => {
+        const fakeOnChange = jest.fn();
+        const fakeOnDelete = jest.fn();
+        const { queryByTestId, getByText, rerender } = render(
+            <TrackList
+                tracks={tracks}
+                availableTracks={availableTracks}
+                availableColors={availableColors}
+                onChange={fakeOnChange}
+                onDelete={fakeOnDelete}
+            />
+        );
+
+        // select file type
+        const fileTypeSelectComponent = queryByTestId("file-type-select-component1");
+        fireEvent.keyDown(fileTypeSelectComponent.firstChild, {key: "ArrowDown"});
+        await waitFor(() => getByText("haplotype"));
+        fireEvent.click(getByText("haplotype"));
+
+        // should wait for file to be selected
+        expect(fakeOnChange).toHaveBeenCalledTimes(0);
+
+        // change file name
+        const fileSelectComponent = queryByTestId('file-select-component1');
+        fireEvent.keyDown(fileSelectComponent.firstChild, {key: "ArrowDown"});
+        await waitFor(() => getByText("fileB1.gbwt"));
+        fireEvent.click(getByText("fileB1.gbwt"));
+
+        expect(fakeOnChange).toHaveBeenCalledTimes(1);
+        let newTracks = JSON.parse(JSON.stringify(tracks));
+        newTracks[1].trackType = "haplotype";
+        newTracks[1].trackFile = {"name": "fileB1.gbwt", "type": "haplotype"};
+        expect(fakeOnChange).toHaveBeenCalledWith(newTracks);
+
+        // rerender file select effect
+        rerenderTrackList(rerender, newTracks, fakeOnChange, fakeOnDelete);
+
+        // change color
+        const settingsButtonComponent = queryByTestId('settings-button-component1');
+        fireEvent.click(settingsButtonComponent);
+        await waitFor(() => getByText("reds"));
+        fireEvent.click(getByText("reds"));
+        fireEvent.click(document);
+
+        expect(fakeOnChange).toHaveBeenCalledTimes(2); 
+
+        newTracks[1].trackColorSettings.mainPallete = "reds";
+        expect(fakeOnChange).toHaveBeenCalledWith(newTracks);
+
+    });
+
+    it('should use a new event handler when passed', async () => {
+        const fakeOnChange1 = jest.fn();
+        const fakeOnChange2 = jest.fn();
+        const fakeOnDelete = jest.fn();
+        const { queryByTestId, getByText, rerender } = render(
+            <TrackList
+                tracks={tracks}
+                availableTracks={availableTracks}
+                availableColors={availableColors}
+                onChange={fakeOnChange1}
+                onDelete={fakeOnDelete}
+            />
+        );
+
+        expect(fakeOnChange1).toHaveBeenCalledTimes(0); 
+
+        rerenderTrackList(rerender, tracks, fakeOnChange2, fakeOnDelete);
+
+
+        const fileSelectComponent = queryByTestId('file-select-component1');
+        fireEvent.keyDown(fileSelectComponent.firstChild, {key: "ArrowDown"});
+        await waitFor(() => getByText("fileA1.vg"));
+        fireEvent.click(getByText("fileA1.vg"));
+
+        expect(fakeOnChange1).toHaveBeenCalledTimes(0); 
+        expect(fakeOnChange2).toHaveBeenCalledTimes(1); 
+
+    });
+
+});
diff --git a/src/components/TrackListItem.test.js b/src/components/TrackListItem.test.js
index e2e5982..19c24ad 100644
--- a/src/components/TrackListItem.test.js
+++ b/src/components/TrackListItem.test.js
@@ -3,7 +3,7 @@ import { render, fireEvent, waitFor }  from '@testing-library/react';
 import {TrackListItem} from './TrackListItem';
 import '@testing-library/jest-dom'
 
-describe('TrackSettings', () => {
+describe('TrackListItem', () => {
     const trackFile = undefined;
     const trackType = "graph";
     const availableColors = ["greys", "ygreys", "reds", "plainColors", "lightColors"];
@@ -33,6 +33,7 @@ describe('TrackSettings', () => {
               availableTracks = {availableTracks}
               onChange = {fakeOnChange}
               onDelete = {fakeOnDelete}
+              trackID = {1}
              />
         );
 
@@ -50,7 +51,7 @@ describe('TrackSettings', () => {
         const fakeOnChange = jest.fn();
         const fakeOnDelete = jest.fn();
         
-        const { getByText, queryByTestId, rerender, getByRole } = render(
+        const { getByText, queryByTestId, rerender } = render(
             <TrackListItem 
             trackProps={{
                 trackFile: trackFile,
@@ -61,13 +62,14 @@ describe('TrackSettings', () => {
               availableTracks = {availableTracks}
               onChange = {fakeOnChange}
               onDelete = {fakeOnDelete}
+              trackID = {1}
              />
         );
 
         expect(fakeOnChange).toHaveBeenCalledTimes(0);
 
         // change track type
-        const fileTypeSelectComponent = queryByTestId("file-type-select-component");
+        const fileTypeSelectComponent = queryByTestId("file-type-select-component1");
         fireEvent.keyDown(fileTypeSelectComponent.firstChild, {key: "ArrowDown"});
         await waitFor(() => getByText("haplotype"));
         fireEvent.click(getByText("haplotype"));
@@ -87,25 +89,41 @@ describe('TrackSettings', () => {
               availableTracks = {availableTracks}
               onChange = {fakeOnChange}
               onDelete = {fakeOnDelete}
+              trackID = {1}
              />
         );
 
         // change file name
-        const fileSelectComponent = queryByTestId('file-select-component');
+        const fileSelectComponent = queryByTestId('file-select-component1');
         fireEvent.keyDown(fileSelectComponent.firstChild, {key: "ArrowDown"});
         await waitFor(() => getByText("fileB1.gbwt"));
         fireEvent.click(getByText("fileB1.gbwt"));
 
         expect(fakeOnChange).toHaveBeenCalledTimes(1);
-        expect(fakeOnChange).toHaveBeenCalledWith({
+        expect(fakeOnChange).toHaveBeenCalledWith(1, {
             trackFile: {"name": "fileB1.gbwt", "type": "haplotype"},
             trackType: "haplotype",
             trackColorSettings: trackColorSettings
         });
 
+        rerender(
+            <TrackListItem 
+            trackProps={{
+                trackFile: {"name": "fileB1.gbwt", "type": "haplotype"},
+                trackType: "haplotype",
+                trackColorSettings: trackColorSettings
+              }}
+              availableColors = {availableColors}
+              availableTracks = {availableTracks}
+              onChange = {fakeOnChange}
+              onDelete = {fakeOnDelete}
+              trackID = {1}
+             />
+        );
+
 
         // change color settings
-        fireEvent.click(getByRole("button", {name: /Settings/i}));
+        fireEvent.click(queryByTestId('settings-button-component1'));
         await waitFor(() => getByText("reds"));
         fireEvent.click(getByText("reds"));
         fireEvent.click(document);
@@ -129,12 +147,13 @@ describe('TrackSettings', () => {
               availableTracks = {availableTracks}
               onChange = {fakeOnChange}
               onDelete = {fakeOnDelete}
+              trackID = {1}
              />
         );
 
         expect(fakeOnDelete).toHaveBeenCalledTimes(0); 
 
-        fireEvent.click(queryByTestId("delete-button"));
+        fireEvent.click(queryByTestId("delete-button-component1"));
         expect(fakeOnDelete).toHaveBeenCalledTimes(1); 
     });
 
diff --git a/src/components/TrackSettingsButton.test.js b/src/components/TrackSettingsButton.test.js
index 595f57b..491d8a0 100644
--- a/src/components/TrackSettingsButton.test.js
+++ b/src/components/TrackSettingsButton.test.js
@@ -12,11 +12,11 @@ describe('TrackSettingsButton', () => {
                 colorReadsByMappingQuality: false}} setTrackColorSetting={function(a, b){}}></TrackSettingsButton>
         );
 
-        await userEvent.click(screen.getByTestId('button'));
+        await userEvent.click(screen.getByTestId('settings-button-component'));
 
         expect(screen.getByRole('heading')).toBeTruthy();
 
-        await userEvent.click(screen.getByTestId('button'));
+        await userEvent.click(screen.getByTestId('settings-button-component'));
 
         expect(screen.queryByRole('heading')).toBeFalsy();