Skip to content

Commit adb248f

Browse files
authored
Improve e2e tests (#314)
Signed-off-by: Daniel Castaño Sánchez <[email protected]>
1 parent 4e31958 commit adb248f

File tree

5 files changed

+235
-57
lines changed

5 files changed

+235
-57
lines changed

.github/workflows/e2e.yml

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,130 @@
11
name: End-to-end tests
2+
23
on:
3-
schedule:
4-
- cron: '0 */2 * * *'
4+
pull_request:
5+
branches:
6+
- main
57
workflow_dispatch:
68

79
jobs:
810
e2e-tests:
911
runs-on: ubuntu-latest
12+
env:
13+
PGHOST: localhost
14+
PGPORT: 5432
15+
PGUSER: gitjobs
16+
PGPASSWORD: password
17+
PGDATABASE: gitjobs
18+
19+
services:
20+
postgres:
21+
image: postgis/postgis:17-3.5
22+
env:
23+
POSTGRES_USER: gitjobs
24+
POSTGRES_PASSWORD: password
25+
POSTGRES_DB: gitjobs
26+
ports:
27+
- 5432:5432
28+
1029
steps:
11-
- uses: actions/checkout@v5
12-
- uses: actions/setup-node@v4
13-
- name: Install Playwright
14-
run: npm i -D @playwright/test
15-
- name: Install Playwright browsers
30+
- name: Checkout repository
31+
uses: actions/checkout@v4
32+
33+
- name: Set up Rust environment
34+
uses: dtolnay/rust-toolchain@stable
35+
with:
36+
toolchain: 1.87.0
37+
components: clippy, rustfmt
38+
39+
- name: Cache Cargo dependencies
40+
uses: actions/cache@v3
41+
with:
42+
path: |
43+
~/.cargo/bin/
44+
~/.cargo/registry/index/
45+
~/.cargo/registry/cache/
46+
~/.cargo/git/db/
47+
target/
48+
key: ${{ runner.os }}-cargo-${{ hashFiles('''**/Cargo.lock''') }}
49+
50+
- name: Set up Node.js environment
51+
uses: actions/setup-node@v4
52+
with:
53+
node-version: '20'
54+
55+
- name: Initialize npm and install dependencies
56+
run: |
57+
npm init -y
58+
npm install -D @playwright/test wait-on
59+
60+
- name: Install Playwright Browsers
1661
run: npx playwright install --with-deps
62+
63+
- name: Install Tailwind CSS
64+
run: |
65+
curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
66+
chmod +x tailwindcss-linux-x64
67+
sudo mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
68+
69+
- name: Install tern
70+
run: |
71+
curl -sL https://github.com/jackc/tern/releases/download/v2.3.2/tern_2.3.2_linux_amd64.tar.gz -o tern.tar.gz
72+
tar -xzf tern.tar.gz
73+
sudo mv tern /usr/local/bin/
74+
75+
- name: Build server
76+
run: cargo build --bin gitjobs-server
77+
78+
- name: Run database migrations
79+
working-directory: database/migrations
80+
env:
81+
TERN_CONF: ${{ github.workspace }}/database/migrations/tern.conf
82+
run: |
83+
touch tern.conf
84+
bash ./migrate.sh
85+
86+
- name: Insert test data
87+
run: |
88+
psql -f database/tests/data/e2e.sql
89+
90+
- name: Create Server Config File
91+
run: |
92+
cat <<EOF > config.testing.yml
93+
db:
94+
url: postgres://gitjobs:[email protected]:5432/gitjobs
95+
email:
96+
from_address: "[email protected]"
97+
from_name: "Test"
98+
smtp:
99+
host: "127.0.0.1"
100+
port: 2525
101+
username: "test"
102+
password: "password"
103+
log:
104+
format: "pretty"
105+
server:
106+
addr: "0.0.0.0:8080"
107+
base_url: "http://127.0.0.1:8080"
108+
cookie:
109+
secure: false
110+
login:
111+
email: true
112+
github: false
113+
linuxfoundation: false
114+
oauth2: {}
115+
oidc: {}
116+
EOF
117+
118+
- name: Start server and wait for it to be ready
119+
uses: JarvusInnovations/background-action@v1
120+
with:
121+
run: ./target/debug/gitjobs-server --config-file config.testing.yml &
122+
wait-on: http://localhost:8080
123+
tail: true
124+
log-output: "stdout,stderr"
125+
log-output-if: true
126+
17127
- name: Run Playwright tests
18128
run: npx playwright test --config tests/e2e/playwright.config.ts
129+
env:
130+
BASE_URL: http://127.0.0.1:8080

database/migrations/schema/0001_initial.sql

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
create extension pgcrypto;
2-
create extension postgis;
3-
create extension pg_trgm;
1+
create extension if not exists pgcrypto;
2+
create extension if not exists postgis;
3+
create extension if not exists pg_trgm;
44

55
create or replace function i_array_to_string(text[], text)
66
returns text language sql immutable as $$select array_to_string($1, $2)$$;

database/tests/data/e2e.sql

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
INSERT INTO "user" (user_id, auth_hash, created_at, email, email_verified, name, username, password, moderator)
2+
VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', gen_random_bytes(32), '2025-08-25 08:43:11.605766+02', '[email protected]', true, 'test', 'test', '$argon2id$v=19$m=19456,t=2,p=1$vUCLsb/lDAepJiWB7VSFNw$yAYeJVIKW0gK3cOJAnpiV9H5uPZDATJh13fDWGivjZM', true);
3+
4+
INSERT INTO employer (employer_id, company, created_at, description, public)
5+
VALUES ('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Test Inc.', '2025-08-25 09:20:05.88454+02', 'test', false);
6+
7+
INSERT INTO employer_team (user_id, employer_id, approved)
8+
VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', '18fff2d7-c794-4130-85e4-76b9d7c60b72', true);
9+
10+
INSERT INTO job (employer_id, title, description, kind, seniority, workplace, status, salary, salary_max_usd_year, salary_currency, salary_period, skills, published_at) VALUES
11+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Frontend Developer', 'React expert', 'full-time', 'senior', 'remote', 'published', 120000, 120000, 'USD', 'year', '{"React", "TypeScript", "JavaScript"}', CURRENT_TIMESTAMP),
12+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Backend Developer', 'Node.js expert', 'full-time', 'senior', 'hybrid', 'published', 130000, 130000, 'USD', 'year', '{"Node.js", "PostgreSQL", "REST"}', CURRENT_TIMESTAMP),
13+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'DevOps Engineer', 'Kubernetes expert', 'full-time', 'lead', 'on-site', 'published', 150000, 150000, 'USD', 'year', '{"Kubernetes", "Docker", "AWS"}', CURRENT_TIMESTAMP),
14+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Data Scientist', 'Python expert', 'part-time', 'mid', 'remote', 'published', 80000, 80000, 'USD', 'year', '{"Python", "Pandas", "scikit-learn"}', CURRENT_TIMESTAMP),
15+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'UI/UX Designer', 'Figma expert', 'contractor', 'junior', 'remote', 'published', 60000, 60000, 'USD', 'year', '{"Figma", "UI", "UX"}', CURRENT_TIMESTAMP),
16+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Software Engineer in Test', 'Playwright expert', 'full-time', 'mid', 'hybrid', 'published', 110000, 110000, 'USD', 'year', '{"Playwright", "TypeScript", "CI/CD"}', CURRENT_TIMESTAMP),
17+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Product Manager', 'Agile expert', 'full-time', 'senior', 'on-site', 'published', 140000, 140000, 'USD', 'year', '{"Agile", "Scrum", "Jira"}', CURRENT_TIMESTAMP),
18+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Mobile Developer', 'React Native expert', 'internship', 'entry', 'remote', 'published', 40000, 40000, 'USD', 'year', '{"React Native", "iOS", "Android"}', CURRENT_TIMESTAMP),
19+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Full-stack Developer', 'Ruby on Rails expert', 'full-time', 'mid', 'hybrid', 'published', 115000, 115000, 'USD', 'year', '{"Ruby on Rails", "PostgreSQL", "React"}', CURRENT_TIMESTAMP),
20+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Security Engineer', 'Cybersecurity expert', 'full-time', 'lead', 'on-site', 'published', 160000, 160000, 'USD', 'year', '{"Cybersecurity", "Penetration Testing", "CISSP"}', CURRENT_TIMESTAMP),
21+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 11', 'Description for Job 11', 'full-time', 'junior', 'remote', 'published', 50000, 50000, 'USD', 'year', '{"React", "JavaScript"}', CURRENT_TIMESTAMP),
22+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 12', 'Description for Job 12', 'part-time', 'mid', 'on-site', 'published', 60000, 60000, 'USD', 'year', '{"Node.js", "REST"}', CURRENT_TIMESTAMP),
23+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 13', 'Description for Job 13', 'contractor', 'senior', 'hybrid', 'published', 70000, 70000, 'USD', 'year', '{"Kubernetes", "Docker"}', CURRENT_TIMESTAMP),
24+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 14', 'Description for Job 14', 'internship', 'entry', 'remote', 'published', 30000, 30000, 'USD', 'year', '{"Python", "scikit-learn"}', CURRENT_TIMESTAMP),
25+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 15', 'Description for Job 15', 'full-time', 'lead', 'on-site', 'published', 90000, 90000, 'USD', 'year', '{"Figma", "UX"}', CURRENT_TIMESTAMP),
26+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 16', 'Description for Job 16', 'part-time', 'junior', 'hybrid', 'published', 45000, 45000, 'USD', 'year', '{"Playwright", "CI/CD"}', CURRENT_TIMESTAMP),
27+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 17', 'Description for Job 17', 'full-time', 'mid', 'remote', 'published', 65000, 65000, 'USD', 'year', '{"Agile", "Jira"}', CURRENT_TIMESTAMP),
28+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 18', 'Description for Job 18', 'contractor', 'senior', 'on-site', 'published', 75000, 75000, 'USD', 'year', '{"React Native", "Android"}', CURRENT_TIMESTAMP),
29+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 19', 'Description for Job 19', 'internship', 'entry', 'hybrid', 'published', 35000, 35000, 'USD', 'year', '{"Ruby on Rails", "React"}', CURRENT_TIMESTAMP),
30+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 20', 'Description for Job 20', 'full-time', 'lead', 'remote', 'published', 95000, 95000, 'USD', 'year', '{"Cybersecurity", "CISSP"}', CURRENT_TIMESTAMP),
31+
('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 21', 'Description for Job 21', 'full-time', 'senior', 'on-site', 'published', 100000, 100000, 'USD', 'year', '{"TypeScript", "PostgreSQL"}', CURRENT_TIMESTAMP);

