Download tests/test_analysis.py from InsightUX/Insight_UX_1.0: direct link, hf CLI and curl.
- Browser
- Download file 8.82 kB
-
https://huggingface.co/InsightUX/Insight_UX_1.0/resolve/main/tests/test_analysis.py
- Command line
-
hf download hf://InsightUX/Insight_UX_1.0/tests/test_analysis.py
-
curl -L -o test_analysis.py https://huggingface.co/InsightUX/Insight_UX_1.0/resolve/main/tests/test_analysis.py
8.82 kB
| """ | |
| Unit tests for analysis.py's pure computation — session loading, AOI | |
| attribution, dwell ranking, mouse summarization. No cv2/mediapipe needed | |
| (analysis.py itself only imports stdlib + theme). | |
| """ | |
| import os | |
| import json | |
| import analysis | |
| # --- _load_jsonl / load_session ------------------------------------------- | |
| def test_load_jsonl_missing_file_returns_empty(tmp_path): | |
| assert analysis._load_jsonl(str(tmp_path / "nope.jsonl")) == [] | |
| def test_load_jsonl_skips_malformed_lines(tmp_path): | |
| path = tmp_path / "log.jsonl" | |
| path.write_text('{"a": 1}\nnot json\n{"b": 2}\n\n', encoding="utf-8") | |
| records = analysis._load_jsonl(str(path)) | |
| assert records == [{"a": 1}, {"b": 2}] | |
| def _write_jsonl(path, records): | |
| with open(path, "w", encoding="utf-8") as f: | |
| for r in records: | |
| f.write(json.dumps(r) + "\n") | |
| def test_load_session_filters_by_type_and_sorts_by_time(tmp_path): | |
| session_dir = tmp_path / "20260101_120000" | |
| session_dir.mkdir() | |
| _write_jsonl(session_dir / "gaze_log.jsonl", [ | |
| {"type": "gaze", "t": 2.0, "sx": 10, "sy": 10}, | |
| {"type": "other", "t": 0.5}, | |
| {"type": "gaze", "t": 1.0, "sx": 5, "sy": 5}, | |
| ]) | |
| _write_jsonl(session_dir / "dom_log.jsonl", [ | |
| {"type": "dom", "t": 1.5, "url": "https://example.com", "aois": []}, | |
| ]) | |
| gaze, dom = analysis.load_session(str(session_dir)) | |
| assert [g["t"] for g in gaze] == [1.0, 2.0] # sorted, "other" filtered out | |
| assert len(dom) == 1 | |
| assert dom[0]["url"] == "https://example.com" | |
| def test_load_session_handles_missing_files(tmp_path): | |
| session_dir = tmp_path / "20260101_120000" | |
| session_dir.mkdir() | |
| gaze, dom = analysis.load_session(str(session_dir)) | |
| assert gaze == [] | |
| assert dom == [] | |
| # --- session_summary -------------------------------------------------------- | |
| def test_session_summary_empty_gaze(): | |
| summary = analysis.session_summary([], []) | |
| assert summary == {"duration": 0.0, "url": None, "samples": 0} | |
| def test_session_summary_computes_duration_and_url(): | |
| gaze = [{"t": 10.0}, {"t": 12.5}, {"t": 15.0}] | |
| dom = [{"t": 10.0, "url": "https://a.com"}, {"t": 14.0, "url": "https://b.com"}] | |
| summary = analysis.session_summary(gaze, dom) | |
| assert summary["duration"] == 5.0 | |
| assert summary["url"] == "https://b.com" # most recent dom snapshot | |
| assert summary["samples"] == 3 | |
| def test_session_summary_url_none_without_dom(): | |
| summary = analysis.session_summary([{"t": 0.0}, {"t": 1.0}], []) | |
| assert summary["url"] is None | |
| # --- friendly_label ----------------------------------------------------- | |
| def test_friendly_label_handles_none_and_empty(): | |
| assert analysis.friendly_label(None) == "Unlabeled area" | |
| assert analysis.friendly_label("") == "Unlabeled area" | |
| def test_friendly_label_known_landmarks(): | |
| assert analysis.friendly_label("navbar") == "Navigation bar" | |
| assert analysis.friendly_label("header") == "Page header" | |
| assert analysis.friendly_label("footer") == "Page footer" | |
| assert analysis.friendly_label("video") == "Video" | |
| def test_friendly_label_image(): | |
| assert analysis.friendly_label("img: logo.png") == "Image — logo.png" | |
| def test_friendly_label_headings(): | |
| assert analysis.friendly_label("h1: Welcome") == 'Main heading — “Welcome”' | |
| assert analysis.friendly_label("h2: About") == 'Heading — “About”' | |
| assert analysis.friendly_label("h3: Details") == 'Sub-heading — “Details”' | |
| def test_friendly_label_paragraph(): | |
| assert analysis.friendly_label("p (some text)") == 'Text — “some text…”' | |
| def test_friendly_label_id_and_class_selectors(): | |
| assert analysis.friendly_label("#hero") == "Section: hero" | |
| assert analysis.friendly_label(".card") == "Block: card" | |
| def test_friendly_label_falls_back_to_capitalized_raw(): | |
| assert analysis.friendly_label("button") == "Button" | |
| # --- _find_aoi / attribute_gaze ---------------------------------------- | |
| def _aoi(label, x, y, w, h): | |
| return {"label": label, "x": x, "y": y, "w": w, "h": h} | |
| def test_find_aoi_returns_none_when_no_match(): | |
| aois = [_aoi("header", 0, 0, 100, 50)] | |
| assert analysis._find_aoi(500, 500, aois) is None | |
| def test_find_aoi_matches_point_inside_box(): | |
| aois = [_aoi("header", 0, 0, 100, 50)] | |
| assert analysis._find_aoi(50, 25, aois) == "header" | |
| def test_find_aoi_respects_padding(): | |
| aois = [_aoi("header", 100, 100, 50, 50)] | |
| # Just outside the box but within PAD_PX (90) of it | |
| assert analysis._find_aoi(95, 125, aois) == "header" | |
| # Far outside the padded box entirely | |
| assert analysis._find_aoi(1000, 1000, aois) is None | |
| def test_find_aoi_picks_smallest_area_on_overlap(): | |
| aois = [ | |
| _aoi("big", 0, 0, 500, 500), | |
| _aoi("small", 100, 100, 20, 20), | |
| ] | |
| assert analysis._find_aoi(110, 110, aois) == "small" | |
| def test_find_aoi_skips_embed_label(): | |
| aois = [_aoi("embed", 0, 0, 1000, 1000)] | |
| assert analysis._find_aoi(500, 500, aois) is None | |
| def test_attribute_gaze_all_none_without_dom(): | |
| gaze = [{"t": 1.0, "sx": 5, "sy": 5}, {"t": 2.0, "sx": 6, "sy": 6}] | |
| result = analysis.attribute_gaze(gaze, []) | |
| assert result == [(1.0, None), (2.0, None)] | |
| def test_attribute_gaze_uses_most_recent_dom_snapshot(): | |
| gaze = [{"t": 5.0, "sx": 50, "sy": 25}] | |
| dom = [ | |
| {"t": 1.0, "aois": [_aoi("old", 0, 0, 10, 10)]}, | |
| {"t": 4.0, "aois": [_aoi("header", 0, 0, 100, 50)]}, | |
| ] | |
| result = analysis.attribute_gaze(gaze, dom) | |
| assert result == [(5.0, "header")] | |
| def test_attribute_gaze_before_first_dom_snapshot_is_none(): | |
| gaze = [{"t": 0.5, "sx": 50, "sy": 25}] | |
| dom = [{"t": 4.0, "aois": [_aoi("header", 0, 0, 100, 50)]}] | |
| result = analysis.attribute_gaze(gaze, dom) | |
| assert result == [(0.5, None)] | |
| # --- compute_dwell_ranking ----------------------------------------------- | |
| def test_compute_dwell_ranking_empty_for_fewer_than_two_points(): | |
| assert analysis.compute_dwell_ranking([]) == [] | |
| assert analysis.compute_dwell_ranking([(0.0, "header")]) == [] | |
| def test_compute_dwell_ranking_accumulates_time_per_label(): | |
| attributed = [ | |
| (0.0, "header"), (1.0, "header"), (2.0, "footer"), (3.0, "footer"), (4.0, None), | |
| ] | |
| ranking = analysis.compute_dwell_ranking(attributed) | |
| labels = {r["label"]: r for r in ranking} | |
| assert "Page header" in labels | |
| assert "Page footer" in labels | |
| assert labels["Page header"]["seconds"] == 2.0 # (1.0-0.0) + (2.0-1.0) | |
| assert labels["Page footer"]["seconds"] == 2.0 # (3.0-2.0) + (4.0-3.0) | |
| assert labels["Page header"]["hits"] == 2 | |
| def test_compute_dwell_ranking_sorted_descending_by_seconds(): | |
| attributed = [ | |
| (0.0, "footer"), (1.0, "footer"), (2.0, "header"), (2.5, None), | |
| ] | |
| ranking = analysis.compute_dwell_ranking(attributed) | |
| assert ranking[0]["label"] == "Page footer" | |
| assert ranking[0]["seconds"] >= ranking[1]["seconds"] | |
| def test_compute_dwell_ranking_pct_sums_to_roughly_100(): | |
| attributed = [(0.0, "header"), (1.0, "footer"), (2.0, None)] | |
| ranking = analysis.compute_dwell_ranking(attributed) | |
| assert abs(sum(r["pct"] for r in ranking) - 100.0) < 0.5 | |
| # --- summarize_mouse --------------------------------------------------- | |
| def test_summarize_mouse_no_file(tmp_path): | |
| summary = analysis.summarize_mouse(str(tmp_path)) | |
| assert summary["click_count"] == 0 | |
| assert summary["interests"] == [] | |
| assert summary["trail_points"] == 0 | |
| assert summary["heatmap_points"] == 0 | |
| def test_summarize_mouse_aggregates_dwell_and_clicks(tmp_path): | |
| _write_jsonl(tmp_path / "mouse_log.jsonl", [ | |
| {"type": "mouse_batch", "dwell": [{"element": "header", "duration": 1000}], | |
| "click": [{"timestamp": "2026-01-01T00:00:01"}], "trail": [1, 2], "heatmap": [1]}, | |
| {"type": "mouse_batch", "dwell": [{"element": "header", "duration": 500}], | |
| "click": [{"timestamp": "2026-01-01T00:00:00"}], "trail": [1], "heatmap": []}, | |
| ]) | |
| summary = analysis.summarize_mouse(str(tmp_path)) | |
| assert summary["click_count"] == 2 | |
| assert summary["trail_points"] == 3 | |
| assert summary["heatmap_points"] == 1 | |
| assert summary["interests"][0]["element"] == "header" | |
| assert summary["interests"][0]["seconds"] == 1.5 # (1000+500)ms -> 1.5s | |
| # clicks sorted ascending by timestamp | |
| assert summary["clicks"][0]["timestamp"] == "2026-01-01T00:00:00" | |
| def test_summarize_mouse_caps_clicks_at_fifty(tmp_path): | |
| clicks = [{"timestamp": f"2026-01-01T00:{i:02d}:00"} for i in range(60)] | |
| _write_jsonl(tmp_path / "mouse_log.jsonl", [ | |
| {"type": "mouse_batch", "dwell": [], "click": clicks, "trail": [], "heatmap": []}, | |
| ]) | |
| summary = analysis.summarize_mouse(str(tmp_path)) | |
| assert summary["click_count"] == 60 | |
| assert len(summary["clicks"]) == 50 | |