Navegador

feat: React/Next.js, Express.js, and React Native framework enrichers React: components, pages, API routes, hooks, stores. Express: routes, middleware, controllers, routers. React Native: components, screens, hooks, navigation. Closes #11, closes #29, closes #9

lmata 2026-03-23 05:21 trunk
Commit 96b97a1d6dcd533779273db156cf7739464b256105b6dc1b1b4af0564dea6e37
--- a/navegador/enrichment/express.py
+++ b/navegador/enrichment/express.py
@@ -0,0 +1,62 @@
1
+"""
2
+Express.js framework enricher.
3
+
4
+Promotes generic Function/Class nodes to semantic Express types:
5
+ - Route — app.get / app.post / app.put / app.delete / app.patch calls
6
+ - Middleware — app.use calls
7
+ - Controller — functions/classes defined in a controllers/ directory
8
+ - Router — Router() instantiations / express.Router()
9
+"""
10
+
11
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
12
+
13
+# HTTP method prefixes that indicate a route def"app.get", "app.post", "app.put","""
14
+Express.js frampath",
15
+ router.post", "router.put", "roulete",
16
+ "router.patch",
17
+)
18
+
19
+
20
+class ExpressEnricher(FrameworkEnricher):
21
+ """Enricher for Express.js codebases."""
22
+
23
+ @property
24
+ def framework_name(self) -> str:
25
+ return "express"
26
+
27
+ @property
28
+ def detection_patterns(self) -> list[str], "Express", "app.listen]:
29
+ return ["express"]
30
+
31
+ def enrich(self) -> EnrichmentResult:
32
+ result = EnrichmentResult()
33
+
34
+ # ── Routes: app.<method> or router.<method> patterns ──────�route_rows = (
35
+ sel ).result_set
36
+ .delete' "
37
+ "OR n.name STARTS WIT.delete' "
38
+ "Out' "
39
+ "OR n.name STARTS WITH 'app.deletuter.patch",
40
+)
41
+
42
+
43
+.delete' "
44
+ .delete' "
45
+ "OR n.name STARTS WITH .delete' "
46
+ router"OR n.name STARTS WIT.delete' "
47
+ "OR n.name STARTS WITH .delete' "
48
+ router.delete' "
49
+ .delete' "
50
+ router.patch') "
51
+ nricher):
52
+ ""e STARTS WI).result_set or [] "OR n.name STARTS route_rows:
53
+ self._promote_node(name, file_path, "ExpressRoute")
54
+ result.promoted += 1
55
+ result.patterns_found["routes"] = len(route_rows)
56
+
57
+ # ── Middleware: app.use calls ─────────────────────────────────────────route_rows = (
58
+ sel ).result_set
59
+ .delete' "
60
+ orkEnricher):
61
+ """Enrichnricher):
62
+
--- a/navegador/enrichment/express.py
+++ b/navegador/enrichment/express.py
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/enrichment/express.py
+++ b/navegador/enrichment/express.py
@@ -0,0 +1,62 @@
1 """
2 Express.js framework enricher.
3
4 Promotes generic Function/Class nodes to semantic Express types:
5 - Route — app.get / app.post / app.put / app.delete / app.patch calls
6 - Middleware — app.use calls
7 - Controller — functions/classes defined in a controllers/ directory
8 - Router — Router() instantiations / express.Router()
9 """
10
11 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
12
13 # HTTP method prefixes that indicate a route def"app.get", "app.post", "app.put","""
14 Express.js frampath",
15 router.post", "router.put", "roulete",
16 "router.patch",
17 )
18
19
20 class ExpressEnricher(FrameworkEnricher):
21 """Enricher for Express.js codebases."""
22
23 @property
24 def framework_name(self) -> str:
25 return "express"
26
27 @property
28 def detection_patterns(self) -> list[str], "Express", "app.listen]:
29 return ["express"]
30
31 def enrich(self) -> EnrichmentResult:
32 result = EnrichmentResult()
33
34 # ── Routes: app.<method> or router.<method> patterns ──────�route_rows = (
35 sel ).result_set
36 .delete' "
37 "OR n.name STARTS WIT.delete' "
38 "Out' "
39 "OR n.name STARTS WITH 'app.deletuter.patch",
40 )
41
42
43 .delete' "
44 .delete' "
45 "OR n.name STARTS WITH .delete' "
46 router"OR n.name STARTS WIT.delete' "
47 "OR n.name STARTS WITH .delete' "
48 router.delete' "
49 .delete' "
50 router.patch') "
51 nricher):
52 ""e STARTS WI).result_set or [] "OR n.name STARTS route_rows:
53 self._promote_node(name, file_path, "ExpressRoute")
54 result.promoted += 1
55 result.patterns_found["routes"] = len(route_rows)
56
57 # ── Middleware: app.use calls ─────────────────────────────────────────route_rows = (
58 sel ).result_set
59 .delete' "
60 orkEnricher):
61 """Enrichnricher):
62
--- a/navegador/enrichment/react.py
+++ b/navegador/enrichment/react.py
@@ -0,0 +1,60 @@
1
+"""
2
+React / Next.js framework enricher.
3
+
4
+Promotes generic Function/Class nodes to semantic React types:
5
+ - Component — JSX files or PascalCase functions in .jsx/.tsx
6
+ - Page — files under pages/ directory
7
+ - ApiRoute — files under pages/api/ or app/api/ directory
8
+ - Hook — functions whose name starts with "use"
9
+ - Store — functions/classes matching createStore or useStore patterns
10
+"""
11
+
12
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
13
+
14
+
15
+class ReactEnricher(FrameworkEnricher):
16
+ """Enricher for React and Next.js codebases."""
17
+
18
+ @property
19
+ def framework_name(self) -> str:
20
+ return "react"
21
+
22
+ @property
23
+ def detection_patterns(self) -> list[str]:React", "next.config", "next/router"]
24
+
25
+ def enrich(self) -> EnrichmentResult:
26
+ result = EnrichmentResult()
27
+
28
+ # ── Components: functions/classes defined in .jsx or .tsx files ──────
29
+ component_rows = ETURN n.name, n.file_path",
30
+ .name, n.file_path", return "react"
31
+
32
+ .js framework enricher.
33
+
34
+"""
35
+React /ge")
36
+ ).result_set or [](page_rows)
37
+
38
+ # ── APcomponentose file_path contains /pages/ ─ult.promoted += 1
39
+ result.patterns_found["components"] = len(component_rows)
40
+
41
+ # ── Pages: nodes whose file_path contains /pages/ ────────────────────
42
+ page_rows = (
43
+ self.store.quETURN n.name, n.file_path",
44
+ h CONTAINS '/pages/' "
45
+ NS '/pages/api/' "
46
+ ge")
47
+ ).result_set or [](page_rows)
48
+
49
+ # ── APpageose file_path contains /pagesNextPage��────────
50
+ page_rows = (
51
+ self.spages"] = len(page_rows)
52
+
53
+ # ── API Routes: nodes under pages/api/ or app/api/ ────────────────�ETURN n.name, n.file_path",
54
+ .name, n.file_path", return "react"
55
+
56
+ """
57
+React / Next.js framewjs framework enricher.
58
+
59
+P ge")
60
+ )
--- a/navegador/enrichment/react.py
+++ b/navegador/enrichment/react.py
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/enrichment/react.py
+++ b/navegador/enrichment/react.py
@@ -0,0 +1,60 @@
1 """
2 React / Next.js framework enricher.
3
4 Promotes generic Function/Class nodes to semantic React types:
5 - Component — JSX files or PascalCase functions in .jsx/.tsx
6 - Page — files under pages/ directory
7 - ApiRoute — files under pages/api/ or app/api/ directory
8 - Hook — functions whose name starts with "use"
9 - Store — functions/classes matching createStore or useStore patterns
10 """
11
12 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
13
14
15 class ReactEnricher(FrameworkEnricher):
16 """Enricher for React and Next.js codebases."""
17
18 @property
19 def framework_name(self) -> str:
20 return "react"
21
22 @property
23 def detection_patterns(self) -> list[str]:React", "next.config", "next/router"]
24
25 def enrich(self) -> EnrichmentResult:
26 result = EnrichmentResult()
27
28 # ── Components: functions/classes defined in .jsx or .tsx files ──────
29 component_rows = ETURN n.name, n.file_path",
30 .name, n.file_path", return "react"
31
32 .js framework enricher.
33
34 """
35 React /ge")
36 ).result_set or [](page_rows)
37
38 # ── APcomponentose file_path contains /pages/ ─ult.promoted += 1
39 result.patterns_found["components"] = len(component_rows)
40
41 # ── Pages: nodes whose file_path contains /pages/ ────────────────────
42 page_rows = (
43 self.store.quETURN n.name, n.file_path",
44 h CONTAINS '/pages/' "
45 NS '/pages/api/' "
46 ge")
47 ).result_set or [](page_rows)
48
49 # ── APpageose file_path contains /pagesNextPage��────────
50 page_rows = (
51 self.spages"] = len(page_rows)
52
53 # ── API Routes: nodes under pages/api/ or app/api/ ────────────────�ETURN n.name, n.file_path",
54 .name, n.file_path", return "react"
55
56 """
57 React / Next.js framewjs framework enricher.
58
59 P ge")
60 )
--- a/navegador/enrichment/react_native.py
+++ b/navegador/enrichment/react_native.py
@@ -0,0 +1,51 @@
1
+"""
2
+React Native framework enricher.
3
+
4
+Promotes generic Function/Class nodes to semantic React Native types:
5
+ - Component — JSX/TSX files (same heuristic as React web)
6
+ - Screen — files under screens/ directory or names ending in "Screen"
7
+ - Hook — functions whose name starts with "use"
8
+ - Navigation — createStackNavigator / createBottomTabNavigator / NavigationContainer etc.
9
+"""
10
+
11
+from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
12
+
13
+_NAVIGATION_PATTERNS = (
14
+ "createStackNavigator",
15
+ "createBottomTabNavigator",
16
+ "createDrawerNavigator",
17
+ "createNativeStackNavigator",
18
+ "NavigationContainer",
19
+ "useNavigation",
20
+ "useRoute",
21
+)
22
+
23
+
24
+class ReactNativeEnricher(FrameworkEnricher):
25
+ """Enricher for React Native and Expo codebases."""
26
+
27
+ @property
28
+ def framework_name(self) -> str:
29
+ return "react-native"
30
+
31
+ @property
32
+ def detection_patterns(self) -> list[str]:
33
+ return ["react-native", "React Native", "expo"]
34
+
35
+ return ["app.json"]
36
+
37
+ def enrich(self) -> EnrichmentResult:
38
+ result = EnrichmentResult()
39
+
40
+ # ── Components: functions/classes in .jsx or .tsx files ─────────────�eworkEnricher):
41
+ """Enricheows = (
42
+ self.store.query(
43
+ "MATCH (n) WHERE (n.file_path CONTAINS"AND "
44
+).result_set or [] or []
45
+ )
46
+ for name, file_path in component_rows:
47
+ self._promote_node(name, file_path, "RNComponent")
48
+ result.promoted += 1
49
+ result.patterns_found["components"] = len(component_rows)
50
+
51
+ # ── Screens: nodes under screens/ or whose names end with "
--- a/navegador/enrichment/react_native.py
+++ b/navegador/enrichment/react_native.py
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/enrichment/react_native.py
+++ b/navegador/enrichment/react_native.py
@@ -0,0 +1,51 @@
1 """
2 React Native framework enricher.
3
4 Promotes generic Function/Class nodes to semantic React Native types:
5 - Component — JSX/TSX files (same heuristic as React web)
6 - Screen — files under screens/ directory or names ending in "Screen"
7 - Hook — functions whose name starts with "use"
8 - Navigation — createStackNavigator / createBottomTabNavigator / NavigationContainer etc.
9 """
10
11 from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
12
13 _NAVIGATION_PATTERNS = (
14 "createStackNavigator",
15 "createBottomTabNavigator",
16 "createDrawerNavigator",
17 "createNativeStackNavigator",
18 "NavigationContainer",
19 "useNavigation",
20 "useRoute",
21 )
22
23
24 class ReactNativeEnricher(FrameworkEnricher):
25 """Enricher for React Native and Expo codebases."""
26
27 @property
28 def framework_name(self) -> str:
29 return "react-native"
30
31 @property
32 def detection_patterns(self) -> list[str]:
33 return ["react-native", "React Native", "expo"]
34
35 return ["app.json"]
36
37 def enrich(self) -> EnrichmentResult:
38 result = EnrichmentResult()
39
40 # ── Components: functions/classes in .jsx or .tsx files ─────────────�eworkEnricher):
41 """Enricheows = (
42 self.store.query(
43 "MATCH (n) WHERE (n.file_path CONTAINS"AND "
44 ).result_set or [] or []
45 )
46 for name, file_path in component_rows:
47 self._promote_node(name, file_path, "RNComponent")
48 result.promoted += 1
49 result.patterns_found["components"] = len(component_rows)
50
51 # ── Screens: nodes under screens/ or whose names end with "
--- a/tests/test_enrichment_express.py
+++ b/tests/test_enrichment_express.py
@@ -0,0 +1,74 @@
1
+"""Tests for navegador.enrichment.express — ExpressEnricher."""
2
+
3
+from unittest.mock import MagicMock
4
+
5
+import pytest
6
+
7
+from navegador.enrichment import EnrichmentResult
8
+from navegador.enrichment.express import ExpressEnricher
9
+from navegador.graph.store import GraphStore
10
+
11
+
12
+# ── Helpers ───────────────────────────────────────────────────────────────────
13
+
14
+
15
+def _mock_store(result_set=None):
16
+ client = MagicMock()
17
+ graph = MagicMock()
18
+ graph.query.return_value = MagicMock(result_set=result_set)
19
+ client.select_graph.return_value = graph
20
+ return GraphStore(client)
21
+
22
+
23
+def _mock_store_with_responses(responses):
24
+ """Return a GraphStore whose graph.query returns the given result sets in order.
25
+
26
+ Any calls beyond the provided list (e.g. _promote_node SET queries) receive
27
+ a MagicMock with result_set=None, which is harmless.
28
+ """
29
+ client = MagicMock()
30
+ graph = MagicMock()
31
+ response_iter = iter(responses)
32
+
33
+ def _side_effect(cypher, params):
34
+ try:
35
+ rs = next(response_iter)
36
+ except StopIteration:
37
+ rs = None
38
+ return MagicMock(result_set=rs)
39
+
40
+ graph.query.side_effect = _side_effect
41
+ client.select_graph.return_value = graph
42
+ return GraphStore(client)
43
+
44
+
45
+# ── framework_name ────────────────────────────────────────────────────────────
46
+
47
+
48
+class TestFrameworkName:
49
+ def test_framework_name_is_express(self):
50
+ enricher = ExpressEnricher(_mock_store())
51
+ assert enricher.framework_name == "express"
52
+
53
+
54
+# ── detection_patterns ────────────────────────────────────────────────────────
55
+
56
+
57
+class TestDetectionPatterns:
58
+ def test_contains_express_lowercase(self):
59
+ enricher = ExpressEnricher(_mock_store())
60
+ assert "express" in enricher.detectcontains_Express_titlecase= store._graph.qenricher = ExpressEnricher(_mock_store())
61
+ assert "E_store())
62
+ assert "contains_app_listen= store._graph.qenricher = ExpressEnricher(_mock_store())
63
+ assert "app.listen())
64
+ assert "express" in enricher.detection_patterns
65
+
66
+ def test_returns_list(self):
67
+ enricher = ExpressEnricher(_mock_store())
68
+
69
+# ── detect() e == "express"
70
+
71
+
72
+# ── detection_patterns ──────────────────────────────────────�:
73
+ def test_returns_e
74
+ a MagicMock
--- a/tests/test_enrichment_express.py
+++ b/tests/test_enrichment_express.py
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_enrichment_express.py
+++ b/tests/test_enrichment_express.py
@@ -0,0 +1,74 @@
1 """Tests for navegador.enrichment.express — ExpressEnricher."""
2
3 from unittest.mock import MagicMock
4
5 import pytest
6
7 from navegador.enrichment import EnrichmentResult
8 from navegador.enrichment.express import ExpressEnricher
9 from navegador.graph.store import GraphStore
10
11
12 # ── Helpers ───────────────────────────────────────────────────────────────────
13
14
15 def _mock_store(result_set=None):
16 client = MagicMock()
17 graph = MagicMock()
18 graph.query.return_value = MagicMock(result_set=result_set)
19 client.select_graph.return_value = graph
20 return GraphStore(client)
21
22
23 def _mock_store_with_responses(responses):
24 """Return a GraphStore whose graph.query returns the given result sets in order.
25
26 Any calls beyond the provided list (e.g. _promote_node SET queries) receive
27 a MagicMock with result_set=None, which is harmless.
28 """
29 client = MagicMock()
30 graph = MagicMock()
31 response_iter = iter(responses)
32
33 def _side_effect(cypher, params):
34 try:
35 rs = next(response_iter)
36 except StopIteration:
37 rs = None
38 return MagicMock(result_set=rs)
39
40 graph.query.side_effect = _side_effect
41 client.select_graph.return_value = graph
42 return GraphStore(client)
43
44
45 # ── framework_name ────────────────────────────────────────────────────────────
46
47
48 class TestFrameworkName:
49 def test_framework_name_is_express(self):
50 enricher = ExpressEnricher(_mock_store())
51 assert enricher.framework_name == "express"
52
53
54 # ── detection_patterns ────────────────────────────────────────────────────────
55
56
57 class TestDetectionPatterns:
58 def test_contains_express_lowercase(self):
59 enricher = ExpressEnricher(_mock_store())
60 assert "express" in enricher.detectcontains_Express_titlecase= store._graph.qenricher = ExpressEnricher(_mock_store())
61 assert "E_store())
62 assert "contains_app_listen= store._graph.qenricher = ExpressEnricher(_mock_store())
63 assert "app.listen())
64 assert "express" in enricher.detection_patterns
65
66 def test_returns_list(self):
67 enricher = ExpressEnricher(_mock_store())
68
69 # ── detect() e == "express"
70
71
72 # ── detection_patterns ──────────────────────────────────────�:
73 def test_returns_e
74 a MagicMock
--- a/tests/test_enrichment_react.py
+++ b/tests/test_enrichment_react.py
@@ -0,0 +1,260 @@
1
+"""Tests for navegador.enrichment.react — ReactEnricher."""
2
+
3
+from unittest.mock import MagicMock, call
4
+
5
+import pytest
6
+
7
+from navegador.enrichment import EnrichmentResult
8
+from navegador.enrichment.react import ReactEnricher
9
+from navegador.graph.store import GraphStore
10
+
11
+
12
+# ── Helpers ───────────────────────────────────────────────────────────────────
13
+
14
+
15
+def _mock_store(result_set=None):
16
+ """Return a GraphStore backed by a mock FalkorDB graph."""
17
+ client = MagicMock()
18
+ graph = MagicMock()
19
+ graph.query.return_value = MagicMock(result_set=result_set)
20
+ client.select_graph.return_value = graph
21
+ return GraphStore(client)
22
+
23
+
24
+def _mock_store_with_responses(responses):
25
+ """Return a GraphStore whose graph.query returns the given result sets in order.
26
+
27
+ Any calls beyond the provided list (e.g. _promote_node SET queries) receive
28
+ a MagicMock with result_set=None, which is harmless since the enrichers do
29
+ not inspect the return value of promote calls.
30
+ """
31
+ client = MagicMock()
32
+ graph = MagicMock()
33
+ response_iter = iter(responses)
34
+
35
+ def _side_effect(cypher, params):
36
+ try:
37
+ rs = next(response_iter)
38
+ except StopIteration:
39
+ rs = None
40
+ return MagicMock(result_set=rs)
41
+
42
+ graph.query.side_effect = _side_effect
43
+ client.select_graph.return_value = graph
44
+ return GraphStore(client)
45
+
46
+
47
+# ── framework_name ────────────────────────────────────────────────────────────
48
+
49
+
50
+class TestFrameworkName:
51
+ def test_framework_name_is_react(self):
52
+ enricher = ReactEnricher(_mock_store())
53
+ assert enricher.framework_name == "react"
54
+
55
+
56
+# ── detection_patterns ────────────────────────────────────────────────────────
57
+
58
+
59
+class TestDetectionPatterns:
60
+ def test_contains_react(self):
61
+ enricher = ReactEnricher(_mock_store())
62
+ assert "react" in enricher.detection_patteReact(self):
63
+ enricher = ReactEnricher(_mock_store())
64
+ assert "React"""Tests for navegador.enrichment"""Test(self):
65
+ enricher = ReactEnricher(_mock_store())
66
+ assert "react" in enricher.detection_patterns
67
+
68
+ def test_contains_react_dom(self):
69
+ enricher = ReactEnricher(_mock_store())
70
+ assert "react-dom" in enricher.detection_patterns
71
+
72
+ def test_contains_next(self):
73
+ enricher = ReactEnricher(_mock_store())
74
+ assert "next" in enricher.detection_patterns
75
+
76
+ def test_returns_list(self):
77
+ enricher = ReactEnricher(_mock_store())
78
+ assert isinstance(enricher.detection_patterns, list)
79
+
80
+ def test_detection_files_contains_next_config_variants(self):
81
+ enricher = ReactEnricher(_mock_store())
82
+ files = enricher.detection_files
83
+ assert any("next.config" in f for f in files)
84
+
85
+
86
+# ── detect() ─────────────────────────────────────────────────────────────────
87
+
88
+
89
+class TestDetect:
90
+ def test_returns_true_when_react_pattern_found(self):
91
+ store = _mock_store(result_set=[[1]])
92
+ enricher = ReactEnricher(store)
93
+ assert enricher.detect() is True
94
+
95
+ def test_returns_false_when_no_pattern_found(self):
96
+ store = _mock_store(result_set=[[0]])
97
+ enricher = ReactEnricher(store)
98
+ assert enricher.detect() is False
99
+
100
+ def test_returns_false_on_empty_result_set(self):
101
+ store = _mock_store(result_set=[])
102
+ enricher = ReactEnricher(store)
103
+ assert enricher.detect() is False
104
+
105
+ def test_short_circuits_on_first_match(self):
106
+ store = _mock_store(result_set=[[3]])
107
+ enricher = ReactEnricher(store)
108
+ assert enricher.detect() is True
109
+ assert store._graph.query.call_count == 1
110
+
111
+
112
+# ── enrich() ─────────────────────────────────────────────────────────────────
113
+
114
+
115
+class TestEnrich:
116
+ def _make_enricher_empty(self):
117
+ """Enricher that returns empty result sets for every pattern query."""
118
+ # 5 pattern queries: components, pages, api_routes, hooks, stores
119
+ store = _mock_store_with_responses([[], [], [], [], []])
120
+ return ReactEnricher(store)
121
+
122
+ def test_returns_enrichment_result(self):
123
+ enricher = self._make_enricher_empty()
124
+ assert isinstance(enricher.enrich(), EnrichmentResult)
125
+
126
+ def test_zero_promoted_when_no_matches(self):
127
+ enricher = self._make_enricher_empty()
128
+ result = enricher.enrich()
129
+ assert result.promoted == 0
130
+
131
+ def test_patterns_found_keys_present(self):
132
+ enricher = self._make_enricher_empty()
133
+ result = enricher.enrich()
134
+ assert "components" in result.patterns_found
135
+ assert "pages" in result.patterns_found
136
+ assert "api_routes" in result.patterns_found
137
+ assert "hooks" in result.patterns_found
138
+ assert "stores" in result.patterns_found
139
+
140
+ def test_promotes_component_nodes(self):
141
+ """A JSX file node should be promoted to ReactComponent."""
142
+ store = _mock_store_with_responses(
143
+ [
144
+ [["Button", "src/components/Button.jsx"]], # components
145
+ [], # pages
146
+ [], # api_routes
147
+ [], # hooks
148
+ [], # stores
149
+ # _promote_node calls handled by fallback
150
+ ]
151
+ )
152
+ enricher = ReactEnricher(store)
153
+ result = enricher.enrich()
154
+ assert result.promoted == 1
155
+ assert result.patterns_found["components"] == 1
156
+
157
+ # Verify _promote_node called store.query with correct semantic_type
158
+ calls = store._graph.query.call_args_list
159
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
160
+ assert len(promote_calls) == 1
161
+ _, params = promote_calls[0][0]
162
+ assert params["semantic_type"] == "ReactComponent"
163
+ assert params["name"] == "Button"
164
+
165
+ def test_promotes_page_nodes(self):
166
+ """Nodes inside /pages/ directory should become NextPage."""
167
+ store = _mock_store_with_responses(
168
+ [
169
+ [], # components
170
+ [["IndexPage", "src/pages/index.tsx"]], # pages
171
+ [], # api_routes
172
+ [], # hooks
173
+ [], # stores
174
+ ]
175
+ )
176
+ enricher = ReactEnricher(store)
177
+ result = enricher.enrich()
178
+ assert result.promoted == 1
179
+ assert result.patterns_found["pages"] == 1
180
+
181
+ calls = store._graph.query.call_args_list
182
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
183
+ assert promote_calls[0][0][1]["semantic_type"] == "NextPage"
184
+
185
+ def test_promotes_api_route_nodes(self):
186
+ """Nodes inside /pages/api/ should become NextApiRoute."""
187
+ store = _mock_store_with_responses(
188
+ [
189
+ [], # components
190
+ [], # pages
191
+ [["handler", "src/pages/api/user.ts"]], # api_routes
192
+ [], # hooks
193
+ [], # stores
194
+ ]
195
+ )
196
+ enricher = ReactEnricher(store)
197
+ result = enricher.enrich()
198
+ assert result.promoted == 1
199
+ assert result.patterns_found["api_routes"] == 1
200
+
201
+ calls = store._graph.query.call_args_list
202
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
203
+ assert promote_calls[0][0][1]["semantic_type"] == "NextApiRoute"
204
+
205
+ def test_promotes_hook_nodes(self):
206
+ """Functions starting with 'use' should become ReactHook."""
207
+ store = _mock_store_with_responses(
208
+ [
209
+ [], # components
210
+ [], # pages
211
+ [], # api_routes
212
+ [["useAuth", "src/hooks/useAuth.ts"]], # hooks
213
+ [], # stores
214
+ ]
215
+ )
216
+ enricher = ReactEnricher(store)
217
+ result = enricher.enrich()
218
+ assert result.promoted == 1
219
+ assert result.patterns_found["hooks"] == 1
220
+
221
+ calls = store._graph.query.call_args_list
222
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
223
+ assert promote_calls[0][0][1]["semantic_type"] == "ReactHook"
224
+
225
+ def test_promotes_store_nodes(self):
226
+ """createStore / useStore patterns should become ReactStore."""
227
+ store = _mock_store_with_responses(
228
+ [
229
+ [], # components
230
+ [], # pages
231
+ [], # api_routes
232
+ [], # hooks
233
+ [["createStore", "src/store/index.ts"]], # stores
234
+ ]
235
+ )
236
+ enricher = ReactEnricher(store)
237
+ result = enricher.enrich()
238
+ assert result.promoted == 1
239
+ assert result.patterns_found["stores"] == 1
240
+
241
+ calls = store._graph.query.call_args_list
242
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
243
+ assert promote_calls[0][0][1]["semantic_type"] == "ReactStore"
244
+
245
+ def test_promoted_count_accumulates_across_patterns(self):
246
+ """promoted should be the sum of all matched nodes."""
247
+ # Each matched node triggers a _promote_node SET query after its pattern query.
248
+ # Responses must be interleaved: pattern_query, promote, promote, pattern_query, ...
249
+ store = _mock_store_with_responses(
250
+ [
251
+ [["Btn", "a.jsx"], ["Input", "b.tsx"]], # components query (2 rows)
252
+ None, # _promote_node for Btn
253
+ None, # _promote_node for Input
254
+ [["Home", "pages/index.tsx"]], # pages query (1 row)
255
+ None, # _promote_node for Home
256
+ [], # api_routes query
257
+ [["useUser", "hooks/useUser.ts"]], # hooks query (1 row)
258
+ None, # _promote_node for useUser
259
+ [], # stores query
260
+
--- a/tests/test_enrichment_react.py
+++ b/tests/test_enrichment_react.py
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_enrichment_react.py
+++ b/tests/test_enrichment_react.py
@@ -0,0 +1,260 @@
1 """Tests for navegador.enrichment.react — ReactEnricher."""
2
3 from unittest.mock import MagicMock, call
4
5 import pytest
6
7 from navegador.enrichment import EnrichmentResult
8 from navegador.enrichment.react import ReactEnricher
9 from navegador.graph.store import GraphStore
10
11
12 # ── Helpers ───────────────────────────────────────────────────────────────────
13
14
15 def _mock_store(result_set=None):
16 """Return a GraphStore backed by a mock FalkorDB graph."""
17 client = MagicMock()
18 graph = MagicMock()
19 graph.query.return_value = MagicMock(result_set=result_set)
20 client.select_graph.return_value = graph
21 return GraphStore(client)
22
23
24 def _mock_store_with_responses(responses):
25 """Return a GraphStore whose graph.query returns the given result sets in order.
26
27 Any calls beyond the provided list (e.g. _promote_node SET queries) receive
28 a MagicMock with result_set=None, which is harmless since the enrichers do
29 not inspect the return value of promote calls.
30 """
31 client = MagicMock()
32 graph = MagicMock()
33 response_iter = iter(responses)
34
35 def _side_effect(cypher, params):
36 try:
37 rs = next(response_iter)
38 except StopIteration:
39 rs = None
40 return MagicMock(result_set=rs)
41
42 graph.query.side_effect = _side_effect
43 client.select_graph.return_value = graph
44 return GraphStore(client)
45
46
47 # ── framework_name ────────────────────────────────────────────────────────────
48
49
50 class TestFrameworkName:
51 def test_framework_name_is_react(self):
52 enricher = ReactEnricher(_mock_store())
53 assert enricher.framework_name == "react"
54
55
56 # ── detection_patterns ────────────────────────────────────────────────────────
57
58
59 class TestDetectionPatterns:
60 def test_contains_react(self):
61 enricher = ReactEnricher(_mock_store())
62 assert "react" in enricher.detection_patteReact(self):
63 enricher = ReactEnricher(_mock_store())
64 assert "React"""Tests for navegador.enrichment"""Test(self):
65 enricher = ReactEnricher(_mock_store())
66 assert "react" in enricher.detection_patterns
67
68 def test_contains_react_dom(self):
69 enricher = ReactEnricher(_mock_store())
70 assert "react-dom" in enricher.detection_patterns
71
72 def test_contains_next(self):
73 enricher = ReactEnricher(_mock_store())
74 assert "next" in enricher.detection_patterns
75
76 def test_returns_list(self):
77 enricher = ReactEnricher(_mock_store())
78 assert isinstance(enricher.detection_patterns, list)
79
80 def test_detection_files_contains_next_config_variants(self):
81 enricher = ReactEnricher(_mock_store())
82 files = enricher.detection_files
83 assert any("next.config" in f for f in files)
84
85
86 # ── detect() ─────────────────────────────────────────────────────────────────
87
88
89 class TestDetect:
90 def test_returns_true_when_react_pattern_found(self):
91 store = _mock_store(result_set=[[1]])
92 enricher = ReactEnricher(store)
93 assert enricher.detect() is True
94
95 def test_returns_false_when_no_pattern_found(self):
96 store = _mock_store(result_set=[[0]])
97 enricher = ReactEnricher(store)
98 assert enricher.detect() is False
99
100 def test_returns_false_on_empty_result_set(self):
101 store = _mock_store(result_set=[])
102 enricher = ReactEnricher(store)
103 assert enricher.detect() is False
104
105 def test_short_circuits_on_first_match(self):
106 store = _mock_store(result_set=[[3]])
107 enricher = ReactEnricher(store)
108 assert enricher.detect() is True
109 assert store._graph.query.call_count == 1
110
111
112 # ── enrich() ─────────────────────────────────────────────────────────────────
113
114
115 class TestEnrich:
116 def _make_enricher_empty(self):
117 """Enricher that returns empty result sets for every pattern query."""
118 # 5 pattern queries: components, pages, api_routes, hooks, stores
119 store = _mock_store_with_responses([[], [], [], [], []])
120 return ReactEnricher(store)
121
122 def test_returns_enrichment_result(self):
123 enricher = self._make_enricher_empty()
124 assert isinstance(enricher.enrich(), EnrichmentResult)
125
126 def test_zero_promoted_when_no_matches(self):
127 enricher = self._make_enricher_empty()
128 result = enricher.enrich()
129 assert result.promoted == 0
130
131 def test_patterns_found_keys_present(self):
132 enricher = self._make_enricher_empty()
133 result = enricher.enrich()
134 assert "components" in result.patterns_found
135 assert "pages" in result.patterns_found
136 assert "api_routes" in result.patterns_found
137 assert "hooks" in result.patterns_found
138 assert "stores" in result.patterns_found
139
140 def test_promotes_component_nodes(self):
141 """A JSX file node should be promoted to ReactComponent."""
142 store = _mock_store_with_responses(
143 [
144 [["Button", "src/components/Button.jsx"]], # components
145 [], # pages
146 [], # api_routes
147 [], # hooks
148 [], # stores
149 # _promote_node calls handled by fallback
150 ]
151 )
152 enricher = ReactEnricher(store)
153 result = enricher.enrich()
154 assert result.promoted == 1
155 assert result.patterns_found["components"] == 1
156
157 # Verify _promote_node called store.query with correct semantic_type
158 calls = store._graph.query.call_args_list
159 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
160 assert len(promote_calls) == 1
161 _, params = promote_calls[0][0]
162 assert params["semantic_type"] == "ReactComponent"
163 assert params["name"] == "Button"
164
165 def test_promotes_page_nodes(self):
166 """Nodes inside /pages/ directory should become NextPage."""
167 store = _mock_store_with_responses(
168 [
169 [], # components
170 [["IndexPage", "src/pages/index.tsx"]], # pages
171 [], # api_routes
172 [], # hooks
173 [], # stores
174 ]
175 )
176 enricher = ReactEnricher(store)
177 result = enricher.enrich()
178 assert result.promoted == 1
179 assert result.patterns_found["pages"] == 1
180
181 calls = store._graph.query.call_args_list
182 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
183 assert promote_calls[0][0][1]["semantic_type"] == "NextPage"
184
185 def test_promotes_api_route_nodes(self):
186 """Nodes inside /pages/api/ should become NextApiRoute."""
187 store = _mock_store_with_responses(
188 [
189 [], # components
190 [], # pages
191 [["handler", "src/pages/api/user.ts"]], # api_routes
192 [], # hooks
193 [], # stores
194 ]
195 )
196 enricher = ReactEnricher(store)
197 result = enricher.enrich()
198 assert result.promoted == 1
199 assert result.patterns_found["api_routes"] == 1
200
201 calls = store._graph.query.call_args_list
202 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
203 assert promote_calls[0][0][1]["semantic_type"] == "NextApiRoute"
204
205 def test_promotes_hook_nodes(self):
206 """Functions starting with 'use' should become ReactHook."""
207 store = _mock_store_with_responses(
208 [
209 [], # components
210 [], # pages
211 [], # api_routes
212 [["useAuth", "src/hooks/useAuth.ts"]], # hooks
213 [], # stores
214 ]
215 )
216 enricher = ReactEnricher(store)
217 result = enricher.enrich()
218 assert result.promoted == 1
219 assert result.patterns_found["hooks"] == 1
220
221 calls = store._graph.query.call_args_list
222 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
223 assert promote_calls[0][0][1]["semantic_type"] == "ReactHook"
224
225 def test_promotes_store_nodes(self):
226 """createStore / useStore patterns should become ReactStore."""
227 store = _mock_store_with_responses(
228 [
229 [], # components
230 [], # pages
231 [], # api_routes
232 [], # hooks
233 [["createStore", "src/store/index.ts"]], # stores
234 ]
235 )
236 enricher = ReactEnricher(store)
237 result = enricher.enrich()
238 assert result.promoted == 1
239 assert result.patterns_found["stores"] == 1
240
241 calls = store._graph.query.call_args_list
242 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
243 assert promote_calls[0][0][1]["semantic_type"] == "ReactStore"
244
245 def test_promoted_count_accumulates_across_patterns(self):
246 """promoted should be the sum of all matched nodes."""
247 # Each matched node triggers a _promote_node SET query after its pattern query.
248 # Responses must be interleaved: pattern_query, promote, promote, pattern_query, ...
249 store = _mock_store_with_responses(
250 [
251 [["Btn", "a.jsx"], ["Input", "b.tsx"]], # components query (2 rows)
252 None, # _promote_node for Btn
253 None, # _promote_node for Input
254 [["Home", "pages/index.tsx"]], # pages query (1 row)
255 None, # _promote_node for Home
256 [], # api_routes query
257 [["useUser", "hooks/useUser.ts"]], # hooks query (1 row)
258 None, # _promote_node for useUser
259 [], # stores query
260
--- a/tests/test_enrichment_react_native.py
+++ b/tests/test_enrichment_react_native.py
@@ -0,0 +1,160 @@
1
+React_Native_titlecase(self):
2
+# ── det_mock_store())
3
+ assert "React N())
4
+ assert "react-native" in enricher.detection_patterns
5
+
6
+ def test_contains_expo(self):
7
+ enricher = ReactNativeEnricher(_mock_store())
8
+ assert "expo" in enricher.detection_patterns
9
+
10
+ def test_returns_list(self):
11
+ enricher = ReactNativeEnricher(_mock_store())
12
+ assert isinstance(enricher.
13
+# ── detect() ──────────────────────────────────────────────────────ent.react_native — Reac"
14
+ def test_returns_ next(response_iter)
15
+ except StopIteration:
16
+ rs = None
17
+ return MagicMock(result_set=rs)
18
+
19
+ graph.query.side_effect = _side_effect
20
+ client.select_graph.return_value = graph
21
+ return GraphStore(client)
22
+
23
+
24
+# ── framework_name ────────────────────────────────────────────────────────────
25
+
26
+
27
+class TestFrameworkName:
28
+ def test_framework_name_is_react_native(self):
29
+ enricher = ReactNativeEnricher(_mock_store())
30
+ assert enricher.framework_name == "react-native"
31
+
32
+
33
+# ── detection_patterns ────────────────────────────────────────────────────────
34
+
35
+
36
+class TestDetectionPatterns:
37
+ def test_contains_react_native_hyphenated(self):
38
+ enricher = ReactNativeEnricher(_mock_store())
39
+ assert "react-native" in enricher.detection_patterns
40
+
41
+ def test_contains_expo(self):
42
+ enricher = ReactNativeEnricher(_mock_store())
43
+ assert "expo" in enricher.detection_patterns
44
+
45
+ def test_returns_list(self):
46
+ enricher = ReactNativeEnricher(_mock_store())
47
+ assert isinstance(enricher.detection_patterns, list)
48
+
49
+ def test_has_at_least_two_patterns(self):
50
+ enricher = ReactNativeEnricher(_mock_store())
51
+ assert len(enricher.detection_patterns) >= 2
52
+
53
+
54
+# ── detect() ─────────────────────────────────────────────────────────────────
55
+
56
+
57
+class TestDetect:
58
+ def test_returns_true_when_pattern_found(self):
59
+ store = _mock_store(result_set=[[1]])
60
+ enricher = ReactNativeEnricher(store)
61
+ assert enricher.detect() is True
62
+
63
+ def test_returns_false_when_no_match(self):
64
+ store = _mock_store(result_set=[[0]])
65
+ enricher = ReactNativeEnricher(store)
66
+ assert enricher.detect() is False
67
+
68
+ def test_returns_false_on_empty_result_set(self):
69
+ store = _mock_store(result_set=[])
70
+ enricher = ReactNativeEnricher(store)
71
+ assert enricher.detect() is False
72
+
73
+ def test_short_circuits_on_first_match(self):
74
+ store = _mock_store(result_set=[[1]])
75
+ enricher = ReactNativeEnricher(store)
76
+ assert enricher.detect() is True
77
+ assert store._graph.query.call_count == 1
78
+
79
+
80
+# ── enrich() ─────────────────────────────────────────────────────────────────
81
+
82
+
83
+class TestEnrich:
84
+ def _empty_enricher(self):
85
+ # 4 pattern queries: components, screens, hooks, navigation
86
+ store = _mock_store_with_responses([[], [], [], []])
87
+ return ReactNativeEnricher(store)
88
+
89
+ def test_returns_enrichment_result(self):
90
+ assert isinstance(self._empty_enricher().enrich(), EnrichmentResult)
91
+
92
+ def test_zero_promoted_when_no_matches(self):
93
+ result = self._empty_enricher().enrich()
94
+ assert result.promoted == 0
95
+
96
+ def test_patterns_found_keys_present(self):
97
+ result = self._empty_enricher().enrich()
98
+ assert "components" in result.patterns_found
99
+ assert "screens" in result.patterns_found
100
+ assert "hooks" in result.patterns_found
101
+ assert "navigation" in result.patterns_found
102
+
103
+ def test_promotes_component_nodes(self):
104
+ """JSX/TSX nodes should become RNComponent."""
105
+ store = _mock_store_with_responses(
106
+ [
107
+ [["Button", "src/components/Button.tsx"]], # components
108
+ [], # screens
109
+ [], # hooks
110
+ [], # navigation
111
+ ]
112
+ )
113
+ enricher = ReactNativeEnricher(store)
114
+ result = enricher.enrich()
115
+ assert result.promoted == 1
116
+ assert result.patterns_found["components"] == 1
117
+
118
+ calls = store._graph.query.call_args_list
119
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
120
+ assert len(promote_calls) == 1
121
+ assert promote_calls[0][0][1]["semantic_type"] == "RNComponent"
122
+
123
+ def test_promotes_screen_nodes_by_path(self):
124
+ """Nodes under /screens/ should become RNScreen."""
125
+ store = _mock_store_with_responses(
126
+ [
127
+ [], # components
128
+ [["HomeScreen", "src/screens/HomeScreen.tsx"]], # screens
129
+ [], # hooks
130
+ [], # navigation
131
+ ]
132
+ )
133
+ enricher = ReactNativeEnricher(store)
134
+ result = enricher.enrich()
135
+ assert result.promoted == 1
136
+ assert result.patterns_found["screens"] == 1
137
+
138
+ calls = store._graph.query.call_args_list
139
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
140
+ assert promote_calls[0][0][1]["semantic_type"] == "RNScreen"
141
+
142
+ def test_promotes_screen_nodes_by_name_suffix(self):
143
+ """Nodes whose name ends with 'Screen' should become RNScreen."""
144
+ store = _mock_store_with_responses(
145
+ [
146
+ [], # components
147
+ [["ProfileScreen", "src/ProfileScreen.tsx"]], # screens
148
+ [], # hooks
149
+ [], # navigation
150
+ ]
151
+ )
152
+ enricher = ReactNativeEnricher(store)
153
+ result = enricher.enrich()
154
+ assert result.promoted == 1
155
+
156
+ calls = store._graph.query.call_args_list
157
+ promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
158
+ assert promote_calls[0][0][1]["semantic_type"] == "RNScreen"
159
+
160
+
--- a/tests/test_enrichment_react_native.py
+++ b/tests/test_enrichment_react_native.py
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_enrichment_react_native.py
+++ b/tests/test_enrichment_react_native.py
@@ -0,0 +1,160 @@
1 React_Native_titlecase(self):
2 # ── det_mock_store())
3 assert "React N())
4 assert "react-native" in enricher.detection_patterns
5
6 def test_contains_expo(self):
7 enricher = ReactNativeEnricher(_mock_store())
8 assert "expo" in enricher.detection_patterns
9
10 def test_returns_list(self):
11 enricher = ReactNativeEnricher(_mock_store())
12 assert isinstance(enricher.
13 # ── detect() ──────────────────────────────────────────────────────ent.react_native — Reac"
14 def test_returns_ next(response_iter)
15 except StopIteration:
16 rs = None
17 return MagicMock(result_set=rs)
18
19 graph.query.side_effect = _side_effect
20 client.select_graph.return_value = graph
21 return GraphStore(client)
22
23
24 # ── framework_name ────────────────────────────────────────────────────────────
25
26
27 class TestFrameworkName:
28 def test_framework_name_is_react_native(self):
29 enricher = ReactNativeEnricher(_mock_store())
30 assert enricher.framework_name == "react-native"
31
32
33 # ── detection_patterns ────────────────────────────────────────────────────────
34
35
36 class TestDetectionPatterns:
37 def test_contains_react_native_hyphenated(self):
38 enricher = ReactNativeEnricher(_mock_store())
39 assert "react-native" in enricher.detection_patterns
40
41 def test_contains_expo(self):
42 enricher = ReactNativeEnricher(_mock_store())
43 assert "expo" in enricher.detection_patterns
44
45 def test_returns_list(self):
46 enricher = ReactNativeEnricher(_mock_store())
47 assert isinstance(enricher.detection_patterns, list)
48
49 def test_has_at_least_two_patterns(self):
50 enricher = ReactNativeEnricher(_mock_store())
51 assert len(enricher.detection_patterns) >= 2
52
53
54 # ── detect() ─────────────────────────────────────────────────────────────────
55
56
57 class TestDetect:
58 def test_returns_true_when_pattern_found(self):
59 store = _mock_store(result_set=[[1]])
60 enricher = ReactNativeEnricher(store)
61 assert enricher.detect() is True
62
63 def test_returns_false_when_no_match(self):
64 store = _mock_store(result_set=[[0]])
65 enricher = ReactNativeEnricher(store)
66 assert enricher.detect() is False
67
68 def test_returns_false_on_empty_result_set(self):
69 store = _mock_store(result_set=[])
70 enricher = ReactNativeEnricher(store)
71 assert enricher.detect() is False
72
73 def test_short_circuits_on_first_match(self):
74 store = _mock_store(result_set=[[1]])
75 enricher = ReactNativeEnricher(store)
76 assert enricher.detect() is True
77 assert store._graph.query.call_count == 1
78
79
80 # ── enrich() ─────────────────────────────────────────────────────────────────
81
82
83 class TestEnrich:
84 def _empty_enricher(self):
85 # 4 pattern queries: components, screens, hooks, navigation
86 store = _mock_store_with_responses([[], [], [], []])
87 return ReactNativeEnricher(store)
88
89 def test_returns_enrichment_result(self):
90 assert isinstance(self._empty_enricher().enrich(), EnrichmentResult)
91
92 def test_zero_promoted_when_no_matches(self):
93 result = self._empty_enricher().enrich()
94 assert result.promoted == 0
95
96 def test_patterns_found_keys_present(self):
97 result = self._empty_enricher().enrich()
98 assert "components" in result.patterns_found
99 assert "screens" in result.patterns_found
100 assert "hooks" in result.patterns_found
101 assert "navigation" in result.patterns_found
102
103 def test_promotes_component_nodes(self):
104 """JSX/TSX nodes should become RNComponent."""
105 store = _mock_store_with_responses(
106 [
107 [["Button", "src/components/Button.tsx"]], # components
108 [], # screens
109 [], # hooks
110 [], # navigation
111 ]
112 )
113 enricher = ReactNativeEnricher(store)
114 result = enricher.enrich()
115 assert result.promoted == 1
116 assert result.patterns_found["components"] == 1
117
118 calls = store._graph.query.call_args_list
119 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
120 assert len(promote_calls) == 1
121 assert promote_calls[0][0][1]["semantic_type"] == "RNComponent"
122
123 def test_promotes_screen_nodes_by_path(self):
124 """Nodes under /screens/ should become RNScreen."""
125 store = _mock_store_with_responses(
126 [
127 [], # components
128 [["HomeScreen", "src/screens/HomeScreen.tsx"]], # screens
129 [], # hooks
130 [], # navigation
131 ]
132 )
133 enricher = ReactNativeEnricher(store)
134 result = enricher.enrich()
135 assert result.promoted == 1
136 assert result.patterns_found["screens"] == 1
137
138 calls = store._graph.query.call_args_list
139 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
140 assert promote_calls[0][0][1]["semantic_type"] == "RNScreen"
141
142 def test_promotes_screen_nodes_by_name_suffix(self):
143 """Nodes whose name ends with 'Screen' should become RNScreen."""
144 store = _mock_store_with_responses(
145 [
146 [], # components
147 [["ProfileScreen", "src/ProfileScreen.tsx"]], # screens
148 [], # hooks
149 [], # navigation
150 ]
151 )
152 enricher = ReactNativeEnricher(store)
153 result = enricher.enrich()
154 assert result.promoted == 1
155
156 calls = store._graph.query.call_args_list
157 promote_calls = [c for c in calls if "SET n.semantic_type" in c[0][0]]
158 assert promote_calls[0][0][1]["semantic_type"] == "RNScreen"
159
160

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button