tests/e2e/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default defineConfig({
44
testDir: '.',
55
failOnFlakyTests: true,
66
use: {
7-
baseURL: process.env.CI ? 'https://gitjobs.dev' : 'http://localhost:9000',
7+
baseURL: process.env.BASE_URL || 'http://localhost:9000',
88
},
99
reporter: 'list',
1010
projects: [

tests/e2e/playwright.spec.ts

Lines changed: 81 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ test.describe('GitJobs', () => {
1010
console.log(`Failed to navigate to page, retrying... (${i + 1}/3)`);
1111
}
1212
}
13-
// Handle cookie consent
14-
try {
15-
await page.getByRole('button', { name: 'Accept all' }).click({ timeout: 5000 });
16-
} catch (error) {
17-
// Ignore if the cookie consent is not visible
18-
}
1913
});
2014

2115
test('should have the correct title and heading', async ({ page }) => {
@@ -24,20 +18,13 @@ test.describe('GitJobs', () => {
2418
});
2519

2620
test('should apply a filter and verify that the results are updated', async ({ page }) => {
27-
const jobCount = await page.getByRole('button', { name: /Job type/ }).count();
28-
if (jobCount === 0) {
29-
console.log('No jobs found, skipping test.');
30-
return;
31-
}
32-
const initialJobCount = await page.getByRole('button', { name: /Job type/ }).count();
3321
await page.locator('div:nth-child(4) > div > .font-semibold').first().click();
3422
await page.locator('label').filter({ hasText: 'Full Time' }).nth(1).click();
3523
await page.waitForFunction(
36-
(initialCount) => {
37-
const currentCount = document.querySelectorAll('[role="button"][name*="Job type"]').length;
38-
return currentCount < initialCount;
39-
},
40-
initialJobCount
24+
() => {
25+
const currentCount = document.querySelectorAll('[data-preview-job="true"]').length;
26+
return currentCount === 12;
27+
}
4128
);
4229

4330
const jobCards = await page.getByRole('button', { name: /Job type/ }).all();
@@ -48,23 +35,35 @@ test.describe('GitJobs', () => {
4835
}
4936
}
5037
});
51-
5238
test('should reset filters', async ({ page }) => {
53-
const jobCount = await page.getByRole('button', { name: /Job type/ }).count();
54-
if (jobCount === 0) {
55-
console.log('No jobs found, skipping test.');
56-
return;
57-
}
58-
const initialFirstJob = await page.getByRole('button', { name: /Job type/ }).first().textContent();
59-
await page.locator('label').filter({ hasText: 'Full Time' }).nth(1).click();
39+
await page.locator('label').filter({ hasText: 'Part Time' }).nth(1).click();
40+
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();
48+
expect(firstJobAfterFilter!.trim()).toBe('Data Scientist');
6049
await page.locator('#reset-desktop-filters').click();
61-
const newFirstJob = await page.getByRole('button', { name: /Job type/ }).first().textContent();
62-
expect(newFirstJob).toEqual(initialFirstJob);
50+
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();
52+
expect(firstJobAfterReset!.trim()).toBe('Frontend Developer');
6353
});
6454

6555
test('should sort jobs', async ({ page }) => {
66-
await page.locator('#sort-desktop').selectOption('open-source');
67-
await expect(page).toHaveURL(/\?sort=open-source/);
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());
57+
await page.locator('#sort-desktop').selectOption('salary');
58+
await expect(page).toHaveURL(/\?sort=salary/);
59+
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());
61+
expect(sortedJobTitles[0]).toBe('Security Engineer');
62+
expect(sortedJobTitles[1]).toBe('DevOps Engineer');
63+
expect(sortedJobTitles[2]).toBe('Product Manager');
64+
expect(sortedJobTitles[3]).toBe('Backend Developer');
65+
expect(sortedJobTitles[4]).toBe('Frontend Developer');
66+
expect(sortedJobTitles).not.toEqual(initialJobTitles);
6867
});
6968

7069
test('should navigate to the stats page and interact with charts', async ({ page, browserName }) => {
@@ -99,34 +98,70 @@ test.describe('GitJobs', () => {
9998
await expect(page).toHaveURL(/\/sign-up/);
10099
});
101100

101+
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();
104+
await page.waitForURL('**/log-in');
105+
await page.locator('#username').fill('test');
106+
await page.locator('#password').fill('test');
107+
await page.getByRole('button', { name: 'Submit' }).click();
108+
});
109+
110+
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();
117+
await page.goto('/');
102118

119+
await page.getByRole('link', { name: 'Post a job' }).click();
120+
await page.waitForURL('**/dashboard/employer');
121+
await page.getByRole('button', { name: 'Add Job' }).click();
122+
await page.getByRole('textbox', { name: 'Title *' }).click();
123+
await page.getByRole('textbox', { name: 'Title *' }).fill('job');
124+
await page.locator('#description pre').nth(1).click();
125+
await page.locator('#description').getByRole('application').getByRole('textbox').fill('description');
126+
await page.getByRole('button', { name: 'Publish' }).click();
127+
expect(page.url()).toContain('/dashboard/employer');
128+
});
103129

