Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 173 additions & 30 deletions tests/e2e/playwright.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { test, expect } from '@playwright/test';
import {
jobCards,
jobTitles,
jobTypeButtons,
loginWithCredentials,
openLoginPage,
openSignUpPage,
openUserMenu,
searchInput,
waitForJobCount,
JOB_TITLE_SELECTOR,
} from './utils';

test.describe('GitJobs', () => {
test.beforeEach(async ({ page }) => {
for (let i = 0; i < 3; i++) {
for (let i = 0; i < 3; i++) {
try {
await page.goto('/', { timeout: 60000 });
break;
Expand All @@ -20,44 +32,91 @@ test.describe('GitJobs', () => {
test('should apply a filter and verify that the results are updated', async ({ page }) => {
await page.locator('div:nth-child(4) > div > .font-semibold').first().click();
await page.locator('label').filter({ hasText: 'Full Time' }).nth(1).click();
await page.waitForFunction(
() => {
const currentCount = document.querySelectorAll('[data-preview-job="true"]').length;
return currentCount === 12;
await waitForJobCount(page, 12);

const jobTypeButtonsList = await jobTypeButtons(page).all();
for (const jobCard of jobTypeButtonsList) {
const jobTypeElement = jobCard.locator('.capitalize').first();
if (await jobTypeElement.isVisible()) {
await expect(jobTypeElement).toHaveText('full time');
}
}
});

test('should apply multiple filters and verify that the results are updated', async ({ page }) => {
await page.locator('div:nth-child(4) > div > .font-semibold').first().click();
await page.locator('label').filter({ hasText: 'Part Time' }).nth(1).click();
await page.locator('label').filter({ hasText: 'Internship' }).nth(1).click();

await waitForJobCount(page, 6);

const jobTypeButtonsList = await jobTypeButtons(page).all();
for (const jobCard of jobTypeButtonsList) {
const jobTypeElement = jobCard.locator('.capitalize').first();
if (await jobTypeElement.isVisible()) {
const jobTypeText = await jobTypeElement.textContent();
expect(['part time', 'internship']).toContain(jobTypeText?.trim());
}
}
});

test('should search for a job and verify that the results are updated and contain the search term', async ({ page }) => {
await searchInput(page).click();
await searchInput(page).fill('Engineer');
await page.locator('#search-jobs-btn').click();

await page.waitForFunction(
({ selector, term }) => {
const nodes = Array.from(document.querySelectorAll(selector));
if (nodes.length === 0) {
return false;
}
return nodes.every(node => node.textContent?.toLowerCase().includes(term));
},
{ selector: JOB_TITLE_SELECTOR, term: 'engineer' }
);

const jobCards = await page.getByRole('button', { name: /Job type/ }).all();
for (const jobCard of jobCards) {
const jobTitleValues = await jobTitles(page).allTextContents();
for (const title of jobTitleValues) {
expect(title.trim().toLowerCase()).toContain('engineer');
}
});

test('should apply a filter and verify that the results are updated on mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.locator('#open-filters').click();
await page.waitForSelector('#drawer-filters', { state: 'visible' });
await page.locator('#drawer-filters label').filter({ hasText: 'Full Time' }).click();
await page.locator('#close-filters').click();
await page.waitForTimeout(500);

const jobTypeButtonsList = await jobTypeButtons(page).all();
for (const jobCard of jobTypeButtonsList) {
const jobTypeElement = jobCard.locator('.capitalize').first();
if (await jobTypeElement.isVisible()) {
await expect(jobTypeElement).toHaveText('full time');
}
}
});

test('should reset filters', async ({ page }) => {
await page.locator('label').filter({ hasText: 'Part Time' }).nth(1).click();

await page.waitForFunction(
() => {
const currentCount = document.querySelectorAll('[data-preview-job="true"]').length;
return currentCount === 3;
}
);
const firstJobAfterFilter = await page.locator('.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1').first().textContent();
await waitForJobCount(page, 3);
const firstJobAfterFilter = await jobTitles(page).first().textContent();
expect(firstJobAfterFilter!.trim()).toBe('Data Scientist');
await page.locator('#reset-desktop-filters').click();
await expect(page.locator('#results')).toHaveText('1 - 20 of 21 results');
const firstJobAfterReset = await page.locator('.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1').first().textContent();
const firstJobAfterReset = await jobTitles(page).first().textContent();
expect(firstJobAfterReset!.trim()).toBe('Frontend Developer');
});

test('should sort jobs', async ({ page }) => {
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());
const initialJobTitles = (await jobTitles(page).allTextContents()).map(title => title.trim());
await page.locator('#sort-desktop').selectOption('salary');
await expect(page).toHaveURL(/\?sort=salary/);
await page.waitForTimeout(500);
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());
const sortedJobTitles = (await jobTitles(page).allTextContents()).map(title => title.trim());
expect(sortedJobTitles[0]).toBe('Security Engineer');
expect(sortedJobTitles[1]).toBe('DevOps Engineer');
expect(sortedJobTitles[2]).toBe('Product Manager');
Expand All @@ -66,6 +125,49 @@ test.describe('GitJobs', () => {
expect(sortedJobTitles).not.toEqual(initialJobTitles);
});

test('ensure filters and search persist on page refresh', async ({ page }) => {
await searchInput(page).fill('Engineer');
await page.locator('label').filter({ hasText: 'Full Time' }).nth(1).click();
await page.waitForTimeout(500);

const urlBeforeRefresh = page.url();
expect(urlBeforeRefresh).toContain('Engineer');
expect(urlBeforeRefresh).toContain('full-time');

await page.reload();
await page.waitForTimeout(500);

const urlAfterRefresh = page.url();
expect(urlAfterRefresh).toBe(urlBeforeRefresh);

const persistedSearch = await searchInput(page).inputValue();
expect(persistedSearch).toBe('Engineer');

const fullTimeCheckbox = await page.locator('input[id="desktop-kind[]-full-time"]').isChecked();
expect(fullTimeCheckbox).toBe(true);
});

test('should show hover states and preview on job card interactions', async ({ page }) => {
await jobCards(page).first().waitFor();
const firstJobCard = jobCards(page).first();

// Test quick preview without opening modal
const jobTitle = await firstJobCard.locator(JOB_TITLE_SELECTOR).textContent();

// Verify job card shows basic info without modal
expect(jobTitle?.trim()).toBeTruthy();
expect(jobTitle?.trim()).toBe('Frontend Developer');

// Test hover state - verify card is hoverable
await firstJobCard.hover();
await expect(firstJobCard).toBeVisible();

// Ensure modal is not open before or after hovering
await expect(page.locator('#preview-modal')).not.toBeVisible();
await page.waitForTimeout(300);
await expect(page.locator('#preview-modal')).not.toBeVisible();
});

test('should navigate to the stats page and interact with charts', async ({ page, browserName }) => {
if (browserName === 'firefox') {
// Skip this test on Firefox as it's failing due to a rendering issue with the charts
Expand Down Expand Up @@ -93,27 +195,35 @@ test.describe('GitJobs', () => {
});

test('should navigate to the sign-up page', async ({ page }) => {
await page.locator('#user-dropdown-button').click();
await page.getByRole('link', { name: 'Sign up' }).click();
await openSignUpPage(page);
await expect(page).toHaveURL(/\/sign-up/);
});

test('should log in a user', async ({ page }) => {
await page.locator('#user-dropdown-button').click();
await page.getByRole('link', { name: 'Log in' }).click();
await loginWithCredentials(page, 'test', 'test');
});

test('should log out a user', async ({ page }) => {
await loginWithCredentials(page, 'test', 'test');

await expect(page).toHaveURL(/\/$/);
await openUserMenu(page);
await page.getByRole('link', { name: 'Log out' }).click();
await page.waitForURL('**/log-in');
});

test('invalid credentials stay on log in page', async ({ page }) => {
await openLoginPage(page);

await page.locator('#username').fill('test');
await page.locator('#password').fill('test');
await page.locator('#password').fill('wrong');
await page.getByRole('button', { name: 'Submit' }).click();

await expect(page).toHaveURL('/log-in');
});

test('should add a new job', async ({ page }) => {
await page.locator('#user-dropdown-button').click();
await page.getByRole('link', { name: 'Log in' }).click();
await page.waitForURL('**/log-in');
await page.locator('#username').fill('test');
await page.locator('#password').fill('test');
await page.getByRole('button', { name: 'Submit' }).click();
await loginWithCredentials(page, 'test', 'test');
await page.goto('/');

await page.getByRole('link', { name: 'Post a job' }).click();
Expand All @@ -137,8 +247,8 @@ test.describe('GitJobs', () => {
const expectedSalaryCurrency = 'USD';
const expectedSalaryPeriod = '/ year';

await page.waitForSelector('[data-preview-job="true"]');
await page.locator('[data-preview-job="true"]').first().click();
await jobCards(page).first().waitFor();
await jobCards(page).first().click();
await expect(page.locator('#preview-modal .text-xl')).toBeVisible({ timeout: 10000 });

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);
Expand All @@ -154,6 +264,39 @@ test.describe('GitJobs', () => {
await expect(page.getByText('Share this job')).toBeVisible();
});

test('should display share buttons properly', async ({ page }) => {
await jobCards(page).first().waitFor();
await jobCards(page).first().click();
await expect(page.locator('#preview-modal .text-xl')).toBeVisible({ timeout: 10000 });

const shareButtons = [
{ title: 'Twitter share link', name: 'Twitter' },
{ title: 'Facebook share link', name: 'Facebook' },
{ title: 'LinkedIn share link', name: 'LinkedIn' },
{ title: 'Email share link', name: 'Email' },
{ title: 'Copy link', name: 'Copy' },
];

for (const button of shareButtons) {
const element = page.getByTitle(button.title);
await expect(element).toBeVisible();
if (button.title !== 'Copy link' && button.title !== 'Email share link') {
const href = await element.getAttribute('href');
expect(href).toBeTruthy();
expect(href).toMatch(/^https?:\/\//);
expect(href).toContain(button.name.toLowerCase());
} else {
if (button.title === 'Email share link') {
const href = await element.getAttribute('href');
expect(href).toBeTruthy();
expect(href).toMatch(/^mailto:/);
} else {
await expect(element).toBeEnabled();
}
}
}
});

test('should allow paginating through jobs', async ({ page }) => {
const nextButton = page.getByRole('link', { name: 'Next' });
if (!(await nextButton.isVisible())) {
Expand Down
51 changes: 51 additions & 0 deletions tests/e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Page, Locator } from '@playwright/test';

export const HOME_PATH = '/';
export const JOB_CARD_SELECTOR = '[data-preview-job="true"]';
export const JOB_TITLE_SELECTOR =
'.text-base.font-stretch-condensed.font-medium.text-stone-900.line-clamp-2.md\\:line-clamp-1';

export const jobCards = (page: Page): Locator => page.locator(JOB_CARD_SELECTOR);

export const jobTitles = (page: Page): Locator => page.locator(JOB_TITLE_SELECTOR);

export const waitForJobCount = async (page: Page, expected: number): Promise<void> => {
await page.waitForFunction(
({ selector, count }: { selector: string; count: number }) =>
document.querySelectorAll(selector).length === count,
{ selector: JOB_CARD_SELECTOR, count: expected }
);
};

export const jobTypeButtons = (page: Page): Locator =>
page.getByRole('button', { name: /Job type/ });

export const searchInput = (page: Page): Locator =>
page.locator('input[placeholder="Search jobs"]');

export const openUserMenu = async (page: Page): Promise<void> => {
await page.locator('#user-dropdown-button').click();
};

export const openLoginPage = async (page: Page): Promise<void> => {
await openUserMenu(page);
await page.getByRole('link', { name: 'Log in' }).click();
await page.waitForURL('**/log-in');
};

export const openSignUpPage = async (page: Page): Promise<void> => {
await openUserMenu(page);
await page.getByRole('link', { name: 'Sign up' }).click();
await page.waitForURL('**/sign-up');
};

export const loginWithCredentials = async (
page: Page,
username: string,
password: string
): Promise<void> => {
await openLoginPage(page);
await page.locator('#username').fill(username);
await page.locator('#password').fill(password);
await page.getByRole('button', { name: 'Submit' }).click();
};