@@ -134,6 +134,24 @@ test.describe('', () => {
134134 expect ( extensionId ) . toBeTruthy ( ) ;
135135 } ) ;
136136
137+ // Contract test: verify GitHub API returns all fields we depend on.
138+ // If this fails, our mocks need updating to match API changes.
139+ // Note: API doesn't return repository.full_name - popup.js parses from url field instead.
140+ test ( 'GitHub API contract' , async ( ) => {
141+ const issue = TEST_ISSUES [ 0 ] ;
142+ const url = `https://api.github.com/repos/${ issue . owner } /${ issue . repo } /issues/${ issue . number } ` ;
143+ const response = await fetch ( url ) ;
144+ expect ( response . ok ) . toBe ( true ) ;
145+
146+ const data = await response . json ( ) ;
147+ expect ( data ) . toHaveProperty ( 'title' ) ;
148+ expect ( data ) . toHaveProperty ( 'state' ) ;
149+ expect ( data ) . toHaveProperty ( 'html_url' ) ;
150+ expect ( data ) . toHaveProperty ( 'updated_at' ) ;
151+ expect ( data ) . toHaveProperty ( 'comments' ) ;
152+ expect ( data ) . toHaveProperty ( 'url' ) ; // Used by popup.js to parse repo name
153+ } ) ;
154+
137155 test ( 'bookmark button' , async ( ) => {
138156 const bookmarkSelector = '[data-extension-bookmark]' ;
139157 const headerActionsSelector = '[data-component="PH_Actions"]' ;
@@ -241,21 +259,28 @@ test.describe('', () => {
241259 } ) ;
242260
243261 test ( 'displays bookmarked issues' , async ( ) => {
244- const testBookmarks = {
245- 'microsoft/playwright/issues/1' : {
246- owner : 'microsoft' ,
247- repo : 'playwright' ,
248- number : 1 ,
249- type : 'issues' ,
250- bookmarkedAt : Date . now ( )
251- }
252- } ;
253- await addBookmarks ( context , testBookmarks ) ;
262+ const issue = TEST_ISSUES [ 0 ] ;
263+ await addBookmarks ( context , makeBookmark ( issue ) ) ;
254264
255265 const popupPage = await context . newPage ( ) ;
256- await popupPage . goto ( `chrome-extension://${ extensionId } /assets/popup.html` ) ;
257266
258- await popupPage . waitForTimeout ( 2000 ) ;
267+ // Mock API to avoid rate limits and ensure test stability
268+ await popupPage . route ( '**/api.github.com/repos/**' , async ( route ) => {
269+ await route . fulfill ( {
270+ status : 200 ,
271+ contentType : 'application/json' ,
272+ body : JSON . stringify ( {
273+ title : 'Test Issue' ,
274+ state : 'open' ,
275+ html_url : issueUrl ( issue ) ,
276+ url : `https://api.github.com/repos/${ issue . owner } /${ issue . repo } /issues/${ issue . number } ` ,
277+ updated_at : '2024-01-15T10:30:00Z' ,
278+ comments : 5
279+ } )
280+ } ) ;
281+ } ) ;
282+
283+ await popupPage . goto ( `chrome-extension://${ extensionId } /assets/popup.html` ) ;
259284
260285 const issueItem = popupPage . locator ( '.issue-item' ) ;
261286 await expect ( issueItem ) . toBeVisible ( { timeout : 10000 } ) ;
@@ -264,18 +289,28 @@ test.describe('', () => {
264289 } ) ;
265290
266291 test ( 'removes bookmark when remove button clicked' , async ( ) => {
267- const testBookmarks = {
268- 'microsoft/playwright/issues/1' : {
269- owner : 'microsoft' ,
270- repo : 'playwright' ,
271- number : 1 ,
272- type : 'issues' ,
273- bookmarkedAt : Date . now ( )
274- }
275- } ;
276- await addBookmarks ( context , testBookmarks ) ;
292+ const issue = TEST_ISSUES [ 0 ] ;
293+ const bookmarkKey = `${ issue . owner } /${ issue . repo } /issues/${ issue . number } ` ;
294+ await addBookmarks ( context , makeBookmark ( issue ) ) ;
277295
278296 const popupPage = await context . newPage ( ) ;
297+
298+ // Mock API to avoid rate limits and ensure test stability
299+ await popupPage . route ( '**/api.github.com/repos/**' , async ( route ) => {
300+ await route . fulfill ( {
301+ status : 200 ,
302+ contentType : 'application/json' ,
303+ body : JSON . stringify ( {
304+ title : 'Test Issue' ,
305+ state : 'open' ,
306+ html_url : issueUrl ( issue ) ,
307+ url : `https://api.github.com/repos/${ issue . owner } /${ issue . repo } /issues/${ issue . number } ` ,
308+ updated_at : '2024-01-15T10:30:00Z' ,
309+ comments : 5
310+ } )
311+ } ) ;
312+ } ) ;
313+
279314 await popupPage . goto ( `chrome-extension://${ extensionId } /assets/popup.html` ) ;
280315
281316 const issueItem = popupPage . locator ( '.issue-item' ) ;
@@ -288,7 +323,7 @@ test.describe('', () => {
288323 await expect ( issueItem ) . not . toBeVisible ( ) ;
289324
290325 const bookmarks = await getBookmarks ( context ) ;
291- expect ( Object . keys ( bookmarks ) ) . not . toContain ( 'microsoft/playwright/issues/1' ) ;
326+ expect ( Object . keys ( bookmarks ) ) . not . toContain ( bookmarkKey ) ;
292327
293328 await popupPage . close ( ) ;
294329 } ) ;
@@ -618,6 +653,7 @@ test.describe('', () => {
618653
619654 test ( 'token is used in API requests' , async ( ) => {
620655 const testToken = 'github_pat_11ABCDEFGHIJKLMNOPQRST_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW' ;
656+ const issue = TEST_ISSUES [ 0 ] ;
621657
622658 // Store the token
623659 const setupPage = await context . newPage ( ) ;
@@ -630,16 +666,7 @@ test.describe('', () => {
630666 await setupPage . close ( ) ;
631667
632668 // Add a bookmark
633- const testBookmarks = {
634- 'microsoft/playwright/issues/999' : {
635- owner : 'microsoft' ,
636- repo : 'playwright' ,
637- number : 999 ,
638- type : 'issues' ,
639- bookmarkedAt : Date . now ( )
640- }
641- } ;
642- await addBookmarks ( context , testBookmarks ) ;
669+ await addBookmarks ( context , makeBookmark ( issue ) ) ;
643670
644671 // Track Authorization header from intercepted requests
645672 let capturedAuthHeader = null ;
@@ -650,11 +677,11 @@ test.describe('', () => {
650677 status : 200 ,
651678 contentType : 'application/json' ,
652679 body : JSON . stringify ( {
653- id : 999 ,
654- number : 999 ,
680+ number : issue . number ,
655681 title : 'Test Issue' ,
656682 state : 'open' ,
657- html_url : 'https://github.com/microsoft/playwright/issues/999' ,
683+ html_url : issueUrl ( issue ) ,
684+ url : `https://api.github.com/repos/${ issue . owner } /${ issue . repo } /issues/${ issue . number } ` ,
658685 updated_at : new Date ( ) . toISOString ( ) ,
659686 comments : 0
660687 } )
@@ -685,6 +712,8 @@ test.describe('', () => {
685712 } ) ;
686713
687714 test ( 'requests work without token' , async ( ) => {
715+ const issue = TEST_ISSUES [ 1 ] ; // Use different issue from previous test
716+
688717 // Ensure no token is set
689718 const setupPage = await context . newPage ( ) ;
690719 await setupPage . goto ( `chrome-extension://${ extensionId } /assets/options.html` ) ;
@@ -696,16 +725,7 @@ test.describe('', () => {
696725 await setupPage . close ( ) ;
697726
698727 // Add a bookmark
699- const testBookmarks = {
700- 'microsoft/playwright/issues/888' : {
701- owner : 'microsoft' ,
702- repo : 'playwright' ,
703- number : 888 ,
704- type : 'issues' ,
705- bookmarkedAt : Date . now ( )
706- }
707- } ;
708- await addBookmarks ( context , testBookmarks ) ;
728+ await addBookmarks ( context , makeBookmark ( issue ) ) ;
709729
710730 // Track Authorization header
711731 let capturedAuthHeader = null ;
@@ -715,11 +735,11 @@ test.describe('', () => {
715735 status : 200 ,
716736 contentType : 'application/json' ,
717737 body : JSON . stringify ( {
718- id : 888 ,
719- number : 888 ,
738+ number : issue . number ,
720739 title : 'Test Issue Without Token' ,
721740 state : 'open' ,
722- html_url : 'https://github.com/microsoft/playwright/issues/888' ,
741+ html_url : issueUrl ( issue ) ,
742+ url : `https://api.github.com/repos/${ issue . owner } /${ issue . repo } /issues/${ issue . number } ` ,
723743 updated_at : new Date ( ) . toISOString ( ) ,
724744 comments : 0
725745 } )
@@ -741,50 +761,44 @@ test.describe('', () => {
741761
742762 // ============================================================
743763 // VISUAL REGRESSION - Screenshot comparison for CSS development
764+ //
765+ // These tests use mocked API responses for visual stability:
766+ // - Ensures consistent issue titles, states, and timestamps in screenshots
767+ // - Avoids rate limits that would cause flaky failures in CI
768+ // - Contract test in Basics block verifies mocks match real API structure
744769 // ============================================================
745770
746771 test . describe ( 'Visual' , { tag : '@visual' } , ( ) => {
747772 test ( 'popup with issues' , async ( ) => {
748- // Setup mock bookmarks
749- const testBookmarks = {
750- 'microsoft/playwright/issues/123' : {
751- owner : 'microsoft' ,
752- repo : 'playwright' ,
753- number : 123 ,
754- type : 'issues' ,
755- bookmarkedAt : Date . now ( )
756- } ,
757- 'facebook/react/issues/456' : {
758- owner : 'facebook' ,
759- repo : 'react' ,
760- number : 456 ,
761- type : 'issues' ,
762- bookmarkedAt : Date . now ( ) - 86400000
763- }
764- } ;
765- await addBookmarks ( context , testBookmarks ) ;
773+ // Use TEST_ISSUES for consistency, with different timestamps for visual variety
774+ const issue1 = TEST_ISSUES [ 0 ] ; // microsoft/playwright
775+ const issue2 = TEST_ISSUES [ 2 ] ; // facebook/react
776+ await addBookmarks ( context , {
777+ ...makeBookmark ( issue1 ) ,
778+ ...makeBookmark ( issue2 , Date . now ( ) - 86400000 )
779+ } ) ;
766780
767781 const popupPage = await context . newPage ( ) ;
768782
769- // Mock API responses
783+ // Mock API for visual stability - consistent titles/states for screenshot comparison
770784 await popupPage . route ( '**/api.github.com/repos/**' , async ( route ) => {
771785 const url = route . request ( ) . url ( ) ;
772786 let data = {
773787 title : 'Sample Issue' ,
774788 state : 'open' ,
775- html_url : url ,
789+ html_url : issueUrl ( issue1 ) ,
790+ url : url ,
776791 updated_at : '2024-01-15T10:30:00Z' ,
777- comments : 5 ,
778- repository : { full_name : 'org/repo' }
792+ comments : 5
779793 } ;
780- if ( url . includes ( 'playwright' ) ) {
794+ if ( url . includes ( issue1 . repo ) ) {
781795 data . title = 'Add visual regression testing support' ;
782- data . repository . full_name = 'microsoft/playwright' ;
796+ data . html_url = issueUrl ( issue1 ) ;
783797 data . state = 'open' ;
784798 data . comments = 12 ;
785- } else if ( url . includes ( 'react' ) ) {
799+ } else if ( url . includes ( issue2 . repo ) ) {
786800 data . title = 'Improve hydration performance' ;
787- data . repository . full_name = 'facebook/react' ;
801+ data . html_url = issueUrl ( issue2 ) ;
788802 data . state = 'closed' ;
789803 data . comments = 42 ;
790804 }
@@ -1186,6 +1200,7 @@ test.describe('', () => {
11861200 } ) ;
11871201
11881202 // Route REST API with mixed responses: 1 success, 1 404, 1 403
1203+ const successIssue = TEST_ISSUES [ 0 ] ;
11891204 let callCount = 0 ;
11901205 await authContext . route ( '**/api.github.com/repos/**' , ( route ) => {
11911206 callCount ++ ;
@@ -1194,9 +1209,10 @@ test.describe('', () => {
11941209 status : 200 ,
11951210 contentType : 'application/json' ,
11961211 body : JSON . stringify ( {
1197- number : 1 ,
1212+ number : successIssue . number ,
11981213 title : 'Test Issue' ,
1199- html_url : 'https://github.com/microsoft/playwright/issues/1' ,
1214+ html_url : issueUrl ( successIssue ) ,
1215+ url : `https://api.github.com/repos/${ successIssue . owner } /${ successIssue . repo } /issues/${ successIssue . number } ` ,
12001216 state : 'open' ,
12011217 updated_at : new Date ( ) . toISOString ( ) ,
12021218 comments : 0
0 commit comments