Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions src/__test__/components/data-exploration/CellInfo.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,45 @@ const cellInfo = {
cellSets: ['Louvain : cluster1', 'anotherRootCluster : cluster2'],
};

const coordinates = {
current: {
x: 100,
y: 200,
width: 500,
height: 500,
},
};
const coordinates = { x: 100, y: 200 };

// each tooltip line renders as "<bold label> value", so the label and value are
// separate text nodes — match them individually
const lineWithLabelAndValue = (label, value) => (
(content, node) => node?.textContent === `${label} ${value}`
);

describe('CellInfo', () => {
it('renders cell info card with properties', () => {
render(<CellInfo coordinates={coordinates} cellInfo={cellInfo} />);
expect(screen.getByText(`Gene name: ${cellInfo.geneName}`)).toBeInTheDocument();
expect(screen.getByText(`Cell id: ${cellInfo.cellId}`)).toBeInTheDocument();
expect(screen.getByText(`Expression Level: ${cellInfo.expression}`)).toBeInTheDocument();
expect(screen.getByText(cellInfo.cellSets[0])).toBeInTheDocument();
expect(screen.getByText(cellInfo.cellSets[1])).toBeInTheDocument();
render(
<CellInfo
containerWidth={500}
containerHeight={500}
coordinates={coordinates}
cellInfo={cellInfo}
/>,
);
expect(screen.getByText(lineWithLabelAndValue('Cell id:', cellInfo.cellId))).toBeInTheDocument();
expect(screen.getByText(lineWithLabelAndValue('Gene name:', cellInfo.geneName))).toBeInTheDocument();
expect(
screen.getByText(lineWithLabelAndValue('Expression Level:', cellInfo.expression)),
).toBeInTheDocument();
// cell-set entries split into a bold "Parent :" label + value
expect(screen.getByText('cluster1')).toBeInTheDocument();
expect(screen.getByText('cluster2')).toBeInTheDocument();
});

it('renders category labels in bold', () => {
render(
<CellInfo
containerWidth={500}
containerHeight={500}
coordinates={coordinates}
cellInfo={cellInfo}
/>,
);
const label = screen.getByText('Cell id:');
expect(label.tagName).toBe('SPAN');
expect(label).toHaveStyle({ fontWeight: 600 });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,15 @@ describe('SpatialViewer', () => {
);

// a zip store + zarr root per returned url, for both the image and segmentation grids
// (roots are memoised by url, so regrouping rebuilds the grid without re-opening stores)
expect(zarrRoot).toHaveBeenCalledTimes(4);

// loads a grid for the image and a grid for the segmentations
expect(loadOmeZarrGrid).toHaveBeenCalledTimes(2);
// loads a grid for the image (emptyFill undefined) and one for the segmentations
// (emptyFill 0). The grid may be rebuilt when the sample grouping resolves, so
// assert both grids load rather than an exact call count.
const emptyFillArgs = loadOmeZarrGrid.mock.calls.map((call) => call[2]);
expect(emptyFillArgs).toContain(undefined); // image grid
expect(emptyFillArgs).toContain(0); // segmentation grid
});

it('renders correctly with initial data', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,10 @@ describe('HeatmapPlot', () => {
await vitesscePropsSpy.setGeneHighlight(highlightedGene);
});

// It shows the cell info tooltip
expect(screen.getByText(/Cell id: 2/i)).toBeInTheDocument();
expect(screen.getByText(/Gene name: S100a4/i)).toBeInTheDocument();
// It shows the cell info tooltip (label + value are separate nodes now, so
// match the whole line's text content)
expect(screen.getByText((c, node) => node?.textContent === 'Cell id: 2')).toBeInTheDocument();
expect(screen.getByText((c, node) => node?.textContent === 'Gene name: S100a4')).toBeInTheDocument();

// On hovering outside
await act(async () => {
Expand All @@ -417,8 +418,8 @@ describe('HeatmapPlot', () => {
});

// It shows the track cell info tooltip
expect(screen.getByText(/Cell id: 4/i)).toBeInTheDocument();
expect(screen.getByText(/Group name: Cluster 0/i)).toBeInTheDocument();
expect(screen.getByText((c, node) => node?.textContent === 'Cell id: 4')).toBeInTheDocument();
expect(screen.getByText((c, node) => node?.textContent === 'Group name: Cluster 0')).toBeInTheDocument();

// On hovering outside heatmap tracks
await act(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ exports[`cellInfoReducer Updates cell info state on update action 1`] = `
"store": "cellSets",
},
"groupedTrack": "louvain",
"hoverSource": undefined,
"selectedTracks": [
"louvain",
],
Expand Down
64 changes: 64 additions & 0 deletions src/__test__/utils/data-exploration/getCellInfoCoordinates.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import getCellInfoCoordinates from 'utils/data-exploration/getCellInfoCoordinates';

const PADDING = 16;
const panel = { boundingX: 500, boundingY: 500 };
const popup = { width: 120, height: 80 };

const coords = (x, y) => getCellInfoCoordinates(
{ x, y }, popup, panel.boundingX, panel.boundingY,
);

const withinPanel = ({ left, top }) => (
left >= 0
&& top >= 0
&& left + popup.width <= panel.boundingX
&& top + popup.height <= panel.boundingY
);

describe('getCellInfoCoordinates', () => {
it('places the tooltip below-right of the cursor when there is room', () => {
const { left, top } = coords(100, 200);
expect(left).toBe(100 + PADDING);
expect(top).toBe(200 + PADDING);
});

it('flips to the left of the cursor when it would overflow the right edge', () => {
const { left } = coords(450, 200); // 450 + 16 + 120 > 500
expect(left).toBe(450 - PADDING - popup.width);
});

it('flips above the cursor when it would overflow the bottom edge', () => {
const { top } = coords(100, 480); // 480 + 16 + 80 > 500
expect(top).toBe(480 - PADDING - popup.height);
});

it('never falls outside the panel near the top-left edge', () => {
// cursor in the corner: flipping left/up alone would go negative
const result = coords(2, 2);
expect(withinPanel(result)).toBe(true);
expect(result.left).toBeGreaterThanOrEqual(0);
expect(result.top).toBeGreaterThanOrEqual(0);
});

it('never falls outside the panel near the bottom-right edge', () => {
const result = coords(498, 498);
expect(withinPanel(result)).toBe(true);
});

it('stays inside the panel when a tall tooltip flips above the cursor', () => {
// tall tooltip, cursor low in the panel — the flip used to push `top`
// negative; it must land fully inside instead
const tall = { width: 120, height: 220 };
const { left, top } = getCellInfoCoordinates({ x: 40, y: 300 }, tall, 300, 400);
expect(top).toBeGreaterThanOrEqual(PADDING);
expect(top + tall.height).toBeLessThanOrEqual(400 - PADDING);
expect(left).toBeGreaterThanOrEqual(PADDING);
});

it('pins to the top-left corner when the tooltip is larger than the panel', () => {
const huge = { width: 600, height: 600 };
const { left, top } = getCellInfoCoordinates({ x: 100, y: 100 }, huge, 400, 400);
expect(left).toBe(PADDING);
expect(top).toBe(PADDING);
});
});
38 changes: 38 additions & 0 deletions src/__test__/utils/plotSpecs/generateSpatialFeatureSpec.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,42 @@ describe('filterCells / generateData', () => {
// only even cell ids survive
result.forEach((datum) => expect(datum.x % 2).toBe(0));
});

describe('viewport extent (padded vs original dims)', () => {
const signalValue = (spec, name) => spec.signals.find((s) => s.name === name).value;

it('uses the full padded extent when originalSize is absent', () => {
const spec = generateSpec(baseConfig(), 'mock', imageData, []);
// imageData = { imageWidth: 1000, imageHeight: 500 }
expect(signalValue(spec, 'initXdom')).toEqual([0, 1000]);
expect(signalValue(spec, 'initYdom')).toEqual([0, 500]);
expect(signalValue(spec, 'boundsX')).toEqual([0, 1000]);
expect(signalValue(spec, 'boundsY')).toEqual([0, 500]);
});

it('caps the viewport to the original tissue extent when padded', () => {
// padded canvas 1000x500, tissue only 600x300 (padding right + bottom)
const padded = {
imageWidth: 1000, imageHeight: 500, origWidth: 600, origHeight: 300,
};
const spec = generateSpec(baseConfig(), 'mock', padded, []);

// x is not flipped: content is on the left
expect(signalValue(spec, 'initXdom')).toEqual([0, 600]);
// y IS flipped (imageHeight - y), so tissue sits at the TOP of the range
expect(signalValue(spec, 'initYdom')).toEqual([500 - 300, 500]);
// max zoom-out (bounds) matches the tissue extent — no blank padding
expect(signalValue(spec, 'boundsX')).toEqual([0, 600]);
expect(signalValue(spec, 'boundsY')).toEqual([200, 500]);
});

it('keeps the y-flip anchored to the padded height', () => {
const padded = {
imageWidth: 1000, imageHeight: 500, origWidth: 600, origHeight: 300,
};
const spec = generateSpec(padded && baseConfig(), 'mock', padded, []);
const flip = JSON.stringify(spec).includes('500 - datum.y');
expect(flip).toBe(true);
});
});
});
18 changes: 18 additions & 0 deletions src/__test__/utils/plotUtils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ describe('offsetCentroids', () => {
expect(offset[2]).toEqual([205, 5]);
});

it('places samples by the provided sampleRowCol (grouped rows)', () => {
const results = [];
results[0] = [10, 20]; // sample-0
results[2] = [5, 5]; // sample-1

// group layout: sample-0 in row 0, sample-1 in row 1 (one per row)
const sampleRowCol = [{ row: 0, col: 0 }, { row: 1, col: 0 }];
const groupedGrid = [2, 1]; // 2 rows, 1 column

const offset = offsetCentroids(
results, properties, sampleIds, perImageShape, groupedGrid, sampleRowCol,
);

// sample-0: no offset; sample-1: shifted DOWN by one image height (row 1)
expect(offset[0]).toEqual([10, 20]);
expect(offset[2]).toEqual([5, 105]);
});

it('leaves null/undefined (filtered) cells as holes, producing a sparse array', () => {
const results = [];
results[0] = [10, 20];
Expand Down
79 changes: 79 additions & 0 deletions src/__test__/utils/spatial/buildSpatialGridLayout.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import buildSpatialGridLayout from 'utils/spatial/buildSpatialGridLayout';

describe('buildSpatialGridLayout', () => {
describe('ungrouped (dense row-major, max 4 columns)', () => {
it('lays out 3 samples in a single row', () => {
const { gridShape, slotToSampleIndex, sampleRowCol } = buildSpatialGridLayout(3);
expect(gridShape).toEqual([1, 3]);
expect(slotToSampleIndex).toEqual([0, 1, 2]);
expect(sampleRowCol).toEqual([
{ row: 0, col: 0 }, { row: 0, col: 1 }, { row: 0, col: 2 },
]);
});

it('wraps past 4 columns and fills the tail with nulls', () => {
const { gridShape, slotToSampleIndex } = buildSpatialGridLayout(5);
expect(gridShape).toEqual([2, 4]);
// 5 samples in a 2x4 grid → 3 trailing filler cells
expect(slotToSampleIndex).toEqual([0, 1, 2, 3, 4, null, null, null]);
});

it('handles zero samples without crashing', () => {
const { gridShape, slotToSampleIndex } = buildSpatialGridLayout(0);
expect(gridShape).toEqual([1, 1]);
expect(slotToSampleIndex).toEqual([null]);
});

it('places samples in the provided order (Cell sets tile order)', () => {
// 3 samples displayed in reverse of their natural index order
const { slotToSampleIndex, sampleRowCol } = buildSpatialGridLayout(3, null, [2, 0, 1]);
expect(slotToSampleIndex).toEqual([2, 0, 1]);
expect(sampleRowCol[2]).toEqual({ row: 0, col: 0 });
expect(sampleRowCol[0]).toEqual({ row: 0, col: 1 });
expect(sampleRowCol[1]).toEqual({ row: 0, col: 2 });
});
});

describe('grouped (one row per group)', () => {
// 5 samples: group A = [0, 1, 2], group B = [3, 4]
const groups = [
{ groupKey: 'A', sampleIndices: [0, 1, 2] },
{ groupKey: 'B', sampleIndices: [3, 4] },
];

it('puts each group in its own row, width = largest group', () => {
const { gridShape } = buildSpatialGridLayout(5, groups);
expect(gridShape).toEqual([2, 3]); // 2 groups, largest has 3
});

it('leaves filler cells at the end of the smaller group row', () => {
const { slotToSampleIndex } = buildSpatialGridLayout(5, groups);
// row 0: A → [0,1,2]; row 1: B → [3,4,null]
expect(slotToSampleIndex).toEqual([0, 1, 2, 3, 4, null]);
});

it('maps each sample to its group row and column', () => {
const { sampleRowCol } = buildSpatialGridLayout(5, groups);
expect(sampleRowCol[0]).toEqual({ row: 0, col: 0 });
expect(sampleRowCol[2]).toEqual({ row: 0, col: 2 });
expect(sampleRowCol[3]).toEqual({ row: 1, col: 0 });
expect(sampleRowCol[4]).toEqual({ row: 1, col: 1 });
});

it('preserves group order as the row order', () => {
const reordered = [
{ groupKey: 'B', sampleIndices: [3, 4] },
{ groupKey: 'A', sampleIndices: [0, 1, 2] },
];
const { sampleRowCol } = buildSpatialGridLayout(5, reordered);
// B is now row 0, A is row 1
expect(sampleRowCol[3].row).toBe(0);
expect(sampleRowCol[0].row).toBe(1);
});

it('falls back to dense layout when groups is empty', () => {
const { gridShape } = buildSpatialGridLayout(3, []);
expect(gridShape).toEqual([1, 3]);
});
});
});
Loading
Loading