(null)
- const { name, model, id, tree, toggleCollapse } = data
+ const { menuItems, name, model, id, tree, toggleCollapse } = data
return (
{
for (const entry of treeToMap(tree).get(id)?.children || []) {
if (!entry.children.length) {
- model.view.showTrack(entry.id)
+ model.view.showTrack(entry.trackId)
}
}
},
@@ -93,11 +93,12 @@ export default function Category({
onClick: () => {
for (const entry of treeToMap(tree).get(id)?.children || []) {
if (!entry.children.length) {
- model.view.hideTrack(entry.id)
+ model.view.hideTrack(entry.trackId)
}
}
},
},
+ ...menuItems,
]}
onMenuItemClick={(_event, callback) => {
callback()
diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.tsx b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.tsx
index 5ac4f2ef03..4c2d67c4ef 100644
--- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.tsx
+++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/tree/TrackLabel.tsx
@@ -10,6 +10,8 @@ import {
// icons
import MoreHorizIcon from '@mui/icons-material/MoreHoriz'
+import StarIcon from '@mui/icons-material/StarBorderOutlined'
+import FilledStarIcon from '@mui/icons-material/Star'
// locals
import { isUnsupported, NodeData } from '../util'
@@ -37,8 +39,17 @@ export interface InfoArgs {
export default function TrackLabel({ data }: { data: NodeData }) {
const { classes } = useStyles()
- const { checked, conf, model, drawerPosition, id, name, onChange, selected } =
- data
+ const {
+ checked,
+ conf,
+ model,
+ drawerPosition,
+ id,
+ trackId,
+ name,
+ onChange,
+ selected,
+ } = data
const description = (conf && readConfObject(conf, ['description'])) || ''
return (
<>
@@ -52,7 +63,10 @@ export default function TrackLabel({ data }: { data: NodeData }) {
onChange(id)}
+ onChange={() => {
+ onChange(trackId)
+ model.addToRecentlyUsed(trackId)
+ }}
disabled={isUnsupported(name)}
inputProps={{
// @ts-expect-error
@@ -92,6 +106,17 @@ function TrackMenuButton({
data-testid={`htsTrackEntryMenu-${id}`}
menuItems={[
...(getSession(model).getTrackActionMenuItems?.(conf) || []),
+ model.isFavorite(conf)
+ ? {
+ label: 'Remove from favorites',
+ onClick: () => model.removeFromFavorites(conf),
+ icon: StarIcon,
+ }
+ : {
+ label: 'Add to favorites',
+ onClick: () => model.addToFavorites(conf),
+ icon: FilledStarIcon,
+ },
{
label: 'Add to selection',
onClick: () => model.addToSelection([conf]),
diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/util.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/util.ts
index b6490c6c84..c813fdec92 100644
--- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/util.ts
+++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/components/util.ts
@@ -1,6 +1,7 @@
import { AnyConfigurationModel } from '@jbrowse/core/configuration'
import { HierarchicalTrackSelectorModel } from '../model'
import { TreeNode } from '../generateHierarchy'
+import { MenuItem } from '@jbrowse/core/ui'
export interface NodeData {
nestingLevel: number
@@ -8,12 +9,14 @@ export interface NodeData {
conf: AnyConfigurationModel
drawerPosition: unknown
id: string
+ trackId: string
isLeaf: boolean
name: string
onChange: Function
toggleCollapse: (arg: string) => void
tree: TreeNode
selected: boolean
+ menuItems: MenuItem[]
model: HierarchicalTrackSelectorModel
}
diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts
index 606cecd894..d15d2706a3 100644
--- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts
+++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/generateHierarchy.ts
@@ -7,6 +7,7 @@ import { getTrackName } from '@jbrowse/core/util/tracks'
// locals
import { matches } from './util'
+import { MenuItem } from '@jbrowse/core/ui'
function sortConfs(
confs: AnyConfigurationModel[],
@@ -45,6 +46,7 @@ function sortConfs(
export interface TreeNode {
name: string
id: string
+ trackId?: string
conf?: AnyConfigurationModel
checked?: boolean
isOpenByDefault?: boolean
@@ -55,6 +57,8 @@ export function generateHierarchy({
model,
trackConfs,
extra,
+ noCategories,
+ menuItems,
}: {
model: {
filterText: string
@@ -65,6 +69,8 @@ export function generateHierarchy({
tracks: { configuration: AnyConfigurationModel }[]
}
}
+ noCategories?: boolean
+ menuItems?: MenuItem[]
trackConfs: AnyConfigurationModel[]
extra?: string
}): TreeNode[] {
@@ -100,24 +106,27 @@ export function generateHierarchy({
let currLevel = hierarchy
- // find existing category to put track into or create it
- for (let i = 0; i < categories.length; i++) {
- const category = categories[i]
- const ret = currLevel.children.find(c => c.name === category)
- const id = [extra, categories.slice(0, i + 1).join(',')]
- .filter(f => !!f)
- .join('-')
- if (!ret) {
- const n = {
- children: [],
- name: category,
- id,
- isOpenByDefault: !collapsed.get(id),
+ if (!noCategories) {
+ // find existing category to put track into or create it
+ for (let i = 0; i < categories.length; i++) {
+ const category = categories[i]
+ const ret = currLevel.children.find(c => c.name === category)
+ const id = [extra, categories.slice(0, i + 1).join(',')]
+ .filter(f => !!f)
+ .join('-')
+ if (!ret) {
+ const n = {
+ children: [],
+ name: category,
+ id,
+ isOpenByDefault: !collapsed.get(id),
+ menuItems,
+ }
+ currLevel.children.push(n)
+ currLevel = n
+ } else {
+ currLevel = ret
}
- currLevel.children.push(n)
- currLevel = n
- } else {
- currLevel = ret
}
}
@@ -127,7 +136,8 @@ export function generateHierarchy({
const r = currLevel.children.findIndex(elt => elt.children.length)
const idx = r === -1 ? currLevel.children.length : r
currLevel.children.splice(idx, 0, {
- id: conf.trackId,
+ id: [extra, conf.trackId].filter(f => !!f).join(','),
+ trackId: conf.trackId,
name: getTrackName(conf, session),
conf,
checked: viewTracks.some(f => f.configuration === conf),
diff --git a/plugins/data-management/src/HierarchicalTrackSelectorWidget/model.ts b/plugins/data-management/src/HierarchicalTrackSelectorWidget/model.ts
index 266caee47e..7592896475 100644
--- a/plugins/data-management/src/HierarchicalTrackSelectorWidget/model.ts
+++ b/plugins/data-management/src/HierarchicalTrackSelectorWidget/model.ts
@@ -1,18 +1,55 @@
-import { types, Instance } from 'mobx-state-tree'
+import { types, Instance, addDisposer } from 'mobx-state-tree'
+import { autorun } from 'mobx'
import {
getConf,
readConfObject,
AnyConfigurationModel,
} from '@jbrowse/core/configuration'
-import { dedupe, getSession, notEmpty } from '@jbrowse/core/util'
+import {
+ dedupe,
+ getSession,
+ localStorageGetItem,
+ localStorageSetItem,
+ notEmpty,
+} from '@jbrowse/core/util'
import { ElementId } from '@jbrowse/core/util/types/mst'
import PluginManager from '@jbrowse/core/PluginManager'
// locals
import { filterTracks } from './filterTracks'
-import { TreeNode, generateHierarchy } from './generateHierarchy'
+import { generateHierarchy } from './generateHierarchy'
import { findSubCategories, findTopLevelCategories } from './util'
+const config = new URLSearchParams(window.location.search).get('config')
+
+const localStorageKeyFavoritesF = () =>
+ typeof window !== undefined
+ ? `favorite-tracks-${[
+ window.location.host + window.location.pathname + config,
+ ].join('-')}`
+ : 'empty'
+
+const localStorageKeyRecentlyUsedF = () =>
+ typeof window !== undefined
+ ? `recentlyUsed-tracks-${[
+ window.location.host + window.location.pathname + config,
+ ].join('-')}`
+ : 'empty'
+
+const localStorageKeyTrackSettingsFavoritesF = () =>
+ typeof window !== undefined
+ ? `trackSettings-favorites-${[
+ window.location.host + window.location.pathname + config,
+ ].join('-')}`
+ : 'empty'
+
+const localStorageKeyTrackSettingsRecentlyUsedF = () =>
+ typeof window !== undefined
+ ? `trackSettings-recentlyUsed-${[
+ window.location.host + window.location.pathname + config,
+ ].join('-')}`
+ : 'empty'
+
/**
* #stateModel HierarchicalTrackSelectorWidget
*/
@@ -49,6 +86,36 @@ export default function stateTreeFactory(pluginManager: PluginManager) {
view: types.safeReference(
pluginManager.pluggableMstType('view', 'stateModel'),
),
+ /**
+ * #property
+ */
+ favorites: types.optional(types.array(types.string), () =>
+ JSON.parse(localStorageGetItem(localStorageKeyFavoritesF()) || '[]'),
+ ),
+ /**
+ * #property
+ */
+ recentlyUsed: types.optional(types.array(types.string), () =>
+ JSON.parse(localStorageGetItem(localStorageKeyRecentlyUsedF()) || '[]'),
+ ),
+ /**
+ * #property
+ */
+ showRecentlyUsedCategory: types.optional(types.boolean, () =>
+ JSON.parse(
+ localStorageGetItem(localStorageKeyTrackSettingsFavoritesF()) ||
+ 'true',
+ ),
+ ),
+ /**
+ * #property
+ */
+ showFavoritesCategory: types.optional(types.boolean, () =>
+ JSON.parse(
+ localStorageGetItem(localStorageKeyTrackSettingsRecentlyUsedF()) ||
+ 'true',
+ ),
+ ),
})
.volatile(() => ({
selection: [] as AnyConfigurationModel[],
@@ -91,6 +158,67 @@ export default function stateTreeFactory(pluginManager: PluginManager) {
clearSelection() {
self.selection = []
},
+ /**
+ * #action
+ */
+ isFavorite(config: AnyConfigurationModel) {
+ return self.favorites.includes(config.trackId)
+ },
+ /**
+ * #action
+ */
+ addToFavorites(config: AnyConfigurationModel) {
+ self.favorites.push(readConfObject(config, 'trackId'))
+ },
+ /**
+ * #action
+ */
+ removeFromFavorites(config: AnyConfigurationModel) {
+ const ele = self.favorites.find((id: string) => id === config.trackId)
+ if (ele) {
+ self.favorites.remove(ele)
+ }
+ },
+ /**
+ * #action
+ */
+ clearFavorites() {
+ self.favorites.clear()
+ },
+ /**
+ * #action
+ */
+ isRecentlyUsed(config: AnyConfigurationModel) {
+ return self.recentlyUsed.includes(config.trackId)
+ },
+ /**
+ * #action
+ */
+ addToRecentlyUsed(id: string) {
+ if (self.recentlyUsed.length >= 10) {
+ self.recentlyUsed.shift()
+ }
+ if (!self.recentlyUsed.includes(id)) {
+ self.recentlyUsed.push(id)
+ }
+ },
+ /**
+ * #action
+ */
+ removeFromRecentlyUsed(config: AnyConfigurationModel) {
+ const ele = self.recentlyUsed.find(
+ (id: string) => id === config.trackId,
+ )
+ if (ele) {
+ self.recentlyUsed.remove(ele)
+ }
+ },
+ /**
+ * #action
+ */
+ clearRecentlyUsed() {
+ self.recentlyUsed.clear()
+ },
/**
* #action
*/
@@ -198,33 +326,73 @@ export default function stateTreeFactory(pluginManager: PluginManager) {
...filterTracks(getSession(self).tracks, self),
].filter(notEmpty)
},
- }))
- .views(self => ({
+
/**
- * #method
+ * #getter
+ * filters out tracks that are not in the favorites group
*/
- connectionHierarchy(connection: {
- name: string
- tracks: AnyConfigurationModel[]
- }): TreeNode[] {
- return generateHierarchy({
- model: self,
- trackConfs: self.connectionTrackConfigurations(connection),
- extra: connection.name,
- })
+ get favoriteTracks() {
+ return this.trackConfigurations.filter(track =>
+ self.favorites.includes(readConfObject(track, 'trackId')),
+ )
+ },
+
+ /**
+ * #getter
+ * filters out tracks that are not in the recently used group
+ */
+ get recentlyUsedTracks() {
+ return this.trackConfigurations.filter(track =>
+ self.recentlyUsed.includes(readConfObject(track, 'trackId')),
+ )
},
}))
.views(self => ({
get allTracks() {
const { connectionInstances = [] } = getSession(self)
return [
+ ...(self.showFavoritesCategory
+ ? [
+ {
+ group: '✨Favorites',
+ tracks: self.favoriteTracks,
+ noCategories: true,
+ menuItems: [
+ {
+ label: 'Clear all favorites',
+ onClick: () => self.clearFavorites(),
+ },
+ ],
+ },
+ ]
+ : []),
+ ...(self.showRecentlyUsedCategory
+ ? [
+ {
+ group: '🕒 Recently used',
+ tracks: self.recentlyUsedTracks,
+ isOpenByDefault: false,
+ noCategories: true,
+ menuItems: [
+ {
+ label: 'Clear all recently used',
+ onClick: () => self.clearRecentlyUsed(),
+ },
+ ],
+ },
+ ]
+ : []),
{
group: 'Tracks',
tracks: self.trackConfigurations,
+ noCategories: false,
+ menuItems: [],
},
...connectionInstances.flatMap(c => ({
group: getConf(c, 'name'),
tracks: c.tracks,
+ noCategories: false,
+ menuItems: [],
})),
]
},
@@ -237,18 +405,19 @@ export default function stateTreeFactory(pluginManager: PluginManager) {
return {
name: 'Root',
id: 'Root',
- children: self.allTracks
- .map(s => ({
+ children: self.allTracks.map(s => {
+ return {
name: s.group,
id: s.group,
+ menuItems: s.menuItems,
children: generateHierarchy({
model: self,
trackConfs: s.tracks,
extra: s.group,
+ noCategories: s.noCategories,
}),
- }))
- // always keep the Tracks entry at idx 0
- .filter((f, idx) => idx === 0 || !!f.children.length),
+ }
+ }),
}
},
}))
@@ -277,6 +446,18 @@ export default function stateTreeFactory(pluginManager: PluginManager) {
self.setCategoryCollapsed(path, true)
}
},
+ /**
+ * #action
+ */
+ toggleRecentlyUsedCategory() {
+ self.showRecentlyUsedCategory = !self.showRecentlyUsedCategory
+ },
+ /**
+ * #action
+ */
+ toggleFavoritesCategory() {
+ self.showFavoritesCategory = !self.showFavoritesCategory
+ },
}))
.actions(self => ({
afterCreate() {
@@ -318,6 +499,41 @@ export default function stateTreeFactory(pluginManager: PluginManager) {
)
},
}))
+ .actions(self => ({
+ afterAttach() {
+ addDisposer(
+ self,
+ autorun(() => {
+ localStorageSetItem(
+ localStorageKeyFavoritesF(),
+ JSON.stringify(self.favorites),
+ )
+ localStorageSetItem(
+ localStorageKeyRecentlyUsedF(),
+ JSON.stringify(self.recentlyUsed),
+ )
+ localStorageSetItem(
+ localStorageKeyTrackSettingsFavoritesF(),
+ JSON.stringify(self.showFavoritesCategory),
+ )
+ localStorageSetItem(
+ localStorageKeyTrackSettingsRecentlyUsedF(),
+ JSON.stringify(self.showRecentlyUsedCategory),
+ )
+ }),
+ )
+ },
+ }))
+ .postProcessSnapshot(snap => {
+ const {
+ favorites: _,
+ recentlyUsed: __,
+ showFavoritesCategory: ___,
+ showRecentlyUsedCategory: ____,
+ ...rest
+ } = snap as Omit
+ return rest
+ })
}
export type HierarchicalTrackSelectorStateModel = ReturnType<
diff --git a/products/jbrowse-web/src/tests/BookmarkWidget.test.tsx b/products/jbrowse-web/src/tests/BookmarkWidget.test.tsx
index 7a567b2fb4..67727a662b 100644
--- a/products/jbrowse-web/src/tests/BookmarkWidget.test.tsx
+++ b/products/jbrowse-web/src/tests/BookmarkWidget.test.tsx
@@ -70,7 +70,7 @@ test('using the menu button to bookmark the current region', async () => {
const { session, findByTestId, findByText } = await createView()
const user = userEvent.setup()
- await user.click(await findByTestId('trackContainer'))
+ await user.click(await findByTestId('trackContainer', ...opts))
await user.click(await findByTestId('view_menu_icon'))
await user.click(await findByText('Bookmark current region'))
diff --git a/products/jbrowse-web/src/tests/CircularView.test.tsx b/products/jbrowse-web/src/tests/CircularView.test.tsx
index 557cb68982..8465663df1 100644
--- a/products/jbrowse-web/src/tests/CircularView.test.tsx
+++ b/products/jbrowse-web/src/tests/CircularView.test.tsx
@@ -2,7 +2,7 @@ import '@testing-library/jest-dom'
import { fireEvent, waitFor } from '@testing-library/react'
import configSnapshot from '../../test_data/volvox/config.json'
-import { doBeforeEach, createView, setup } from './util'
+import { doBeforeEach, createView, setup, hts } from './util'
setup()
@@ -25,19 +25,17 @@ test('open a circular view', async () => {
fireEvent.click(await findByText(/Open track/, ...opts))
fireEvent.click(await findByText('Open', ...opts))
fireEvent.click(await findByTestId('circular_track_select'))
- fireEvent.click(await findByTestId('htsTrackEntry-volvox_sv_test', {}, delay))
+ fireEvent.click(await findByTestId(hts('volvox_sv_test'), {}, delay))
await findByTestId('structuralVariantChordRenderer', {}, delay)
await findByTestId('chord-test-vcf-66511')
- fireEvent.click(await findByTestId('htsTrackEntry-volvox_sv_test', {}, delay))
+ fireEvent.click(await findByTestId(hts('volvox_sv_test'), {}, delay))
await waitFor(() => {
expect(
queryByTestId('structuralVariantChordRenderer'),
).not.toBeInTheDocument()
})
- fireEvent.click(
- await findByTestId('htsTrackEntry-volvox_sv_test_renamed', {}, delay),
- )
+ fireEvent.click(await findByTestId(hts('volvox_sv_test_renamed'), {}, delay))
// make sure a chord is rendered
await findByTestId('chord-test-vcf-63101', {}, delay)
diff --git a/products/jbrowse-web/src/tests/ConfigurationEditor.test.tsx b/products/jbrowse-web/src/tests/ConfigurationEditor.test.tsx
index a80208f4ad..a008e3aed6 100644
--- a/products/jbrowse-web/src/tests/ConfigurationEditor.test.tsx
+++ b/products/jbrowse-web/src/tests/ConfigurationEditor.test.tsx
@@ -21,7 +21,11 @@ test('change color on track', async () => {
await user.click(await findByTestId(hts('volvox_filtered_vcf'), {}, delay))
await user.click(
- await findByTestId('htsTrackEntryMenu-volvox_filtered_vcf', {}, delay),
+ await findByTestId(
+ 'htsTrackEntryMenu-Tracks,volvox_filtered_vcf',
+ {},
+ delay,
+ ),
)
await user.click(await findByText('Settings'))
const elt = await findByDisplayValue('goldenrod', {}, delay)
diff --git a/products/jbrowse-web/src/tests/CopyAndDelete.test.tsx b/products/jbrowse-web/src/tests/CopyAndDelete.test.tsx
index 5c58386e04..145f69b754 100644
--- a/products/jbrowse-web/src/tests/CopyAndDelete.test.tsx
+++ b/products/jbrowse-web/src/tests/CopyAndDelete.test.tsx
@@ -42,7 +42,11 @@ test(
view.setNewView(0.05, 5000)
fireEvent.click(
- await findByTestId('htsTrackEntryMenu-volvox_filtered_vcf', {}, delay),
+ await findByTestId(
+ 'htsTrackEntryMenu-Tracks,volvox_filtered_vcf',
+ {},
+ delay,
+ ),
)
fireEvent.click(await findByText('Copy track'))
fireEvent.click(await findByText('volvox filtered vcf (copy)'))
@@ -80,7 +84,7 @@ test(
// copy ref seq track disabled
fireEvent.click(
- await findByTestId('htsTrackEntryMenu-volvox_refseq', {}, delay),
+ await findByTestId('htsTrackEntryMenu-Tracks,volvox_refseq', {}, delay),
)
fireEvent.click(await findByText('Copy track'))
expect(queryByText(/Session tracks/)).toBeNull()
@@ -101,7 +105,11 @@ test(
view.setNewView(0.05, 5000)
fireEvent.click(
- await findByTestId('htsTrackEntryMenu-volvox_filtered_vcf', {}, delay),
+ await findByTestId(
+ 'htsTrackEntryMenu-Tracks,volvox_filtered_vcf',
+ {},
+ delay,
+ ),
)
fireEvent.click(await findByText('Copy track'))
fireEvent.click(await findByText('volvox filtered vcf (copy)'))
diff --git a/products/jbrowse-web/src/tests/DrawerWidget.test.tsx b/products/jbrowse-web/src/tests/DrawerWidget.test.tsx
index 6d9425d7a9..8fff4eedb8 100644
--- a/products/jbrowse-web/src/tests/DrawerWidget.test.tsx
+++ b/products/jbrowse-web/src/tests/DrawerWidget.test.tsx
@@ -42,7 +42,11 @@ test('widget drawer navigation', async () => {
// opens a config editor widget
fireEvent.click(await findByTestId(hts('volvox_filtered_vcf'), {}, delay))
fireEvent.click(
- await findByTestId('htsTrackEntryMenu-volvox_filtered_vcf', {}, delay),
+ await findByTestId(
+ 'htsTrackEntryMenu-Tracks,volvox_filtered_vcf',
+ {},
+ delay,
+ ),
)
fireEvent.click(await findByText('Settings'))
await findByTestId('configEditor', {}, delay)
diff --git a/products/jbrowse-web/src/tests/StatsEstimation.test.tsx b/products/jbrowse-web/src/tests/StatsEstimation.test.tsx
index 24b3765cc9..17a01b2fb6 100644
--- a/products/jbrowse-web/src/tests/StatsEstimation.test.tsx
+++ b/products/jbrowse-web/src/tests/StatsEstimation.test.tsx
@@ -36,7 +36,7 @@ test('test stats estimation pileup, force load to see', async () => {
const { view, findAllByText, findByTestId } = await createView()
view.setNewView(25.07852564102564, 283)
- fireEvent.click(await findByTestId('htsTrackEntry-volvox_cram_pileup', ...o))
+ fireEvent.click(await findByTestId(hts('volvox_cram_pileup'), ...o))
await findAllByText(/Requested too much data/, ...o)
const buttons = await findAllByText(/Force load/, ...o)
@@ -49,7 +49,7 @@ test('test stats estimation on vcf track, zoom in to see', async () => {
const { view, findAllByText, findAllByTestId, findByTestId } =
await createView()
view.setNewView(34, 5)
- fireEvent.click(await findByTestId('htsTrackEntry-variant_colors', ...o))
+ fireEvent.click(await findByTestId(hts('variant_colors'), ...o))
await findAllByText(/Zoom in to see features/, ...o)
const before = view.bpPerPx
fireEvent.click(await findByTestId('zoom_in'))
@@ -64,7 +64,7 @@ test('test stats estimation on vcf track, force load to see', async () => {
await createView()
view.setNewView(34, 5)
await findAllByText('ctgA', ...o)
- fireEvent.click(await findByTestId('htsTrackEntry-variant_colors', ...o))
+ fireEvent.click(await findByTestId(hts('variant_colors'), ...o))
fireEvent.click((await findAllByText(/Force load/, ...o))[0])
await findAllByTestId('box-test-vcf-605224', ...o)
}, 30000)
diff --git a/products/jbrowse-web/src/tests/util.tsx b/products/jbrowse-web/src/tests/util.tsx
index 20176062ca..a184b124ab 100644
--- a/products/jbrowse-web/src/tests/util.tsx
+++ b/products/jbrowse-web/src/tests/util.tsx
@@ -113,7 +113,7 @@ export function JBrowse(props: any) {
)
}
-export const hts = (str: string) => 'htsTrackEntry-' + str
+export const hts = (str: string) => 'htsTrackEntry-Tracks,' + str
export const pc = (str: string) => `prerendered_canvas_${str}_done`
export const pv = (str: string) => pc(`{volvox}ctgA:${str}`)