104130
test('should display job details correctly', async ({ page }) => {
105-
const jobCount = await page.getByRole('button', { name: /Job type/ }).count();
106-
if (jobCount === 0) {
107-
console.log('No jobs found, skipping test.');
108-
return;
109-
}
110-
await page.getByRole('button', { name: /Job type/ }).first().click();
131+
const expectedTitle = 'Frontend Developer';
132+
const expectedDescription = 'React expert';
133+
const expectedKind = 'full time';
134+
const expectedSeniority = 'senior';
135+
const expectedWorkplace = 'remote';
136+
const expectedSalaryAmount = '120K';
137+
const expectedSalaryCurrency = 'USD';
138+
const expectedSalaryPeriod = '/ year';
139+
140+
await page.waitForSelector('[data-preview-job="true"]');
141+
await page.locator('[data-preview-job="true"]').first().click();
111142
await expect(page.locator('#preview-modal .text-xl')).toBeVisible({ timeout: 10000 });
112-
await expect(page.locator('#preview-content').getByText(/Job description/)).toBeVisible();
143+
144+
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);
145+
await expect(page.locator('div.text-lg.font-semibold.text-stone-800:has-text("Job description") + div.text-sm\\/6.text-stone-600.markdown p')).toHaveText(expectedDescription);
146+
await expect(page.locator('div:has-text("Job type") + div.flex.items-center.text-xs > div.truncate.capitalize')).toHaveText(expectedKind);
147+
await expect(page.locator('div:has-text("Workplace") + div.flex.items-center.text-xs > div.truncate.capitalize')).toHaveText(expectedWorkplace);
148+
await expect(page.locator('div:has-text("Seniority level") + div.flex.items-center.text-xs > div.truncate.capitalize')).toHaveText(expectedSeniority);
149+
await expect(page.locator('#preview-content div:has-text("Salary") div.flex.items-baseline.font-medium.text-stone-900.text-sm > div.text-xs.text-stone-500.me-1')).toHaveText(expectedSalaryCurrency);
150+
await expect(page.locator('#preview-content div:has-text("Salary") div.flex.items-baseline.font-medium.text-stone-900.text-sm')).toContainText(expectedSalaryAmount);
151+
await expect(page.locator('#preview-content div:has-text("Salary") div.flex.items-baseline > div.text-stone-900.text-xs.ms-1')).toHaveText(expectedSalaryPeriod);
113152
await expect(page.getByRole('button', { name: 'Apply' })).toBeEnabled();
114153
await expect(page.locator('#preview-content').getByText(/Published/)).toBeVisible();
115-
await expect(page.locator('#preview-content').getByText(/Job type/)).toBeVisible();
116-
await expect(page.locator('#preview-content').getByText(/Workplace/)).toBeVisible();
117-
await expect(page.locator('#preview-content').getByText(/Seniority level/)).toBeVisible();
118154
await expect(page.getByText('Share this job')).toBeVisible();
119155
});
120156

121157
test('should allow paginating through jobs', async ({ page }) => {
122-
const paginationVisible = await page.locator('[aria-label="pagination"]').isVisible();
123-
if (!paginationVisible) {
124-
console.log('Pagination not visible, skipping test.');
158+
const nextButton = page.getByRole('link', { name: 'Next' });
159+
if (!(await nextButton.isVisible())) {
160+
console.log('Pagination next button not visible, skipping test.');
125161
return;
126162
}
127-
const initialPageNumber = await page.locator('[aria-current="page"]').textContent();
128-
await page.getByLabel(/Go to page/).last().click();
129-
const newPageNumber = await page.locator('[aria-current="page"]').textContent();
130-
expect(newPageNumber).not.toBe(initialPageNumber);
163+
await nextButton.click();
164+
await expect(page).toHaveURL(/offset=20/);
165+
await expect(page.locator('#results')).toHaveText('21 - 21 of 21 results');
131166
});
132167
});

0 commit comments

Comments
 (0)