From 56c30bb8deac9ac003f04c103f7bce57b1c0f383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Casta=C3=B1o=20S=C3=A1nchez?= Date: Fri, 5 Sep 2025 09:47:02 +0200 Subject: [PATCH 1/6] Upgrade e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Castaño Sánchez --- .github/workflows/e2e.yml | 148 +++++++++++++++++++++-- database/migrations/insert_test_data.sql | 20 +++ tests/e2e/playwright.config.ts | 2 +- tests/e2e/playwright.spec.ts | 54 ++++++--- 4 files changed, 196 insertions(+), 28 deletions(-) create mode 100644 database/migrations/insert_test_data.sql diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4bb5984a..c270990b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,18 +1,150 @@ name: End-to-end tests + on: - schedule: - - cron: '0 */2 * * *' workflow_dispatch: jobs: - e2e-tests: + test: runs-on: ubuntu-latest + + services: + postgres: + image: postgis/postgis:17-3.5 + env: + POSTGRES_USER: gitjobs + POSTGRES_PASSWORD: password + POSTGRES_DB: gitjobs + ports: + - 5432:5432 + steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 - - name: Install Playwright - run: npm i -D @playwright/test - - name: Install Playwright browsers + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Rust environment + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Cache Cargo dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('''**/Cargo.lock''') }} + + - name: Set up Node.js environment + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Initialize npm and install dependencies + run: | + npm init -y + npm install -D @playwright/test wait-on + + - name: Install Playwright Browsers run: npx playwright install --with-deps + + - name: Install Standalone Tailwind CSS + run: | + curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 + chmod +x tailwindcss-linux-x64 + sudo mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss + + - name: Install tern + run: | + curl -sL https://github.com/jackc/tern/releases/download/v2.3.2/tern_2.3.2_linux_amd64.tar.gz -o tern.tar.gz + tar -xzf tern.tar.gz + sudo mv tern /usr/local/bin/ + + - name: Build server + run: cargo build --verbose --bin gitjobs-server + + - name: Prepare Database by Dropping Extensions + env: + PGHOST: localhost + PGPORT: 5432 + PGUSER: gitjobs + PGPASSWORD: password + PGDATABASE: gitjobs + run: | + sudo apt-get update && sudo apt-get install -y postgresql-client + psql -c "DROP EXTENSION IF EXISTS postgis CASCADE;" + psql -c "DROP EXTENSION IF EXISTS pgcrypto CASCADE;" + psql -c "DROP EXTENSION IF EXISTS pg_trgm CASCADE;" + - name: Run database migrations + working-directory: database/migrations + env: + PGHOST: localhost + PGPORT: 5432 + PGUSER: gitjobs + PGPASSWORD: password + PGDATABASE: gitjobs + TERN_CONF: ${{ github.workspace }}/database/migrations/tern.conf + run: | + touch tern.conf + bash ./migrate.sh + + - name: Insert test data + env: + PGHOST: localhost + PGPORT: 5432 + PGUSER: gitjobs + PGPASSWORD: password + PGDATABASE: gitjobs + run: | + psql -f database/migrations/insert_test_data.sql + + - name: Create Server Config File + run: | + cat < config.testing.yml + db: + url: postgres://gitjobs:password@127.0.0.1:5432/gitjobs + email: + from_address: "test@example.com" + from_name: "Test" + smtp: + host: "127.0.0.1" + port: 2525 + username: "test" + password: "password" + log: + format: "pretty" + server: + addr: "0.0.0.0:8080" + base_url: "http://127.0.0.1:8080" + cookie: + secure: false + login: + email: true + github: false + linuxfoundation: false + oauth2: + github: + auth_url: "https://github.com/login/oauth/authorize" + client_id: "dummy_client_id" + client_secret: "dummy_client_secret" + redirect_uri: "http://127.0.0.1:8080/auth/github/callback" + scopes: ["read:user", "user:email"] + token_url: "https://github.com/login/oauth/access_token" + oidc: {} + EOF + + - name: Start server and wait for it to be ready + uses: JarvusInnovations/background-action@v1 + with: + run: ./target/debug/gitjobs-server --config-file config.testing.yml & + wait-on: http://localhost:8080 + tail: true + log-output: "stdout,stderr" + log-output-if: true + - name: Run Playwright tests run: npx playwright test --config tests/e2e/playwright.config.ts + env: + BASE_URL: http://127.0.0.1:8080 diff --git a/database/migrations/insert_test_data.sql b/database/migrations/insert_test_data.sql new file mode 100644 index 00000000..e0645414 --- /dev/null +++ b/database/migrations/insert_test_data.sql @@ -0,0 +1,20 @@ +INSERT INTO "user" (user_id, auth_hash, created_at, email, email_verified, name, username, password, moderator) +VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', gen_random_bytes(32), '2025-08-25 08:43:11.605766+02', 'test@t.com', true, 'test', 'test', '$argon2id$v=19$m=19456,t=2,p=1$vUCLsb/lDAepJiWB7VSFNw$yAYeJVIKW0gK3cOJAnpiV9H5uPZDATJh13fDWGivjZM', true); + +INSERT INTO employer (employer_id, company, created_at, description, public) +VALUES ('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Test Inc.', '2025-08-25 09:20:05.88454+02', 'test', false); + +INSERT INTO employer_team (user_id, employer_id, approved) +VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', '18fff2d7-c794-4130-85e4-76b9d7c60b72', true); + +INSERT INTO job (employer_id, title, description, kind, seniority, workplace, status, salary, salary_currency, salary_period, skills, published_at) VALUES +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Frontend Developer', 'React expert', 'full-time', 'senior', 'remote', 'published', 120000, 'USD', 'year', '{"React", "TypeScript", "JavaScript"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Backend Developer', 'Node.js expert', 'full-time', 'senior', 'hybrid', 'published', 130000, 'USD', 'year', '{"Node.js", "PostgreSQL", "REST"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'DevOps Engineer', 'Kubernetes expert', 'full-time', 'lead', 'on-site', 'published', 150000, 'USD', 'year', '{"Kubernetes", "Docker", "AWS"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Data Scientist', 'Python expert', 'part-time', 'mid', 'remote', 'published', 80000, 'USD', 'year', '{"Python", "Pandas", "scikit-learn"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'UI/UX Designer', 'Figma expert', 'contractor', 'junior', 'remote', 'published', 60000, 'USD', 'year', '{"Figma", "UI", "UX"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Software Engineer in Test', 'Playwright expert', 'full-time', 'mid', 'hybrid', 'published', 110000, 'USD', 'year', '{"Playwright", "TypeScript", "CI/CD"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Product Manager', 'Agile expert', 'full-time', 'senior', 'on-site', 'published', 140000, 'USD', 'year', '{"Agile", "Scrum", "Jira"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Mobile Developer', 'React Native expert', 'internship', 'entry', 'remote', 'published', 40000, 'USD', 'year', '{"React Native", "iOS", "Android"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Full-stack Developer', 'Ruby on Rails expert', 'full-time', 'mid', 'hybrid', 'published', 115000, 'USD', 'year', '{"Ruby on Rails", "PostgreSQL", "React"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Security Engineer', 'Cybersecurity expert', 'full-time', 'lead', 'on-site', 'published', 160000, 'USD', 'year', '{"Cybersecurity", "Penetration Testing", "CISSP"}', CURRENT_TIMESTAMP); diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index b8b6c4de..b26da0d2 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ testDir: '.', failOnFlakyTests: true, use: { - baseURL: process.env.CI ? 'https://gitjobs.dev' : 'http://localhost:9000', + baseURL: process.env.BASE_URL || 'http://localhost:9000', }, reporter: 'list', projects: [ diff --git a/tests/e2e/playwright.spec.ts b/tests/e2e/playwright.spec.ts index 9b157f7e..1099862a 100644 --- a/tests/e2e/playwright.spec.ts +++ b/tests/e2e/playwright.spec.ts @@ -10,12 +10,6 @@ test.describe('GitJobs', () => { console.log(`Failed to navigate to page, retrying... (${i + 1}/3)`); } } - // Handle cookie consent - try { - await page.getByRole('button', { name: 'Accept all' }).click({ timeout: 5000 }); - } catch (error) { - // Ignore if the cookie consent is not visible - } }); test('should have the correct title and heading', async ({ page }) => { @@ -50,15 +44,12 @@ test.describe('GitJobs', () => { }); test('should reset filters', async ({ page }) => { - const jobCount = await page.getByRole('button', { name: /Job type/ }).count(); - if (jobCount === 0) { - console.log('No jobs found, skipping test.'); - return; - } - const initialFirstJob = await page.getByRole('button', { name: /Job type/ }).first().textContent(); + await page.waitForSelector('[data-preview-job="true"]'); + const initialFirstJob = await page.locator('[data-preview-job="true"]').first().textContent(); + 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.locator('#reset-desktop-filters').click(); - const newFirstJob = await page.getByRole('button', { name: /Job type/ }).first().textContent(); + const newFirstJob = await page.locator('[data-preview-job="true"]').first().textContent(); expect(newFirstJob).toEqual(initialFirstJob); }); @@ -99,15 +90,40 @@ test.describe('GitJobs', () => { 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 page.waitForURL('**/log-in'); + await page.locator('#username').fill('test'); + await page.locator('#password').fill('test'); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + test('should add a new job', async ({ page }) => { + // Log in first + 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 page.goto('/'); + + // Add a new job + await page.getByRole('link', { name: 'Post a job' }).click(); + await page.waitForURL('**/dashboard/employer'); + await page.getByRole('button', { name: 'Add Job' }).click(); + await page.getByRole('textbox', { name: 'Title *' }).click(); + await page.getByRole('textbox', { name: 'Title *' }).fill('job'); + await page.locator('#description pre').nth(1).click(); + await page.locator('#description').getByRole('application').getByRole('textbox').fill('description'); + await page.getByRole('button', { name: 'Publish' }).click(); + expect(page.url()).toContain('/dashboard/employer'); + }); test('should display job details correctly', async ({ page }) => { - const jobCount = await page.getByRole('button', { name: /Job type/ }).count(); - if (jobCount === 0) { - console.log('No jobs found, skipping test.'); - return; - } - await page.getByRole('button', { name: /Job type/ }).first().click(); + await page.waitForSelector('[data-preview-job="true"]'); + await page.locator('[data-preview-job="true"]').first().click(); await expect(page.locator('#preview-modal .text-xl')).toBeVisible({ timeout: 10000 }); await expect(page.locator('#preview-content').getByText(/Job description/)).toBeVisible(); await expect(page.getByRole('button', { name: 'Apply' })).toBeEnabled(); From 7c0e4abc883e289f5d31b25148b0e1998c74e664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Casta=C3=B1o=20S=C3=A1nchez?= Date: Fri, 5 Sep 2025 17:24:09 +0200 Subject: [PATCH 2/6] Upgrade e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Castaño Sánchez --- .github/workflows/e2e.yml | 55 ++++--------- database/migrations/insert_test_data.sql | 20 ----- database/migrations/schema/0001_initial.sql | 6 +- database/tests/data/e2e.sql | 31 ++++++++ tests/e2e/playwright.spec.ts | 85 +++++++++++++-------- 5 files changed, 99 insertions(+), 98 deletions(-) delete mode 100644 database/migrations/insert_test_data.sql create mode 100644 database/tests/data/e2e.sql diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c270990b..7a039a64 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,6 +6,12 @@ on: jobs: test: runs-on: ubuntu-latest + env: + PGHOST: localhost + PGPORT: 5432 + PGUSER: gitjobs + PGPASSWORD: password + PGDATABASE: gitjobs services: postgres: @@ -24,7 +30,9 @@ jobs: - name: Set up Rust environment uses: dtolnay/rust-toolchain@stable with: - toolchain: stable + toolchain: 1.87.0 + components: clippy, rustfmt + - name: Cache Cargo dependencies uses: actions/cache@v3 @@ -63,56 +71,26 @@ jobs: sudo mv tern /usr/local/bin/ - name: Build server - run: cargo build --verbose --bin gitjobs-server + run: cargo build --bin gitjobs-server + - - name: Prepare Database by Dropping Extensions - env: - PGHOST: localhost - PGPORT: 5432 - PGUSER: gitjobs - PGPASSWORD: password - PGDATABASE: gitjobs - run: | - sudo apt-get update && sudo apt-get install -y postgresql-client - psql -c "DROP EXTENSION IF EXISTS postgis CASCADE;" - psql -c "DROP EXTENSION IF EXISTS pgcrypto CASCADE;" - psql -c "DROP EXTENSION IF EXISTS pg_trgm CASCADE;" - name: Run database migrations working-directory: database/migrations env: - PGHOST: localhost - PGPORT: 5432 - PGUSER: gitjobs - PGPASSWORD: password - PGDATABASE: gitjobs TERN_CONF: ${{ github.workspace }}/database/migrations/tern.conf run: | touch tern.conf bash ./migrate.sh - name: Insert test data - env: - PGHOST: localhost - PGPORT: 5432 - PGUSER: gitjobs - PGPASSWORD: password - PGDATABASE: gitjobs run: | - psql -f database/migrations/insert_test_data.sql + psql -f database/tests/data/e2e.sql - name: Create Server Config File run: | cat < config.testing.yml db: url: postgres://gitjobs:password@127.0.0.1:5432/gitjobs - email: - from_address: "test@example.com" - from_name: "Test" - smtp: - host: "127.0.0.1" - port: 2525 - username: "test" - password: "password" log: format: "pretty" server: @@ -124,14 +102,7 @@ jobs: email: true github: false linuxfoundation: false - oauth2: - github: - auth_url: "https://github.com/login/oauth/authorize" - client_id: "dummy_client_id" - client_secret: "dummy_client_secret" - redirect_uri: "http://127.0.0.1:8080/auth/github/callback" - scopes: ["read:user", "user:email"] - token_url: "https://github.com/login/oauth/access_token" + oauth2: {} oidc: {} EOF diff --git a/database/migrations/insert_test_data.sql b/database/migrations/insert_test_data.sql deleted file mode 100644 index e0645414..00000000 --- a/database/migrations/insert_test_data.sql +++ /dev/null @@ -1,20 +0,0 @@ -INSERT INTO "user" (user_id, auth_hash, created_at, email, email_verified, name, username, password, moderator) -VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', gen_random_bytes(32), '2025-08-25 08:43:11.605766+02', 'test@t.com', true, 'test', 'test', '$argon2id$v=19$m=19456,t=2,p=1$vUCLsb/lDAepJiWB7VSFNw$yAYeJVIKW0gK3cOJAnpiV9H5uPZDATJh13fDWGivjZM', true); - -INSERT INTO employer (employer_id, company, created_at, description, public) -VALUES ('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Test Inc.', '2025-08-25 09:20:05.88454+02', 'test', false); - -INSERT INTO employer_team (user_id, employer_id, approved) -VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', '18fff2d7-c794-4130-85e4-76b9d7c60b72', true); - -INSERT INTO job (employer_id, title, description, kind, seniority, workplace, status, salary, salary_currency, salary_period, skills, published_at) VALUES -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Frontend Developer', 'React expert', 'full-time', 'senior', 'remote', 'published', 120000, 'USD', 'year', '{"React", "TypeScript", "JavaScript"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Backend Developer', 'Node.js expert', 'full-time', 'senior', 'hybrid', 'published', 130000, 'USD', 'year', '{"Node.js", "PostgreSQL", "REST"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'DevOps Engineer', 'Kubernetes expert', 'full-time', 'lead', 'on-site', 'published', 150000, 'USD', 'year', '{"Kubernetes", "Docker", "AWS"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Data Scientist', 'Python expert', 'part-time', 'mid', 'remote', 'published', 80000, 'USD', 'year', '{"Python", "Pandas", "scikit-learn"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'UI/UX Designer', 'Figma expert', 'contractor', 'junior', 'remote', 'published', 60000, 'USD', 'year', '{"Figma", "UI", "UX"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Software Engineer in Test', 'Playwright expert', 'full-time', 'mid', 'hybrid', 'published', 110000, 'USD', 'year', '{"Playwright", "TypeScript", "CI/CD"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Product Manager', 'Agile expert', 'full-time', 'senior', 'on-site', 'published', 140000, 'USD', 'year', '{"Agile", "Scrum", "Jira"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Mobile Developer', 'React Native expert', 'internship', 'entry', 'remote', 'published', 40000, 'USD', 'year', '{"React Native", "iOS", "Android"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Full-stack Developer', 'Ruby on Rails expert', 'full-time', 'mid', 'hybrid', 'published', 115000, 'USD', 'year', '{"Ruby on Rails", "PostgreSQL", "React"}', CURRENT_TIMESTAMP), -('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Security Engineer', 'Cybersecurity expert', 'full-time', 'lead', 'on-site', 'published', 160000, 'USD', 'year', '{"Cybersecurity", "Penetration Testing", "CISSP"}', CURRENT_TIMESTAMP); diff --git a/database/migrations/schema/0001_initial.sql b/database/migrations/schema/0001_initial.sql index 7675f934..a97b7f55 100644 --- a/database/migrations/schema/0001_initial.sql +++ b/database/migrations/schema/0001_initial.sql @@ -1,6 +1,6 @@ -create extension pgcrypto; -create extension postgis; -create extension pg_trgm; +create extension if not exists pgcrypto; +create extension if not exists postgis; +create extension if not exists pg_trgm; create or replace function i_array_to_string(text[], text) returns text language sql immutable as $$select array_to_string($1, $2)$$; diff --git a/database/tests/data/e2e.sql b/database/tests/data/e2e.sql new file mode 100644 index 00000000..2d5631eb --- /dev/null +++ b/database/tests/data/e2e.sql @@ -0,0 +1,31 @@ +INSERT INTO "user" (user_id, auth_hash, created_at, email, email_verified, name, username, password, moderator) +VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', gen_random_bytes(32), '2025-08-25 08:43:11.605766+02', 'test@t.com', true, 'test', 'test', '$argon2id$v=19$m=19456,t=2,p=1$vUCLsb/lDAepJiWB7VSFNw$yAYeJVIKW0gK3cOJAnpiV9H5uPZDATJh13fDWGivjZM', true); + +INSERT INTO employer (employer_id, company, created_at, description, public) +VALUES ('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Test Inc.', '2025-08-25 09:20:05.88454+02', 'test', false); + +INSERT INTO employer_team (user_id, employer_id, approved) +VALUES ('f39a95c8-9903-4537-8873-2d81bfb86b35', '18fff2d7-c794-4130-85e4-76b9d7c60b72', true); + +INSERT INTO job (employer_id, title, description, kind, seniority, workplace, status, salary, salary_max_usd_year, salary_currency, salary_period, skills, published_at) VALUES +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Frontend Developer', 'React expert', 'full-time', 'senior', 'remote', 'published', 120000, 120000, 'USD', 'year', '{"React", "TypeScript", "JavaScript"}', CURRENT_TIMESTAMP), +('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), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'DevOps Engineer', 'Kubernetes expert', 'full-time', 'lead', 'on-site', 'published', 150000, 150000, 'USD', 'year', '{"Kubernetes", "Docker", "AWS"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Data Scientist', 'Python expert', 'part-time', 'mid', 'remote', 'published', 80000, 80000, 'USD', 'year', '{"Python", "Pandas", "scikit-learn"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'UI/UX Designer', 'Figma expert', 'contractor', 'junior', 'remote', 'published', 60000, 60000, 'USD', 'year', '{"Figma", "UI", "UX"}', CURRENT_TIMESTAMP), +('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), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Product Manager', 'Agile expert', 'full-time', 'senior', 'on-site', 'published', 140000, 140000, 'USD', 'year', '{"Agile", "Scrum", "Jira"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Mobile Developer', 'React Native expert', 'internship', 'entry', 'remote', 'published', 40000, 40000, 'USD', 'year', '{"React Native", "iOS", "Android"}', CURRENT_TIMESTAMP), +('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), +('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), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 11', 'Description for Job 11', 'full-time', 'junior', 'remote', 'published', 50000, 50000, 'USD', 'year', '{"React", "JavaScript"}', CURRENT_TIMESTAMP), +('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), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 13', 'Description for Job 13', 'contractor', 'senior', 'hybrid', 'published', 70000, 70000, 'USD', 'year', '{"Kubernetes", "Docker"}', CURRENT_TIMESTAMP), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 14', 'Description for Job 14', 'internship', 'entry', 'remote', 'published', 30000, 30000, 'USD', 'year', '{"Python", "scikit-learn"}', CURRENT_TIMESTAMP), +('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), +('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), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 17', 'Description for Job 17', 'full-time', 'mid', 'remote', 'published', 65000, 65000, 'USD', 'year', '{"Agile", "Jira"}', CURRENT_TIMESTAMP), +('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), +('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), +('18fff2d7-c794-4130-85e4-76b9d7c60b72', 'Job 20', 'Description for Job 20', 'full-time', 'lead', 'remote', 'published', 95000, 95000, 'USD', 'year', '{"Cybersecurity", "CISSP"}', CURRENT_TIMESTAMP), +('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); \ No newline at end of file diff --git a/tests/e2e/playwright.spec.ts b/tests/e2e/playwright.spec.ts index 1099862a..91038a64 100644 --- a/tests/e2e/playwright.spec.ts +++ b/tests/e2e/playwright.spec.ts @@ -18,20 +18,13 @@ test.describe('GitJobs', () => { }); test('should apply a filter and verify that the results are updated', async ({ page }) => { - const jobCount = await page.getByRole('button', { name: /Job type/ }).count(); - if (jobCount === 0) { - console.log('No jobs found, skipping test.'); - return; - } - const initialJobCount = await page.getByRole('button', { name: /Job type/ }).count(); 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( - (initialCount) => { - const currentCount = document.querySelectorAll('[role="button"][name*="Job type"]').length; - return currentCount < initialCount; - }, - initialJobCount + () => { + const currentCount = document.querySelectorAll('[data-preview-job="true"]').length; + return currentCount === 12; + } ); const jobCards = await page.getByRole('button', { name: /Job type/ }).all(); @@ -42,20 +35,35 @@ test.describe('GitJobs', () => { } } }); - test('should reset filters', async ({ page }) => { - await page.waitForSelector('[data-preview-job="true"]'); - const initialFirstJob = await page.locator('[data-preview-job="true"]').first().textContent(); - 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.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(); + expect(firstJobAfterFilter!.trim()).toBe('Data Scientist'); await page.locator('#reset-desktop-filters').click(); - const newFirstJob = await page.locator('[data-preview-job="true"]').first().textContent(); - expect(newFirstJob).toEqual(initialFirstJob); + 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(); + expect(firstJobAfterReset!.trim()).toBe('Frontend Developer'); }); test('should sort jobs', async ({ page }) => { - await page.locator('#sort-desktop').selectOption('open-source'); - await expect(page).toHaveURL(/\?sort=open-source/); + 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()); + 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()); + expect(sortedJobTitles[0]).toBe('Security Engineer'); + expect(sortedJobTitles[1]).toBe('DevOps Engineer'); + expect(sortedJobTitles[2]).toBe('Product Manager'); + expect(sortedJobTitles[3]).toBe('Backend Developer'); + expect(sortedJobTitles[4]).toBe('Frontend Developer'); + expect(sortedJobTitles).not.toEqual(initialJobTitles); }); test('should navigate to the stats page and interact with charts', async ({ page, browserName }) => { @@ -100,7 +108,6 @@ test.describe('GitJobs', () => { }); test('should add a new job', async ({ page }) => { - // Log in first await page.locator('#user-dropdown-button').click(); await page.getByRole('link', { name: 'Log in' }).click(); await page.waitForURL('**/log-in'); @@ -109,7 +116,6 @@ test.describe('GitJobs', () => { await page.getByRole('button', { name: 'Submit' }).click(); await page.goto('/'); - // Add a new job await page.getByRole('link', { name: 'Post a job' }).click(); await page.waitForURL('**/dashboard/employer'); await page.getByRole('button', { name: 'Add Job' }).click(); @@ -122,27 +128,40 @@ test.describe('GitJobs', () => { }); test('should display job details correctly', async ({ page }) => { + const expectedTitle = 'Frontend Developer'; + const expectedDescription = 'React expert'; + const expectedKind = 'full time'; + const expectedSeniority = 'senior'; + const expectedWorkplace = 'remote'; + const expectedSalaryAmount = '120K'; + const expectedSalaryCurrency = 'USD'; + const expectedSalaryPeriod = '/ year'; + await page.waitForSelector('[data-preview-job="true"]'); await page.locator('[data-preview-job="true"]').first().click(); await expect(page.locator('#preview-modal .text-xl')).toBeVisible({ timeout: 10000 }); - await expect(page.locator('#preview-content').getByText(/Job description/)).toBeVisible(); + + 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); + 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); + await expect(page.locator('div:has-text("Job type") + div.flex.items-center.text-xs > div.truncate.capitalize')).toHaveText(expectedKind); + await expect(page.locator('div:has-text("Workplace") + div.flex.items-center.text-xs > div.truncate.capitalize')).toHaveText(expectedWorkplace); + await expect(page.locator('div:has-text("Seniority level") + div.flex.items-center.text-xs > div.truncate.capitalize')).toHaveText(expectedSeniority); + 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); + await expect(page.locator('#preview-content div:has-text("Salary") div.flex.items-baseline.font-medium.text-stone-900.text-sm')).toContainText(expectedSalaryAmount); + await expect(page.locator('#preview-content div:has-text("Salary") div.flex.items-baseline > div.text-stone-900.text-xs.ms-1')).toHaveText(expectedSalaryPeriod); await expect(page.getByRole('button', { name: 'Apply' })).toBeEnabled(); await expect(page.locator('#preview-content').getByText(/Published/)).toBeVisible(); - await expect(page.locator('#preview-content').getByText(/Job type/)).toBeVisible(); - await expect(page.locator('#preview-content').getByText(/Workplace/)).toBeVisible(); - await expect(page.locator('#preview-content').getByText(/Seniority level/)).toBeVisible(); await expect(page.getByText('Share this job')).toBeVisible(); }); test('should allow paginating through jobs', async ({ page }) => { - const paginationVisible = await page.locator('[aria-label="pagination"]').isVisible(); - if (!paginationVisible) { - console.log('Pagination not visible, skipping test.'); + const nextButton = page.getByRole('link', { name: 'Next' }); + if (!(await nextButton.isVisible())) { + console.log('Pagination next button not visible, skipping test.'); return; } - const initialPageNumber = await page.locator('[aria-current="page"]').textContent(); - await page.getByLabel(/Go to page/).last().click(); - const newPageNumber = await page.locator('[aria-current="page"]').textContent(); - expect(newPageNumber).not.toBe(initialPageNumber); + await nextButton.click(); + await expect(page).toHaveURL(/offset=20/); + await expect(page.locator('#results')).toHaveText('21 - 21 of 21 results'); }); }); From 810cc3ca02bbabf098f0bd059de8e38a9d0ac16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Casta=C3=B1o=20S=C3=A1nchez?= Date: Fri, 5 Sep 2025 17:35:25 +0200 Subject: [PATCH 3/6] Fix e2e workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Castaño Sánchez --- .github/workflows/e2e.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7a039a64..0525f647 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -91,6 +91,7 @@ jobs: cat < config.testing.yml db: url: postgres://gitjobs:password@127.0.0.1:5432/gitjobs + email: {} log: format: "pretty" server: From 00ef8345e3f76ebe64a943fdb1bdb1afe304b3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Casta=C3=B1o=20S=C3=A1nchez?= Date: Fri, 5 Sep 2025 17:42:30 +0200 Subject: [PATCH 4/6] Fix e2e workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Castaño Sánchez --- .github/workflows/e2e.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0525f647..5984f89e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -91,7 +91,14 @@ jobs: cat < config.testing.yml db: url: postgres://gitjobs:password@127.0.0.1:5432/gitjobs - email: {} + email: + from_address: "test@example.com" + from_name: "Test" + smtp: + host: "127.0.0.1" + port: 2525 + username: "test" + password: "password" log: format: "pretty" server: From da6394dd92a27dbaf461af1955a877240c5e0ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Casta=C3=B1o=20S=C3=A1nchez?= Date: Mon, 8 Sep 2025 13:35:49 +0200 Subject: [PATCH 5/6] Improve e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Castaño Sánchez --- .github/workflows/e2e.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5984f89e..158c7ad5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,6 +1,9 @@ name: End-to-end tests on: + pull_request: + branches: + - main workflow_dispatch: jobs: @@ -33,7 +36,6 @@ jobs: toolchain: 1.87.0 components: clippy, rustfmt - - name: Cache Cargo dependencies uses: actions/cache@v3 with: @@ -73,7 +75,6 @@ jobs: - name: Build server run: cargo build --bin gitjobs-server - - name: Run database migrations working-directory: database/migrations env: From 3f97e23b5fa0fd5465ba3a6af1cb0e1245c3d3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Casta=C3=B1o=20S=C3=A1nchez?= Date: Mon, 8 Sep 2025 13:57:19 +0200 Subject: [PATCH 6/6] Fix minor issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Castaño Sánchez --- .github/workflows/e2e.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 158c7ad5..eb4b74fe 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: jobs: - test: + e2e-tests: runs-on: ubuntu-latest env: PGHOST: localhost @@ -60,7 +60,7 @@ jobs: - name: Install Playwright Browsers run: npx playwright install --with-deps - - name: Install Standalone Tailwind CSS + - name: Install Tailwind CSS run: | curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 chmod +x tailwindcss-linux-x64