11import  {  test ,  expect  }  from  '@playwright/test' ; 
2+ import  { 
3+   jobCards , 
4+   jobTitles , 
5+   jobTypeButtons , 
6+   loginWithCredentials , 
7+   openLoginPage , 
8+   openSignUpPage , 
9+   openUserMenu , 
10+   searchInput , 
11+   waitForJobCount , 
12+   JOB_TITLE_SELECTOR , 
13+ }  from  './utils' ; 
214
315test . describe ( 'GitJobs' ,  ( )  =>  { 
416  test . beforeEach ( async  ( {  page } )  =>  { 
5-      for  ( let  i  =  0 ;  i  <  3 ;  i ++ )  { 
17+    for  ( let  i  =  0 ;  i  <  3 ;  i ++ )  { 
618      try  { 
719        await  page . goto ( '/' ,  {  timeout : 60000  } ) ; 
820        break ; 
@@ -20,44 +32,91 @@ test.describe('GitJobs', () => {
2032  test ( 'should apply a filter and verify that the results are updated' ,  async  ( {  page } )  =>  { 
2133    await  page . locator ( 'div:nth-child(4) > div > .font-semibold' ) . first ( ) . click ( ) ; 
2234    await  page . locator ( 'label' ) . filter ( {  hasText : 'Full Time'  } ) . nth ( 1 ) . click ( ) ; 
23-     await  page . waitForFunction ( 
24-       ( )  =>  { 
25-         const  currentCount  =  document . querySelectorAll ( '[data-preview-job="true"]' ) . length ; 
26-         return  currentCount  ===  12 ; 
35+     await  waitForJobCount ( page ,  12 ) ; 
36+ 
37+     const  jobTypeButtonsList  =  await  jobTypeButtons ( page ) . all ( ) ; 
38+     for  ( const  jobCard  of  jobTypeButtonsList )  { 
39+       const  jobTypeElement  =  jobCard . locator ( '.capitalize' ) . first ( ) ; 
40+       if  ( await  jobTypeElement . isVisible ( ) )  { 
41+         await  expect ( jobTypeElement ) . toHaveText ( 'full time' ) ; 
42+       } 
43+     } 
44+   } ) ; 
45+ 
46+   test ( 'should apply multiple filters and verify that the results are updated' ,  async  ( {  page } )  =>  { 
47+     await  page . locator ( 'div:nth-child(4) > div > .font-semibold' ) . first ( ) . click ( ) ; 
48+     await  page . locator ( 'label' ) . filter ( {  hasText : 'Part Time'  } ) . nth ( 1 ) . click ( ) ; 
49+     await  page . locator ( 'label' ) . filter ( {  hasText : 'Internship'  } ) . nth ( 1 ) . click ( ) ; 
50+ 
51+     await  waitForJobCount ( page ,  6 ) ; 
52+ 
53+     const  jobTypeButtonsList  =  await  jobTypeButtons ( page ) . all ( ) ; 
54+     for  ( const  jobCard  of  jobTypeButtonsList )  { 
55+       const  jobTypeElement  =  jobCard . locator ( '.capitalize' ) . first ( ) ; 
56+       if  ( await  jobTypeElement . isVisible ( ) )  { 
57+         const  jobTypeText  =  await  jobTypeElement . textContent ( ) ; 
58+         expect ( [ 'part time' ,  'internship' ] ) . toContain ( jobTypeText ?. trim ( ) ) ; 
2759      } 
60+     } 
61+   } ) ; 
62+ 
63+   test ( 'should search for a job and verify that the results are updated and contain the search term' ,  async  ( {  page } )  =>  { 
64+     await  searchInput ( page ) . click ( ) ; 
65+     await  searchInput ( page ) . fill ( 'Engineer' ) ; 
66+     await  page . locator ( '#search-jobs-btn' ) . click ( ) ; 
67+ 
68+     await  page . waitForFunction ( 
69+       ( {  selector,  term } )  =>  { 
70+         const  nodes  =  Array . from ( document . querySelectorAll ( selector ) ) ; 
71+         if  ( nodes . length  ===  0 )  { 
72+           return  false ; 
73+         } 
74+         return  nodes . every ( node  =>  node . textContent ?. toLowerCase ( ) . includes ( term ) ) ; 
75+       } , 
76+       {  selector : JOB_TITLE_SELECTOR ,  term : 'engineer'  } 
2877    ) ; 
2978
30-     const  jobCards  =  await  page . getByRole ( 'button' ,  {  name : / J o b   t y p e /   } ) . all ( ) ; 
31-     for  ( const  jobCard  of  jobCards )  { 
79+     const  jobTitleValues  =  await  jobTitles ( page ) . allTextContents ( ) ; 
80+     for  ( const  title  of  jobTitleValues )  { 
81+       expect ( title . trim ( ) . toLowerCase ( ) ) . toContain ( 'engineer' ) ; 
82+     } 
83+   } ) ; 
84+ 
85+   test ( 'should apply a filter and verify that the results are updated on mobile' ,  async  ( {  page } )  =>  { 
86+     await  page . setViewportSize ( {  width : 375 ,  height : 667  } ) ; 
87+     await  page . locator ( '#open-filters' ) . click ( ) ; 
88+     await  page . waitForSelector ( '#drawer-filters' ,  {  state : 'visible'  } ) ; 
89+     await  page . locator ( '#drawer-filters label' ) . filter ( {  hasText : 'Full Time'  } ) . click ( ) ; 
90+     await  page . locator ( '#close-filters' ) . click ( ) ; 
91+     await  page . waitForTimeout ( 500 ) ; 
92+ 
93+     const  jobTypeButtonsList  =  await  jobTypeButtons ( page ) . all ( ) ; 
94+     for  ( const  jobCard  of  jobTypeButtonsList )  { 
3295      const  jobTypeElement  =  jobCard . locator ( '.capitalize' ) . first ( ) ; 
3396      if  ( await  jobTypeElement . isVisible ( ) )  { 
3497        await  expect ( jobTypeElement ) . toHaveText ( 'full time' ) ; 
3598      } 
3699    } 
37100  } ) ; 
101+ 
38102  test ( 'should reset filters' ,  async  ( {  page } )  =>  { 
39103    await  page . locator ( 'label' ) . filter ( {  hasText : 'Part Time'  } ) . nth ( 1 ) . click ( ) ; 
40104
41-     await  page . waitForFunction ( 
42-       ( )  =>  { 
43-         const  currentCount  =  document . querySelectorAll ( '[data-preview-job="true"]' ) . length ; 
44-         return  currentCount  ===  3 ; 
45-       } 
46-     ) ; 
47-     const  firstJobAfterFilter  =  await  page . locator ( '.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1' ) . first ( ) . textContent ( ) ; 
105+     await  waitForJobCount ( page ,  3 ) ; 
106+     const  firstJobAfterFilter  =  await  jobTitles ( page ) . first ( ) . textContent ( ) ; 
48107    expect ( firstJobAfterFilter ! . trim ( ) ) . toBe ( 'Data Scientist' ) ; 
49108    await  page . locator ( '#reset-desktop-filters' ) . click ( ) ; 
50109    await  expect ( page . locator ( '#results' ) ) . toHaveText ( '1 - 20 of 21 results' ) ; 
51-     const  firstJobAfterReset  =  await  page . locator ( '.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1' ) . first ( ) . textContent ( ) ; 
110+     const  firstJobAfterReset  =  await  jobTitles ( page ) . first ( ) . textContent ( ) ; 
52111    expect ( firstJobAfterReset ! . trim ( ) ) . toBe ( 'Frontend Developer' ) ; 
53112  } ) ; 
54113
55114  test ( 'should sort jobs' ,  async  ( {  page } )  =>  { 
56-     const  initialJobTitles  =  ( await  page . locator ( '.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1' ) . allTextContents ( ) ) . map ( title  =>  title . trim ( ) ) ; 
115+     const  initialJobTitles  =  ( await  jobTitles ( page ) . allTextContents ( ) ) . map ( title  =>  title . trim ( ) ) ; 
57116    await  page . locator ( '#sort-desktop' ) . selectOption ( 'salary' ) ; 
58117    await  expect ( page ) . toHaveURL ( / \? s o r t = s a l a r y / ) ; 
59118    await  page . waitForTimeout ( 500 ) ; 
60-     const  sortedJobTitles  =  ( await  page . locator ( '.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1' ) . allTextContents ( ) ) . map ( title  =>  title . trim ( ) ) ; 
119+     const  sortedJobTitles  =  ( await  jobTitles ( page ) . allTextContents ( ) ) . map ( title  =>  title . trim ( ) ) ; 
61120    expect ( sortedJobTitles [ 0 ] ) . toBe ( 'Security Engineer' ) ; 
62121    expect ( sortedJobTitles [ 1 ] ) . toBe ( 'DevOps Engineer' ) ; 
63122    expect ( sortedJobTitles [ 2 ] ) . toBe ( 'Product Manager' ) ; 
@@ -66,6 +125,49 @@ test.describe('GitJobs', () => {
66125    expect ( sortedJobTitles ) . not . toEqual ( initialJobTitles ) ; 
67126  } ) ; 
68127
128+   test ( 'ensure filters and search persist on page refresh' ,  async  ( {  page } )  =>  { 
129+     await  searchInput ( page ) . fill ( 'Engineer' ) ; 
130+     await  page . locator ( 'label' ) . filter ( {  hasText : 'Full Time'  } ) . nth ( 1 ) . click ( ) ; 
131+     await  page . waitForTimeout ( 500 ) ; 
132+ 
133+     const  urlBeforeRefresh  =  page . url ( ) ; 
134+     expect ( urlBeforeRefresh ) . toContain ( 'Engineer' ) ; 
135+     expect ( urlBeforeRefresh ) . toContain ( 'full-time' ) ; 
136+ 
137+     await  page . reload ( ) ; 
138+     await  page . waitForTimeout ( 500 ) ; 
139+ 
140+     const  urlAfterRefresh  =  page . url ( ) ; 
141+     expect ( urlAfterRefresh ) . toBe ( urlBeforeRefresh ) ; 
142+ 
143+     const  persistedSearch  =  await  searchInput ( page ) . inputValue ( ) ; 
144+     expect ( persistedSearch ) . toBe ( 'Engineer' ) ; 
145+ 
146+     const  fullTimeCheckbox  =  await  page . locator ( 'input[id="desktop-kind[]-full-time"]' ) . isChecked ( ) ; 
147+     expect ( fullTimeCheckbox ) . toBe ( true ) ; 
148+   } ) ; 
149+ 
150+   test ( 'should show hover states and preview on job card interactions' ,  async  ( {  page } )  =>  { 
151+     await  jobCards ( page ) . first ( ) . waitFor ( ) ; 
152+     const  firstJobCard  =  jobCards ( page ) . first ( ) ; 
153+ 
154+     // Test quick preview without opening modal 
155+     const  jobTitle  =  await  firstJobCard . locator ( JOB_TITLE_SELECTOR ) . textContent ( ) ; 
156+ 
157+     // Verify job card shows basic info without modal 
158+     expect ( jobTitle ?. trim ( ) ) . toBeTruthy ( ) ; 
159+     expect ( jobTitle ?. trim ( ) ) . toBe ( 'Frontend Developer' ) ; 
160+ 
161+     // Test hover state - verify card is hoverable 
162+     await  firstJobCard . hover ( ) ; 
163+     await  expect ( firstJobCard ) . toBeVisible ( ) ; 
164+ 
165+     // Ensure modal is not open before or after hovering 
166+     await  expect ( page . locator ( '#preview-modal' ) ) . not . toBeVisible ( ) ; 
167+     await  page . waitForTimeout ( 300 ) ; 
168+     await  expect ( page . locator ( '#preview-modal' ) ) . not . toBeVisible ( ) ; 
169+   } ) ; 
170+ 
69171  test ( 'should navigate to the stats page and interact with charts' ,  async  ( {  page,  browserName } )  =>  { 
70172    if  ( browserName  ===  'firefox' )  { 
71173      // Skip this test on Firefox as it's failing due to a rendering issue with the charts 
@@ -93,27 +195,35 @@ test.describe('GitJobs', () => {
93195  } ) ; 
94196
95197  test ( 'should navigate to the sign-up page' ,  async  ( {  page } )  =>  { 
96-     await  page . locator ( '#user-dropdown-button' ) . click ( ) ; 
97-     await  page . getByRole ( 'link' ,  {  name : 'Sign up'  } ) . click ( ) ; 
198+     await  openSignUpPage ( page ) ; 
98199    await  expect ( page ) . toHaveURL ( / \/ s i g n - u p / ) ; 
99200  } ) ; 
100201
101202  test ( 'should log in a user' ,  async  ( {  page } )  =>  { 
102-     await  page . locator ( '#user-dropdown-button' ) . click ( ) ; 
103-     await  page . getByRole ( 'link' ,  {  name : 'Log in'  } ) . click ( ) ; 
203+     await  loginWithCredentials ( page ,  'test' ,  'test' ) ; 
204+   } ) ; 
205+ 
206+   test ( 'should log out a user' ,  async  ( {  page } )  =>  { 
207+     await  loginWithCredentials ( page ,  'test' ,  'test' ) ; 
208+ 
209+     await  expect ( page ) . toHaveURL ( / \/ $ / ) ; 
210+     await  openUserMenu ( page ) ; 
211+     await  page . getByRole ( 'link' ,  {  name : 'Log out'  } ) . click ( ) ; 
104212    await  page . waitForURL ( '**/log-in' ) ; 
213+   } ) ; 
214+ 
215+   test ( 'invalid credentials stay on log in page' ,  async  ( {  page } )  =>  { 
216+     await  openLoginPage ( page ) ; 
217+ 
105218    await  page . locator ( '#username' ) . fill ( 'test' ) ; 
106-     await  page . locator ( '#password' ) . fill ( 'test ' ) ; 
219+     await  page . locator ( '#password' ) . fill ( 'wrong ' ) ; 
107220    await  page . getByRole ( 'button' ,  {  name : 'Submit'  } ) . click ( ) ; 
221+ 
222+     await  expect ( page ) . toHaveURL ( '/log-in' ) ; 
108223  } ) ; 
109224
110225  test ( 'should add a new job' ,  async  ( {  page } )  =>  { 
111-     await  page . locator ( '#user-dropdown-button' ) . click ( ) ; 
112-     await  page . getByRole ( 'link' ,  {  name : 'Log in'  } ) . click ( ) ; 
113-     await  page . waitForURL ( '**/log-in' ) ; 
114-     await  page . locator ( '#username' ) . fill ( 'test' ) ; 
115-     await  page . locator ( '#password' ) . fill ( 'test' ) ; 
116-     await  page . getByRole ( 'button' ,  {  name : 'Submit'  } ) . click ( ) ; 
226+     await  loginWithCredentials ( page ,  'test' ,  'test' ) ; 
117227    await  page . goto ( '/' ) ; 
118228
119229    await  page . getByRole ( 'link' ,  {  name : 'Post a job'  } ) . click ( ) ; 
@@ -137,8 +247,8 @@ test.describe('GitJobs', () => {
137247    const  expectedSalaryCurrency  =  'USD' ; 
138248    const  expectedSalaryPeriod  =  '/ year' ; 
139249
140-     await  page . waitForSelector ( '[data-preview-job="true"]' ) ; 
141-     await  page . locator ( '[data-preview-job="true"]' ) . first ( ) . click ( ) ; 
250+     await  jobCards ( page ) . first ( ) . waitFor ( ) ; 
251+     await  jobCards ( page ) . first ( ) . click ( ) ; 
142252    await  expect ( page . locator ( '#preview-modal .text-xl' ) ) . toBeVisible ( {  timeout : 10000  } ) ; 
143253
144254    await  expect ( page . locator ( '.text-xl.lg\\:leading-tight.font-stretch-condensed.font-medium.text-stone-900.lg\\:truncate.my-1\\.5.md\\:my-0' ) ) . toHaveText ( expectedTitle ) ; 
@@ -154,6 +264,39 @@ test.describe('GitJobs', () => {
154264    await  expect ( page . getByText ( 'Share this job' ) ) . toBeVisible ( ) ; 
155265  } ) ; 
156266
267+   test ( 'should display share buttons properly' ,  async  ( {  page } )  =>  { 
268+     await  jobCards ( page ) . first ( ) . waitFor ( ) ; 
269+     await  jobCards ( page ) . first ( ) . click ( ) ; 
270+     await  expect ( page . locator ( '#preview-modal .text-xl' ) ) . toBeVisible ( {  timeout : 10000  } ) ; 
271+ 
272+     const  shareButtons  =  [ 
273+       {  title : 'Twitter share link' ,  name : 'Twitter'  } , 
274+       {  title : 'Facebook share link' ,  name : 'Facebook'  } , 
275+       {  title : 'LinkedIn share link' ,  name : 'LinkedIn'  } , 
276+       {  title : 'Email share link' ,  name : 'Email'  } , 
277+       {  title : 'Copy link' ,  name : 'Copy'  } , 
278+     ] ; 
279+ 
280+     for  ( const  button  of  shareButtons )  { 
281+       const  element  =  page . getByTitle ( button . title ) ; 
282+       await  expect ( element ) . toBeVisible ( ) ; 
283+       if  ( button . title  !==  'Copy link'  &&  button . title  !==  'Email share link' )  { 
284+         const  href  =  await  element . getAttribute ( 'href' ) ; 
285+         expect ( href ) . toBeTruthy ( ) ; 
286+         expect ( href ) . toMatch ( / ^ h t t p s ? : \/ \/ / ) ; 
287+         expect ( href ) . toContain ( button . name . toLowerCase ( ) ) ; 
288+       }  else  { 
289+         if  ( button . title  ===  'Email share link' )  { 
290+           const  href  =  await  element . getAttribute ( 'href' ) ; 
291+           expect ( href ) . toBeTruthy ( ) ; 
292+           expect ( href ) . toMatch ( / ^ m a i l t o : / ) ; 
293+         }  else  { 
294+           await  expect ( element ) . toBeEnabled ( ) ; 
295+         } 
296+       } 
297+     } 
298+   } ) ; 
299+ 
157300  test ( 'should allow paginating through jobs' ,  async  ( {  page } )  =>  { 
158301    const  nextButton  =  page . getByRole ( 'link' ,  {  name : 'Next'  } ) ; 
159302    if  ( ! ( await  nextButton . isVisible ( ) ) )  { 
0 commit comments