1- import time
2- import socket
31import pathlib
4- from typing import Generator
5-
62import pytest
3+ from playwright .sync_api import expect , sync_playwright
4+ from axe_core_python .sync_playwright import Axe
75import frontmatter
8- from xprocess import ProcessStarter
9- from playwright .sync_api import Page , expect , sync_playwright
6+ from typing import Generator
7+ import subprocess
8+ import http .server
9+ import socketserver
10+ import threading
11+ import time
12+ import socket
1013
1114
12- from axe_core_python .sync_playwright import Axe
15+ def find_free_port ():
16+ """Find a free port to use for the test server"""
17+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
18+ s .bind (("" , 0 ))
19+ s .listen (1 )
20+ port = s .getsockname ()[1 ]
21+ return port
1322
1423
15- @pytest .fixture (scope = "module" )
16- def page_url (xprocess , url_port ):
17- """Returns the url of the live server"""
24+ @pytest .fixture (scope = "session" )
25+ def built_site ():
26+ """Build the site once for all tests"""
27+ print ("Building site for tests..." )
28+ result = subprocess .run (
29+ ["uv" , "run" , "render-engine" , "build" ], # use uv
30+ capture_output = True ,
31+ text = True ,
32+ )
33+ if result .returncode != 0 :
34+ pytest .fail (f"Failed to build site: { result .stderr } " )
35+ return pathlib .Path ("output" )
1836
19- url , port = url_port
2037
21- class Starter ( ProcessStarter ):
22- # Start the process
23- args = [ "render-engine" , "serve" ]
24- terminate_on_interrupt = True
38+ @ pytest . fixture ( scope = "session" )
39+ def test_server ( built_site ):
40+ """Start a simple HTTP server to serve the built site"""
41+ port = find_free_port ()
2542
26- def startup_check (self ):
27- # Polling mechanism for a more robust startup check
28- max_attempts = 5
29- attempt_interval = 1 # seconds
43+ class Handler (http .server .SimpleHTTPRequestHandler ):
44+ def __init__ (self , * args , ** kwargs ):
45+ super ().__init__ (* args , directory = str (built_site ), ** kwargs )
3046
31- for _ in range (max_attempts ):
32- try :
33- sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
34- sock .connect (("localhost" , port ))
35- sock .sendall (b"ping\n " )
36- response = sock .recv (
37- 1024
38- ) # Receive enough bytes to get the full response
39- if response == b"pong!" : # Compare to bytes
40- return True
41- except (ConnectionRefusedError , OSError ):
42- # Connection not yet ready, or process not fully up
43- pass
44- finally :
45- sock .close () # Ensure socket is closed
47+ httpd = socketserver .TCPServer (("" , port ), Handler )
4648
47- time .sleep (attempt_interval )
48- return False # Failed to connect after max_attempts
49+ # Start server in a thread
50+ server_thread = threading .Thread (target = httpd .serve_forever )
51+ server_thread .daemon = True
52+ server_thread .start ()
4953
50- xprocess .ensure ("page_url" , Starter )
54+ # Wait for server to start
55+ time .sleep (1 )
5156
57+ base_url = f"http://localhost:{ port } "
58+
59+ yield base_url
60+
61+ httpd .shutdown ()
62+
63+
64+ @pytest .fixture (scope = "session" )
65+ def browser_context (test_server ):
66+ """Create a browser context for all tests"""
5267 with sync_playwright () as p :
5368 browser = p .chromium .launch ()
5469 context = browser .new_context ()
5570 page = context .new_page ()
5671
57- # Return the URL of the live server
58- yield page , url
72+ yield page , test_server
5973
60- # Clean up the process
61- xprocess . getinfo ( "page_url" ). terminate ()
74+ context . close ()
75+ browser . close ()
6276
6377
64- def test_accessibility (page_url : tuple [ Page , str ] ):
78+ def test_accessibility (browser_context ):
6579 """Run accessibility tests on the homepage"""
66- page , live_server_url = page_url
67- page .goto (f"{ live_server_url } /" )
80+ page , base_url = browser_context
81+ page .goto (f"{ base_url } /" )
6882
6983 axe = Axe ()
7084 results = axe .run (page , options = {"runOnly" : ["wcag2a" , "wcag2aa" ]})
@@ -74,49 +88,30 @@ def test_accessibility(page_url: tuple[Page, str]):
7488 ), f"Accessibility violations found: { results ['violations' ]} "
7589
7690
77- def test_destination (
78- loaded_route : str ,
79- page_url : tuple [Page , str ],
80- ) -> None :
91+ def test_destination (loaded_route : str , browser_context ) -> None :
8192 """Test that the destinations page loads with seeded data"""
82- # Create a destination
83- page , live_server_url = page_url
84- response = page .goto (f"{ live_server_url } /{ loaded_route } " )
85-
86- assert response .status == 200 # Check that the page loaded successfully
87-
93+ page , base_url = browser_context
94+ response = page .goto (f"{ base_url } /{ loaded_route } " )
95+ assert response .status == 200
8896
89- # LANG_ROUTES = (
90- # "/es/",
91- # "/es/about/",
92- # "/es/events/",
93- # "/es/community/",
94- # "/sw/",
95- # "/sw/about/",
96- # "/sw/events/",
97- # "/sw/community/",
98- # )
9997
10098LANG_ROUTES = (
10199 "/" ,
102- "/about/ " ,
103- "/events/" ,
104- "/community/ " ,
105- "/support/ " ,
100+ "/about.html " ,
101+ "/bpd- events/" ,
102+ "/community.html " ,
103+ "/support.html " ,
106104 "/blog/" ,
107105)
108106
109107
110108@pytest .mark .parametrize ("route" , LANG_ROUTES )
111- def test_headers_in_language (page_url : tuple [ Page , str ] , route : str ) -> None :
109+ def test_headers_in_language (browser_context , route : str ) -> None :
112110 """checks the route and the language of each route"""
113- page , live_server_url = page_url
114- response = page .goto (f"{ live_server_url } { route } " )
111+ page , base_url = browser_context
112+ response = page .goto (f"{ base_url } { route } " )
115113 assert response .status == 200
116114 doc_lang = page .evaluate ("document.documentElement.lang" )
117- # lang = route.lstrip("/").split("/", maxsplit=1)[
118- # 0
119- # ] # urls start with the language if not en
120115 assert doc_lang == "en"
121116
122117 axe = Axe ()
@@ -131,35 +126,33 @@ def test_headers_in_language(page_url: tuple[Page, str], route: str) -> None:
131126 "title, url" ,
132127 (
133128 ("Home" , "/" ),
134- ("Blog" , "/blog" ),
135- ("About Us" , "/about/ " ),
136- ("Events" , "/events/" ),
137- ("Community" , "/community/ " ),
138- ("Support" , "/support/ " ),
129+ ("Blog" , "/blog/ " ),
130+ ("About Us" , "/about.html " ),
131+ ("BPD Events" , "/bpd- events/" ),
132+ ("Community" , "/community.html " ),
133+ ("Support Us " , "/support.html " ),
139134 ),
140135)
141- def test_bpdevs_title_en (page_url : tuple [ Page , str ] , title : str , url : str ) -> None :
142- page , live_server_url = page_url
143- page .goto (f"{ live_server_url } { url } " )
136+ def test_bpdevs_title_en (browser_context , title : str , url : str ) -> None :
137+ page , base_url = browser_context
138+ page .goto (f"{ base_url } { url } " )
144139 expect (page ).to_have_title (f"Black Python Devs | { title } " )
145140
146141 axe = Axe ()
147- # results = axe.run(page)
148142 results = axe .run (page , options = {"runOnly" : ["wcag2a" , "wcag2aa" ]})
149143
150144 assert (
151145 len (results ["violations" ]) == 0
152146 ), f"Accessibility violations found: { results ['violations' ]} "
153147
154148
155- def test_mailto_bpdevs (page_url : tuple [ Page , str ] ) -> None :
156- page , live_server_url = page_url
157- page .goto (live_server_url )
149+ def test_mailto_bpdevs (browser_context ) -> None :
150+ page , base_url = browser_context
151+ page .goto (base_url )
158152 mailto = page .get_by_role ("link" , name = "email" )
159153 expect (
mailto ).
to_have_attribute (
"href" ,
"mailto:[email protected] " )
160154
161155 axe = Axe ()
162- # results = axe.run(page)
163156 results = axe .run (page , options = {"runOnly" : ["wcag2a" , "wcag2aa" ]})
164157
165158 assert (
@@ -169,12 +162,12 @@ def test_mailto_bpdevs(page_url: tuple[Page, str]) -> None:
169162
170163@pytest .mark .parametrize (
171164 "url" ,
172- ("/blog" ,),
165+ ("/blog/ " ,),
173166)
174- def test_page_description_in_index_and_blog (page_url : tuple [ Page , str ] , url : str ):
167+ def test_page_description_in_index_and_blog (browser_context , url : str ):
175168 """Checks for the descriptions data in the blog posts. There should be some objects with the class `post-description`"""
176- page , live_server_url = page_url
177- page .goto (f"{ live_server_url } { url } " )
169+ page , base_url = browser_context
170+ page .goto (f"{ base_url } { url } " )
178171 expect (page .locator ("p.post-description" ).first ).to_be_visible ()
179172 expect (page .locator ("p.post-description" ).first ).not_to_be_empty ()
180173
@@ -189,8 +182,7 @@ def test_page_description_in_index_and_blog(page_url: tuple[Page, str], url: str
189182def stem_description (
190183 path : pathlib .Path ,
191184) -> Generator [tuple [str , frontmatter .Post ], None , None ]:
192- """iterate throug a list returning the stem of the file and the contents"""
193-
185+ """iterate through a list returning the stem of the file and the contents"""
194186 for entry in path .glob ("*.md" ):
195187 yield (entry .stem , frontmatter .loads (entry .read_text ()))
196188
@@ -199,15 +191,15 @@ def stem_description(
199191
200192
201193@pytest .mark .parametrize ("post" , list (blog_posts ))
202- def test_page_blog_posts (
203- page_url : tuple [Page , str ], post : tuple [str , frontmatter .Post ]
204- ):
194+ def test_page_blog_posts (browser_context , post : tuple [str , frontmatter .Post ]):
205195 """Checks that the meta page description matches the description of the post"""
206- page , live_server_url = page_url
207- entry_stem , frontmatter = post
208- url = f"{ live_server_url } /{ entry_stem } /"
196+ page , base_url = browser_context
197+ entry_stem , frontmatter_data = post
198+
199+ # Convert blog post filename to URL path
200+ # Blog posts are in /blog/ directory in the output
201+ url = f"{ base_url } /blog/{ entry_stem } .html"
209202
210- # Increased timeout and added wait_until="networkidle"
211203 page .goto (url , timeout = 60000 , wait_until = "networkidle" )
212204
213205 # More robust waiting for the meta description
0 commit comments