Skip to content

Commit f23c389

Browse files
authored
Add some more e2e tests (#335)
Signed-off-by: Cintia Sánchez García <[email protected]>
1 parent 09ad405 commit f23c389

File tree

2 files changed

+224
-30
lines changed

2 files changed

+224
-30
lines changed

tests/e2e/playwright.spec.ts

Lines changed: 173 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
import { 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

315
test.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: /Job type/ }).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(/\?sort=salary/);
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(/\/sign-up/);
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(/^https?:\/\//);
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(/^mailto:/);
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())) {

tests/e2e/utils.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Page, Locator } from '@playwright/test';
2+
3+
export const HOME_PATH = '/';
4+
export const JOB_CARD_SELECTOR = '[data-preview-job="true"]';
5+
export const JOB_TITLE_SELECTOR =
6+
'.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1';
7+
8+
export const jobCards = (page: Page): Locator => page.locator(JOB_CARD_SELECTOR);
9+
10+
export const jobTitles = (page: Page): Locator => page.locator(JOB_TITLE_SELECTOR);
11+
12+
export const waitForJobCount = async (page: Page, expected: number): Promise<void> => {
13+
await page.waitForFunction(
14+
({ selector, count }: { selector: string; count: number }) =>
15+
document.querySelectorAll(selector).length === count,
16+
{ selector: JOB_CARD_SELECTOR, count: expected }
17+
);
18+
};
19+
20+
export const jobTypeButtons = (page: Page): Locator =>
21+
page.getByRole('button', { name: /Job type/ });
22+
23+
export const searchInput = (page: Page): Locator =>
24+
page.locator('input[placeholder="Search jobs"]');
25+
26+
export const openUserMenu = async (page: Page): Promise<void> => {
27+
await page.locator('#user-dropdown-button').click();
28+
};
29+
30+
export const openLoginPage = async (page: Page): Promise<void> => {
31+
await openUserMenu(page);
32+
await page.getByRole('link', { name: 'Log in' }).click();
33+
await page.waitForURL('**/log-in');
34+
};
35+
36+
export const openSignUpPage = async (page: Page): Promise<void> => {
37+
await openUserMenu(page);
38+
await page.getByRole('link', { name: 'Sign up' }).click();
39+
await page.waitForURL('**/sign-up');
40+
};
41+
42+
export const loginWithCredentials = async (
43+
page: Page,
44+
username: string,
45+
password: string
46+
): Promise<void> => {
47+
await openLoginPage(page);
48+
await page.locator('#username').fill(username);
49+
await page.locator('#password').fill(password);
50+
await page.getByRole('button', { name: 'Submit' }).click();
51+
};

0 commit comments

Comments
 (0)