Skip to content

Commit 438eeb3

Browse files
add test coverage
1 parent dd84dd1 commit 438eeb3

5 files changed

Lines changed: 439 additions & 0 deletions

File tree

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ module.exports = {
1818
coverageReporters: ["text", "lcov", "html"],
1919
moduleNameMapper: {
2020
"^@/(.*)$": "<rootDir>/src/$1",
21+
// The `build/` CJS modules reference the raw ESM sources `src/version.js` and
22+
// `src/blobs.js`, which jest cannot transform. Stub them for unit tests that load
23+
// `build/` modules (e.g. the ConfidenceSlider toolbox item via html_builder).
24+
"src/version(\\.js)?$": "<rootDir>/tests/testing-utils/version_stub.js",
25+
"src/blobs(\\.js)?$": "<rootDir>/tests/testing-utils/blobs_stub.js",
2126
},
2227
transform: {
2328
"^.+\\.(js|ts)$": "babel-jest",

tests/confidence_slider.test.js

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
// Unit tests for the ConfidenceSlider toolbox item
2+
// NOTE: require `configuration` before `confidence_slider`. `configuration` pulls in `toolbox`
3+
// (which defines the `ToolboxItem` base class) and then `confidence_slider`, ensuring the base
4+
// class is initialized before `ConfidenceSlider extends ToolboxItem` evaluates. Requiring
5+
// `confidence_slider` first hits the circular import before `ToolboxItem` is defined.
6+
const { Configuration, AllowedToolboxItem } = require("../build/configuration");
7+
const { ConfidenceSlider } = require("../build/toolbox_items/confidence_slider");
8+
9+
/**
10+
* Build a minimal annotation-like object.
11+
*/
12+
function make_annotation(id, spatial_type, payloads, extra = {}) {
13+
return {
14+
id,
15+
spatial_type,
16+
classification_payloads: payloads,
17+
deprecated: false,
18+
deprecated_by: { human: false },
19+
...extra,
20+
};
21+
}
22+
23+
/**
24+
* Build a minimal mock ULabel object with a single subtask.
25+
*/
26+
function make_ulabel(annotations, config = {}) {
27+
const access = {};
28+
for (const annotation of annotations) {
29+
access[annotation.id] = annotation;
30+
}
31+
return {
32+
config: { confidence_slider_toolbox_item: config },
33+
subtasks: {
34+
st: {
35+
annotations: { access },
36+
allowed_modes: ["bbox", "polygon", "point", "polyline"],
37+
class_defs: [
38+
{ name: "Car", id: 10, color: "red", keybind: null },
39+
{ name: "Truck", id: 11, color: "blue", keybind: null },
40+
],
41+
},
42+
},
43+
redraw_multiple_spatial_annotations: jest.fn(),
44+
toolbox: { redraw_update_items: jest.fn() },
45+
};
46+
}
47+
48+
describe("ConfidenceSlider", () => {
49+
beforeEach(() => {
50+
// Reset the DOM so slider lookups fall back to defaults
51+
document.body.innerHTML = "";
52+
});
53+
54+
describe("construction and config resolution", () => {
55+
test("applies defaults and identifies as ConfidenceSlider", () => {
56+
const cs = new ConfidenceSlider(make_ulabel([], { filter_on_load: false }));
57+
58+
expect(cs.get_toolbox_item_type()).toBe("ConfidenceSlider");
59+
expect(cs.filter_min).toBe(0);
60+
expect(cs.filter_max).toBe(100);
61+
expect(cs.step_value).toBe(1);
62+
expect(cs.deprecated_by_key).toBe("confidence_slider");
63+
});
64+
65+
test("class_filter_mode 'toggle' shows the toggle and starts in all mode", () => {
66+
const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "toggle", filter_on_load: false }));
67+
expect(cs.show_class_toggle).toBe(true);
68+
expect(cs.is_class_mode).toBe(false);
69+
});
70+
71+
test("class_filter_mode 'all-only' hides the toggle and stays in all mode", () => {
72+
const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false }));
73+
expect(cs.show_class_toggle).toBe(false);
74+
expect(cs.is_class_mode).toBe(false);
75+
});
76+
77+
test("class_filter_mode 'class-only' hides the toggle and starts in class mode", () => {
78+
const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "class-only", filter_on_load: false }));
79+
expect(cs.show_class_toggle).toBe(false);
80+
expect(cs.is_class_mode).toBe(true);
81+
});
82+
83+
test("merges partial config with the defaults", () => {
84+
const cs = new ConfidenceSlider(make_ulabel([], { filter_max: 50, filter_on_load: false }));
85+
expect(cs.filter_max).toBe(50); // provided
86+
expect(cs.filter_min).toBe(0); // default
87+
expect(cs.step_value).toBe(1); // default
88+
});
89+
});
90+
91+
describe("filter_annotations - all mode", () => {
92+
test("deprecates annotations below the threshold and shows those at/above", () => {
93+
const high = make_annotation("high", "bbox", [{ class_id: 10, confidence: 0.9 }]);
94+
const mid = make_annotation("mid", "bbox", [{ class_id: 10, confidence: 0.5 }]);
95+
const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]);
96+
const ulabel = make_ulabel([high, mid, low], {
97+
class_filter_mode: "all-only",
98+
default_values: { all: { confidence: 60 } },
99+
filter_on_load: false,
100+
});
101+
const cs = new ConfidenceSlider(ulabel);
102+
103+
cs.filter_annotations(false);
104+
105+
expect(high.deprecated).toBe(false);
106+
expect(mid.deprecated).toBe(true);
107+
expect(low.deprecated).toBe(true);
108+
expect(mid.deprecated_by.confidence_slider).toBe(true);
109+
});
110+
111+
test("a threshold of 0 deprecates nothing", () => {
112+
const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]);
113+
const cs = new ConfidenceSlider(make_ulabel([low], {
114+
class_filter_mode: "all-only",
115+
default_values: { all: { confidence: 0 } },
116+
filter_on_load: false,
117+
}));
118+
119+
cs.filter_annotations(false);
120+
121+
expect(low.deprecated).toBe(false);
122+
});
123+
124+
test("lowering the threshold un-deprecates previously filtered annotations", () => {
125+
const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]);
126+
const cs = new ConfidenceSlider(make_ulabel([low], {
127+
class_filter_mode: "all-only",
128+
default_values: { all: { confidence: 50 } },
129+
filter_on_load: false,
130+
}));
131+
132+
cs.filter_annotations(false);
133+
expect(low.deprecated).toBe(true);
134+
135+
// Lower the threshold and re-filter
136+
cs.default_values.all.confidence = 0;
137+
cs.filter_annotations(false);
138+
139+
expect(low.deprecated).toBe(false);
140+
expect(low.deprecated_by.confidence_slider).toBe(false);
141+
});
142+
});
143+
144+
describe("filter_annotations - class mode", () => {
145+
test("filters each class by its own threshold and ignores untargeted classes", () => {
146+
const car = make_annotation("car", "bbox", [{ class_id: 10, confidence: 0.5 }]);
147+
const truck = make_annotation("truck", "bbox", [{ class_id: 11, confidence: 0.5 }]);
148+
const other = make_annotation("other", "bbox", [{ class_id: 12, confidence: 0.5 }]);
149+
const cs = new ConfidenceSlider(make_ulabel([car, truck, other], {
150+
class_filter_mode: "class-only",
151+
default_values: { all: { confidence: 0 }, 10: { confidence: 60 }, 11: { confidence: 30 } },
152+
filter_on_load: false,
153+
}));
154+
155+
cs.filter_annotations(false);
156+
157+
expect(car.deprecated).toBe(true); // 50 < 60
158+
expect(truck.deprecated).toBe(false); // 50 >= 30
159+
expect(other.deprecated).toBe(false); // class 12 has no slider
160+
});
161+
162+
test("targets an annotation by its highest-confidence (argmax) class", () => {
163+
// argmax class is 11 (0.8); the class-11 threshold should apply
164+
const anno = make_annotation("multi", "bbox", [
165+
{ class_id: 10, confidence: 0.3 },
166+
{ class_id: 11, confidence: 0.8 },
167+
]);
168+
const cs = new ConfidenceSlider(make_ulabel([anno], {
169+
class_filter_mode: "class-only",
170+
default_values: { all: { confidence: 0 }, 10: { confidence: 90 }, 11: { confidence: 50 } },
171+
filter_on_load: false,
172+
}));
173+
174+
cs.filter_annotations(false);
175+
176+
// 80 >= 50 (class-11 threshold) -> shown, even though 80 would fail class 10's 90
177+
expect(anno.deprecated).toBe(false);
178+
});
179+
});
180+
181+
describe("target_spatial_types", () => {
182+
test("only filters the configured spatial types", () => {
183+
const point = make_annotation("p", "point", [{ class_id: 10, confidence: 0.1 }]);
184+
const box = make_annotation("b", "bbox", [{ class_id: 10, confidence: 0.1 }]);
185+
const cs = new ConfidenceSlider(make_ulabel([point, box], {
186+
class_filter_mode: "all-only",
187+
default_values: { all: { confidence: 50 } },
188+
target_spatial_types: ["bbox"],
189+
filter_on_load: false,
190+
}));
191+
192+
cs.filter_annotations(false);
193+
194+
expect(box.deprecated).toBe(true);
195+
expect(point.deprecated).toBe(false);
196+
expect(point.deprecated_by.confidence_slider).toBeUndefined();
197+
});
198+
});
199+
200+
describe("deprecated_by OR-logic", () => {
201+
test("does not un-deprecate an annotation deprecated by a human", () => {
202+
const anno = make_annotation("h", "bbox", [{ class_id: 10, confidence: 0.9 }], {
203+
deprecated: true,
204+
deprecated_by: { human: true },
205+
});
206+
const cs = new ConfidenceSlider(make_ulabel([anno], {
207+
class_filter_mode: "all-only",
208+
default_values: { all: { confidence: 0 } },
209+
filter_on_load: false,
210+
}));
211+
212+
cs.filter_annotations(false);
213+
214+
expect(anno.deprecated_by.confidence_slider).toBe(false);
215+
expect(anno.deprecated).toBe(true); // still deprecated by human
216+
});
217+
});
218+
219+
describe("filter_on_load", () => {
220+
test("filters during construction when filter_on_load is true", () => {
221+
const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]);
222+
new ConfidenceSlider(make_ulabel([low], {
223+
class_filter_mode: "all-only",
224+
default_values: { all: { confidence: 50 } },
225+
filter_on_load: true,
226+
}));
227+
228+
expect(low.deprecated).toBe(true);
229+
});
230+
231+
test("does not filter during construction when filter_on_load is false", () => {
232+
const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]);
233+
new ConfidenceSlider(make_ulabel([low], {
234+
class_filter_mode: "all-only",
235+
default_values: { all: { confidence: 50 } },
236+
filter_on_load: false,
237+
}));
238+
239+
expect(low.deprecated).toBe(false);
240+
});
241+
});
242+
243+
describe("redraw", () => {
244+
test("redraws changed annotations and updates toolbox items when redraw is true", () => {
245+
const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]);
246+
const ulabel = make_ulabel([low], {
247+
class_filter_mode: "all-only",
248+
default_values: { all: { confidence: 50 } },
249+
filter_on_load: false,
250+
});
251+
const cs = new ConfidenceSlider(ulabel);
252+
253+
cs.filter_annotations(true);
254+
255+
expect(ulabel.redraw_multiple_spatial_annotations).toHaveBeenCalledWith(["low"], "st");
256+
expect(ulabel.toolbox.redraw_update_items).toHaveBeenCalled();
257+
});
258+
});
259+
260+
describe("get_current_values", () => {
261+
test("returns null when no sliders are in the DOM", () => {
262+
const cs = new ConfidenceSlider(make_ulabel([], { filter_on_load: false }));
263+
expect(cs.get_current_values()).toBeNull();
264+
});
265+
266+
test("reads the single 'all' slider value", () => {
267+
const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false }));
268+
document.body.innerHTML = `<input id="confidence-slider-all" class="confidence-slider-input" type="range" min="0" max="100" value="40">`;
269+
270+
expect(cs.get_current_values()).toEqual({ all: { confidence: 40 } });
271+
});
272+
273+
test("reads per-class slider values in class mode", () => {
274+
const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "class-only", filter_on_load: false }));
275+
document.body.innerHTML = `
276+
<input id="confidence-slider-all" class="confidence-slider-input" type="range" min="0" max="100" value="40">
277+
<input id="confidence-slider-10" class="confidence-slider-input" type="range" min="0" max="100" value="30">
278+
`;
279+
280+
expect(cs.get_current_values()).toEqual({
281+
all: { confidence: 40 },
282+
10: { confidence: 30 },
283+
});
284+
});
285+
});
286+
287+
describe("get_html", () => {
288+
test("renders the multi-class toggle only in toggle mode", () => {
289+
const toggle = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "toggle", filter_on_load: false }));
290+
expect(toggle.get_html()).toContain("confidence-slider-multi-checkbox");
291+
292+
const allOnly = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false }));
293+
expect(allOnly.get_html()).not.toContain("confidence-slider-multi-checkbox");
294+
});
295+
296+
test("target_class_ids limits which per-class sliders are rendered", () => {
297+
const cs = new ConfidenceSlider(make_ulabel([], {
298+
class_filter_mode: "class-only",
299+
target_class_ids: [10],
300+
filter_on_load: false,
301+
}));
302+
const html = cs.get_html();
303+
304+
expect(html).toContain("confidence-slider-all");
305+
expect(html).toContain("confidence-slider-10");
306+
expect(html).not.toContain("confidence-slider-11");
307+
});
308+
});
309+
});
310+
311+
describe("Default toolbox order", () => {
312+
test("includes ConfidenceSlider and not the deprecated KeypointSlider", () => {
313+
const config = new Configuration();
314+
expect(config.toolbox_order).toContain(AllowedToolboxItem.ConfidenceSlider);
315+
expect(config.toolbox_order).not.toContain(AllowedToolboxItem.KeypointSlider);
316+
});
317+
});

0 commit comments

Comments
 (0)