Skip to content

Commit 7202d25

Browse files
committed
test: add glossary i18n, E2E, and visual Playwright tests (#158)
Unit tests verify glossary keys present in all 4 languages. E2E tests verify popover open/close/dismiss behavior. Visual tests capture screenshots for desktop, mobile, light theme.
1 parent eb26f2e commit 7202d25

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed

tests/e2e/test_glossary.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""E2E tests for in-app glossary tooltips."""
2+
3+
import pytest
4+
from playwright.sync_api import expect
5+
6+
7+
class TestGlossaryPresence:
8+
"""Verify glossary hint icons are rendered on the dashboard."""
9+
10+
def test_snr_card_has_glossary_hint(self, demo_page):
11+
hint = demo_page.locator('#view-dashboard .metric-card .glossary-hint').first
12+
expect(hint).to_be_visible()
13+
14+
def test_glossary_hint_has_info_icon(self, demo_page):
15+
icon = demo_page.locator('#view-dashboard .glossary-hint svg').first
16+
expect(icon).to_be_visible()
17+
18+
def test_multiple_glossary_hints_on_dashboard(self, demo_page):
19+
hints = demo_page.locator('#view-dashboard .glossary-hint')
20+
assert hints.count() >= 4, f"Expected at least 4 glossary hints, got {hints.count()}"
21+
22+
23+
class TestGlossaryPopover:
24+
"""Verify popover open/close behavior."""
25+
26+
def test_popover_hidden_by_default(self, demo_page):
27+
popover = demo_page.locator('#view-dashboard .glossary-popover').first
28+
expect(popover).not_to_be_visible()
29+
30+
def test_click_opens_popover(self, demo_page):
31+
hint = demo_page.locator('#view-dashboard .glossary-hint').first
32+
hint.click()
33+
popover = hint.locator('.glossary-popover')
34+
expect(popover).to_be_visible()
35+
36+
def test_popover_has_text_content(self, demo_page):
37+
hint = demo_page.locator('#view-dashboard .glossary-hint').first
38+
hint.click()
39+
popover = hint.locator('.glossary-popover')
40+
text = popover.text_content()
41+
assert len(text) > 20, f"Popover text too short: {text}"
42+
43+
def test_click_outside_closes_popover(self, demo_page):
44+
hint = demo_page.locator('#view-dashboard .glossary-hint').first
45+
hint.click()
46+
popover = hint.locator('.glossary-popover')
47+
expect(popover).to_be_visible()
48+
demo_page.locator('body').click(position={"x": 10, "y": 10})
49+
expect(popover).not_to_be_visible()
50+
51+
def test_escape_closes_popover(self, demo_page):
52+
hint = demo_page.locator('#view-dashboard .glossary-hint').first
53+
hint.click()
54+
popover = hint.locator('.glossary-popover')
55+
expect(popover).to_be_visible()
56+
demo_page.keyboard.press("Escape")
57+
expect(popover).not_to_be_visible()
58+
59+
def test_clicking_second_hint_closes_first(self, demo_page):
60+
hints = demo_page.locator('#view-dashboard .glossary-hint')
61+
first = hints.nth(0)
62+
second = hints.nth(1)
63+
first.click()
64+
expect(first.locator('.glossary-popover')).to_be_visible()
65+
second.click()
66+
expect(first.locator('.glossary-popover')).not_to_be_visible()
67+
expect(second.locator('.glossary-popover')).to_be_visible()
68+
69+
70+
class TestGlossaryChannels:
71+
"""Verify glossary hints in channel tables."""
72+
73+
def test_ds_channel_group_has_glossary_hint(self, demo_page):
74+
demo_page.locator('a.nav-item[data-view="channels"]').click()
75+
demo_page.wait_for_timeout(500)
76+
hint = demo_page.locator('#view-channels .docsis-group-header .glossary-hint').first
77+
expect(hint).to_be_visible()
78+
79+
def test_channel_glossary_popover_works(self, demo_page):
80+
demo_page.locator('a.nav-item[data-view="channels"]').click()
81+
demo_page.wait_for_timeout(500)
82+
hint = demo_page.locator('#view-channels .docsis-group-header .glossary-hint').first
83+
hint.click()
84+
popover = hint.locator('.glossary-popover')
85+
expect(popover).to_be_visible()
86+
text = popover.text_content()
87+
assert len(text) > 20
88+
89+
90+
class TestGlossaryModulation:
91+
"""Verify glossary hints on modulation KPI cards."""
92+
93+
def test_modulation_kpi_has_glossary_hints(self, demo_page):
94+
demo_page.locator('a.nav-item[data-view="modulation"]').click()
95+
demo_page.wait_for_timeout(2000)
96+
hints = demo_page.locator('#view-modulation .glossary-hint')
97+
assert hints.count() >= 3, f"Expected 3 modulation glossary hints, got {hints.count()}"
98+
99+
def test_health_index_popover(self, demo_page):
100+
demo_page.locator('a.nav-item[data-view="modulation"]').click()
101+
demo_page.wait_for_timeout(2000)
102+
hint = demo_page.locator('#view-modulation .glossary-hint').first
103+
hint.click()
104+
popover = hint.locator('.glossary-popover')
105+
expect(popover).to_be_visible()

tests/e2e/test_glossary_visual.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Visual QA tests for in-app glossary feature.
2+
3+
Screenshots saved to tests/e2e/screenshots/glossary/ for manual review.
4+
"""
5+
6+
import os
7+
8+
import pytest
9+
from playwright.sync_api import expect
10+
11+
12+
SCREENSHOT_DIR = os.path.join(os.path.dirname(__file__), "screenshots", "glossary")
13+
14+
15+
@pytest.fixture(autouse=True, scope="module")
16+
def ensure_screenshot_dir():
17+
"""Create screenshot output directory."""
18+
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
19+
20+
21+
class TestGlossaryVisualDesktop:
22+
"""Desktop screenshots of glossary popovers."""
23+
24+
def test_screenshot_dashboard_with_hints(self, demo_page):
25+
demo_page.screenshot(
26+
path=os.path.join(SCREENSHOT_DIR, "dashboard_hints.png"),
27+
full_page=False,
28+
)
29+
hints = demo_page.locator('#view-dashboard .glossary-hint')
30+
assert hints.count() >= 4
31+
32+
def test_screenshot_popover_open(self, demo_page):
33+
hint = demo_page.locator('#view-dashboard .glossary-hint').first
34+
hint.click()
35+
demo_page.wait_for_timeout(300)
36+
expect(hint.locator('.glossary-popover')).to_be_visible()
37+
demo_page.screenshot(
38+
path=os.path.join(SCREENSHOT_DIR, "popover_open.png"),
39+
full_page=False,
40+
)
41+
42+
def test_screenshot_channel_popover(self, demo_page):
43+
demo_page.locator('a.nav-item[data-view="channels"]').click()
44+
demo_page.wait_for_timeout(500)
45+
hint = demo_page.locator('#view-channels .docsis-group-header .glossary-hint').first
46+
hint.click()
47+
demo_page.wait_for_timeout(300)
48+
demo_page.screenshot(
49+
path=os.path.join(SCREENSHOT_DIR, "channel_popover.png"),
50+
full_page=False,
51+
)
52+
53+
def test_screenshot_modulation_popovers(self, demo_page):
54+
demo_page.locator('a.nav-item[data-view="modulation"]').click()
55+
demo_page.wait_for_timeout(2000)
56+
hint = demo_page.locator('#view-modulation .glossary-hint').first
57+
hint.click()
58+
demo_page.wait_for_timeout(300)
59+
demo_page.screenshot(
60+
path=os.path.join(SCREENSHOT_DIR, "modulation_popover.png"),
61+
full_page=False,
62+
)
63+
64+
65+
class TestGlossaryVisualMobile:
66+
"""Mobile viewport screenshots of glossary popovers."""
67+
68+
@pytest.fixture()
69+
def mobile_page(self, page, live_server):
70+
page.set_viewport_size({"width": 375, "height": 812})
71+
page.goto(live_server)
72+
page.wait_for_load_state("networkidle")
73+
return page
74+
75+
def test_screenshot_mobile_dashboard(self, mobile_page):
76+
mobile_page.screenshot(
77+
path=os.path.join(SCREENSHOT_DIR, "mobile_dashboard.png"),
78+
full_page=False,
79+
)
80+
81+
def test_screenshot_mobile_popover(self, mobile_page):
82+
hint = mobile_page.locator('#view-dashboard .glossary-hint').first
83+
hint.click()
84+
mobile_page.wait_for_timeout(300)
85+
mobile_page.screenshot(
86+
path=os.path.join(SCREENSHOT_DIR, "mobile_popover.png"),
87+
full_page=False,
88+
)
89+
90+
91+
class TestGlossaryVisualLightTheme:
92+
"""Light theme screenshots."""
93+
94+
@pytest.fixture()
95+
def light_page(self, page, live_server):
96+
page.goto(live_server)
97+
page.wait_for_load_state("networkidle")
98+
page.evaluate("document.documentElement.setAttribute('data-theme', 'light')")
99+
page.wait_for_timeout(300)
100+
return page
101+
102+
def test_screenshot_light_popover(self, light_page):
103+
hint = light_page.locator('#view-dashboard .glossary-hint').first
104+
hint.click()
105+
light_page.wait_for_timeout(300)
106+
light_page.screenshot(
107+
path=os.path.join(SCREENSHOT_DIR, "light_popover.png"),
108+
full_page=False,
109+
)

tests/test_glossary_i18n.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Tests for glossary i18n key completeness."""
2+
3+
import json
4+
import os
5+
6+
import pytest
7+
8+
I18N_DIR = os.path.join(os.path.dirname(__file__), "..", "app", "i18n")
9+
MOD_I18N_DIR = os.path.join(
10+
os.path.dirname(__file__), "..", "app", "modules", "modulation", "i18n"
11+
)
12+
13+
CORE_GLOSSARY_KEYS = [
14+
"glossary_snr",
15+
"glossary_power",
16+
"glossary_correctable",
17+
"glossary_uncorrectable",
18+
"glossary_scqam",
19+
"glossary_ofdm",
20+
"glossary_modulation",
21+
"glossary_docsis",
22+
"glossary_gaming_index",
23+
]
24+
25+
MOD_GLOSSARY_KEYS = [
26+
"glossary_health_index",
27+
"glossary_low_qam",
28+
"glossary_sample_density",
29+
]
30+
31+
LANGUAGES = ["en", "de", "fr", "es"]
32+
33+
34+
@pytest.mark.parametrize("lang", LANGUAGES)
35+
def test_core_glossary_keys_present(lang):
36+
path = os.path.join(I18N_DIR, f"{lang}.json")
37+
with open(path, encoding="utf-8") as f:
38+
data = json.load(f)
39+
for key in CORE_GLOSSARY_KEYS:
40+
assert key in data, f"Missing {key} in {lang}.json"
41+
assert len(data[key]) > 10, f"Empty/too-short value for {key} in {lang}.json"
42+
43+
44+
@pytest.mark.parametrize("lang", LANGUAGES)
45+
def test_modulation_glossary_keys_present(lang):
46+
path = os.path.join(MOD_I18N_DIR, f"{lang}.json")
47+
with open(path, encoding="utf-8") as f:
48+
data = json.load(f)
49+
for key in MOD_GLOSSARY_KEYS:
50+
assert key in data, f"Missing {key} in modulation/{lang}.json"
51+
assert len(data[key]) > 10, f"Empty/too-short value for {key} in modulation/{lang}.json"

0 commit comments

Comments
 (0)