@@ -287,3 +287,100 @@ def test_no_frontend_section_for_pure_python(self):
287287 dna = CodebaseDNA (repo_name = "python-only" )
288288 out = render_agents_md (dna )
289289 assert "## Frontend" not in out
290+
291+
292+ class TestReactPatternDetection :
293+
294+ def test_detects_react_query_usage (self , tmp_path : Path ):
295+ _write_pkg (tmp_path , {"react" : "^18" , "@tanstack/react-query" : "^5" })
296+ # need 2+ files with useQuery to pass the threshold
297+ (tmp_path / "useUser.tsx" ).write_text (
298+ "import { useQuery } from '@tanstack/react-query';\n "
299+ "export const useUser = (id) => useQuery({ queryKey: ['user', id] });\n "
300+ )
301+ (tmp_path / "useRepos.tsx" ).write_text (
302+ "import { useQuery } from '@tanstack/react-query';\n "
303+ "export const useRepos = () => useQuery({ queryKey: ['repos'] });\n "
304+ )
305+ extractor = DNAExtractor ()
306+ dna = extractor .extract (str (tmp_path ))
307+ assert dna .frontend_patterns .uses_react_query is True
308+
309+ def test_detects_cn_utility (self , tmp_path : Path ):
310+ _write_pkg (tmp_path , {"react" : "^18" })
311+ (tmp_path / "Button.tsx" ).write_text (
312+ "import { cn } from '@/lib/utils';\n "
313+ "export const Button = ({ className }) => (\n "
314+ " <button className={cn('btn', className)}>click</button>\n "
315+ ");\n "
316+ )
317+ (tmp_path / "Card.tsx" ).write_text (
318+ "import { cn } from '@/lib/utils';\n "
319+ "export const Card = ({ cls }) => <div className={cn('card', cls)} />;\n "
320+ )
321+ (tmp_path / "Input.tsx" ).write_text (
322+ "import { cn } from '@/lib/utils';\n "
323+ "export const Input = ({ cls }) => <input className={cn('input', cls)} />;\n "
324+ )
325+ extractor = DNAExtractor ()
326+ dna = extractor .extract (str (tmp_path ))
327+ assert dna .frontend_patterns .uses_cn_utility is True
328+
329+ def test_detects_custom_hooks (self , tmp_path : Path ):
330+ _write_pkg (tmp_path , {"react" : "^18" })
331+ hooks_dir = tmp_path / "hooks"
332+ hooks_dir .mkdir ()
333+ (hooks_dir / "useAuth.ts" ).write_text ("export const useAuth = () => ({});" )
334+ (hooks_dir / "useRepos.ts" ).write_text ("export const useRepos = () => ({});" )
335+ (tmp_path / "app.tsx" ).write_text ("const x = 1;" )
336+ extractor = DNAExtractor ()
337+ dna = extractor .extract (str (tmp_path ))
338+ assert dna .frontend_patterns .has_custom_hooks is True
339+
340+ def test_detects_canonical_hook (self , tmp_path : Path ):
341+ _write_pkg (tmp_path , {"react" : "^18" , "@tanstack/react-query" : "^5" })
342+ hooks_dir = tmp_path / "hooks"
343+ hooks_dir .mkdir ()
344+ (hooks_dir / "useCachedQuery.ts" ).write_text ("export const useCachedQuery = () => ({});" )
345+ # import useCachedQuery in 3 different components
346+ for i in range (3 ):
347+ (tmp_path / f"Component{ i } .tsx" ).write_text (
348+ "import { useCachedQuery } from '../hooks/useCachedQuery';\n "
349+ "export const C = () => { const d = useCachedQuery(); return null; };\n "
350+ )
351+ extractor = DNAExtractor ()
352+ dna = extractor .extract (str (tmp_path ))
353+ assert dna .frontend_patterns .has_custom_hooks is True
354+ assert dna .frontend_patterns .canonical_data_hook == "useCachedQuery"
355+
356+ def test_detects_shared_types_file (self , tmp_path : Path ):
357+ _write_pkg (tmp_path , {"react" : "^18" })
358+ src = tmp_path / "src"
359+ src .mkdir ()
360+ (src / "types.ts" ).write_text ("export interface User { id: string; }" )
361+ (tmp_path / "app.tsx" ).write_text ("const x = 1;" )
362+ extractor = DNAExtractor ()
363+ dna = extractor .extract (str (tmp_path ))
364+ assert dna .frontend_patterns .shared_types_file == "src/types.ts"
365+
366+ def test_react_patterns_rendered_in_agents_md (self , tmp_path : Path ):
367+ from saar .formatters .agents_md import render_agents_md
368+ from saar .models import CodebaseDNA , FrontendPattern
369+
370+ dna = CodebaseDNA (
371+ repo_name = "test" ,
372+ frontend_patterns = FrontendPattern (
373+ framework = "React" ,
374+ uses_react_query = True ,
375+ uses_cn_utility = True ,
376+ has_custom_hooks = True ,
377+ canonical_data_hook = "useCachedQuery" ,
378+ shared_types_file = "src/types.ts" ,
379+ )
380+ )
381+ out = render_agents_md (dna )
382+ assert "useQuery" in out
383+ assert "useEffect" in out
384+ assert "cn()" in out
385+ assert "useCachedQuery" in out
386+ assert "src/types.ts" in out
0 commit comments