Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Playwright end-to-end tests #928

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9bbab46
feat(frontend:e2e): add Playwright end-to-end tests
ErvinRacz Feb 3, 2025
5c28469
feat(frontend:e2e): increase server startup timeout to 120 seconds
ErvinRacz Feb 3, 2025
126413d
feat(frontend:e2e): add debug logs and increase timeout
ErvinRacz Feb 3, 2025
363ab97
chore: remove debug logs from GitHub Actions workflow
ErvinRacz Feb 3, 2025
c9c36e7
fix(frontend:e2e:ci): update path for Playwright report in GitHub Act…
ErvinRacz Feb 3, 2025
d7c62d6
feat(ci): deploy Playwright report to GitHub Pages
ErvinRacz Feb 3, 2025
c3b88b9
chore(playwright): remove unnecessary docker logs command in webServe…
ErvinRacz Feb 3, 2025
55e4ad7
refactor(frontend:e2e:ci): update Playwright workflow
ErvinRacz Feb 3, 2025
b7e142f
test(ci): see if the playwright workflow gets triggered
ErvinRacz Feb 3, 2025
062e122
fix(ci): Playwright workflow
ErvinRacz Feb 4, 2025
39aac54
fix(ci): ensure deploy-reports job always runs after test execution
ErvinRacz Feb 4, 2025
0f3f3d5
fix(ci): correct job name and artifact path in Playwright workflow
ErvinRacz Feb 4, 2025
00c661b
fix(frontend:e2e:ci): correct permissions in Playwright workflow
ErvinRacz Feb 4, 2025
34bb3fe
refactor(frontend:e2e): add style to the page to limit the width of
ErvinRacz Feb 5, 2025
415bb93
test: increase maxDiffPixels for screenshot comparisons
ErvinRacz Feb 5, 2025
632ec5a
chore(frontend:e2e): increase maxDiffPixels
ErvinRacz Feb 5, 2025
36455c4
docs: add Playwright testing guide
ErvinRacz Feb 5, 2025
2970783
fixup! docs: add Playwright testing guide
ErvinRacz Feb 5, 2025
6b53b0f
Update frontend/e2e/instance.spec.ts
ErvinRacz Feb 5, 2025
f279c3b
Update frontend/e2e/global.teardown.ts
ErvinRacz Feb 5, 2025
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
47 changes: 47 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 3
- name: Playwright Inspect Test Summary
run: |
echo "🔍 To check test errors in GitHub Actions:"
echo ""
echo "1️⃣ **Download the HTML Report:**"
echo " - Go to the **Artifacts** section in GitHub Actions."
echo " - Click on **playwright-report** to download the ZIP file."
echo ""
echo "2️⃣ **View the Report:**"
echo " - Extract the ZIP file in a folder with Playwright installed."
echo " - Run: **npx playwright show-report <extracted-folder-name>**"
echo ""
echo "3️⃣ **Analyze Failures with Trace Viewer:**"
echo " - Open the report and click the **trace** icon next to a failed test."
echo " - Inspect each test action to diagnose the issue."
echo ""
echo "📖 More details: https://playwright.dev/docs/test-reporters#html-reporter"
6 changes: 6 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ yarn-error.log*
node_modules
yarn-error.log
src/js/icons/*.json

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
52 changes: 52 additions & 0 deletions frontend/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## 🏆 Playwright Testing Guide

### 📦 Install Dependencies
```sh
cd frontend
npm install
npx playwright install
```

### 🚀 Run Tests
```sh
npx playwright test # Run all tests
npx playwright test file.spec.ts # Run specific test file
npx playwright test --ui # Run in UI mode
npx playwright test --ui --update-snapshots # To update snapshots
```

### 🛠 Develop Tests
Create a new test in `e2e/`:
```ts
import { test, expect } from '@playwright/test';

test('example test', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
});
```
Run the test:
```sh
npx playwright test e2e/example.spec.ts
```

### 🔍 Debugging
- Use `--debug` to pause execution.
- Open the **HTML report**:
```sh
npx playwright show-report
```
- Enable tracing:
```sh
npx playwright test --trace on
```

### 🔄 GitHub Actions
1. Push changes to trigger CI tests.
2. Download **playwright-report** from GitHub Actions artifacts.
3. Extract and view the report:
```sh
npx playwright show-report extracted-folder-name
```

📖 More: [Playwright Docs](https://playwright.dev)
62 changes: 62 additions & 0 deletions frontend/e2e/application.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, test } from '@playwright/test';
import { createApplication, deleteApplication, generateSalt } from './helpers';


test.describe('Applications', () => {

let appName: string; let appId: string;

test.beforeEach(async ({ page }, testInfo): Promise<void> => {
const appNameSalt = generateSalt(testInfo.title);
appName = "Test app" + appNameSalt;
appId = "io.test.app." + appNameSalt;

await page.goto('http://localhost:8002/');
});

test.afterEach(async ({ page }) => {
await deleteApplication(page, appName);

await expect(page.getByRole('list')).not.toContainText(appName);
await expect(page.getByRole('list')).not.toContainText(appId);
});

test('should create new application', async ({ page }) => {
await createApplication(page, appName, appId);

await expect(page.getByRole('list')).toContainText(appName);
await expect(page.getByRole('list')).toContainText(appId);
});

test('should not allow the creation of applications with existing id', async ({ page }) => {
await createApplication(page, appName, appId);
await createApplication(page, appName, appId);

await expect(page.getByRole('paragraph').filter({ hasText: 'Something went wrong. Check the form or try again' })).toHaveCount(1);
await page.getByRole('button', { name: 'Cancel' }).click();
});

test('should edit an existing application', async ({ page }) => {
await createApplication(page, appName, appId);

await page.locator('li').filter({ hasText: appName }).getByTestId('more-menu-open-button').click();
await page.getByRole('menuitem', { name: 'Edit' }).click();
await page.locator('input[name="name"]').click();
await page.locator('input[name="name"]').fill('Test app modified');
await page.locator('input[name="name"]').press('Tab');
await page.locator('input[name="product_id"]').press('ArrowRight');
await page.locator('input[name="product_id"]').fill('io.test.app.modified');
await page.locator('input[name="product_id"]').press('Tab');
await page.getByLabel('Description').press('ArrowRight');
await page.getByLabel('Description').fill('Test Application modified');

await expect(page).toHaveScreenshot('landing.png');

await page.getByRole('button', { name: 'Update' }).click();

appName = "Test app modified";

await expect(page.getByRole('list')).toContainText('Test app modified');
await expect(page.getByRole('list')).toContainText('io.test.app.modified');
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions frontend/e2e/channel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { expect, test } from '@playwright/test';
import { createApplication, createChannel, createPackage, deleteApplication, generateSalt } from './helpers';

test.describe('Channels', () => {

let appName: string; let appId: string;

test.beforeEach(async ({ page }, testInfo) => {
const appNameSalt = generateSalt(testInfo.title);
appName = "Test app" + appNameSalt;
appId = "io.test.app." + appNameSalt;

await page.goto('http://localhost:8002/');
await createApplication(page, appName, appId);

await expect(page.getByRole('list')).toContainText(appName);
await expect(page.getByRole('list')).toContainText(appId);

await page.getByRole('link', { name: appName }).click();

await createPackage(page, '4117.0.0');
await createPackage(page, '5439.0.0');
await createPackage(page, '87.194.0');

await page.goto('http://localhost:8002/');
await page.getByRole('link', { name: appName }).click();
});

test.afterEach(async ({ page }) => {
await page.goto('http://localhost:8002/');

await deleteApplication(page, appName);

await expect(page.getByRole('list')).not.toContainText(appName);
await expect(page.getByRole('list')).not.toContainText(appId);
});

test('should create a package', async ({ page }) => {
await createChannel(page, 'test1', 'AMD64', '5439.0.0');

await expect(page.locator('#main')).toContainText('AMD64');
await expect(page.locator('#main')).toContainText('test1');
await expect(page.locator('#main')).toContainText('5439.0.0');
});

test('should create a second package', async ({ page }) => {
await createChannel(page, 'test2', 'AMD64', '4117.0.0');

await expect(page.locator('#main')).toContainText('AMD64');
await expect(page.locator('#main')).toContainText('test2');
await expect(page.locator('#main')).toContainText('4117.0.0');
});

test('should create a third package', async ({ page }) => {
await createChannel(page, 'test3', 'X86');

await expect(page.locator('#main')).toContainText('X86');
await expect(page.locator('#main')).toContainText('test3');
await expect(page.locator('#main')).toContainText('No package');
});
});

57 changes: 57 additions & 0 deletions frontend/e2e/global.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect, test as setup } from '@playwright/test';
import { Client } from 'pg';

setup('create new node instances in database', async ({ page }) => {
const client = new Client({
user: 'postgres',
host: 'localhost',
database: 'nebraska_tests',
password: 'nebraska',
port: 8001,
});
await client.connect();

// insert an instance
await client.query(
`INSERT INTO public.instance (alias, created_ts, id, ip) VALUES ($1, $2, $3, $4)`,
['', '2025-01-29 15:27:00.771461+00', '2c517ad881474ec6b5ab928df2a7b5f4', '172.31.239.34']
);

// insert an instance mapping to the default test application
await client.query(
`INSERT INTO public.instance_application (application_id, created_ts, group_id, instance_id, last_check_for_updates, last_update_granted_ts, last_update_version, status, update_in_progress, version) VALUES ($1, $2, $3, $4, NOW(), $5, $6, $7, $8, $9)`,
['e96281a6-d1af-4bde-9a0a-97b76e56dc57', '2025-01-29 15:27:00.771461+00', '5b810680-e36a-4879-b98a-4f989e80b899', '2c517ad881474ec6b5ab928df2a7b5f4', '2025-01-30 09:57:49.885602+00', '5261.0.0', 6, true, '4081.2.0']
);


// insert an instance stats
await client.query(
`INSERT INTO public.instance_stats (arch, channel_name, instances, timestamp, version) VALUES ($1, $2, $3, $4, $5), ($6, $7, $8, $9, $10), ($11, $12, $13, $14, $15), ($16, $17, $18, $19, $20)`,
['AMD64', 'alpha', 1, '2025-01-29 17:36:04.47415+00', '4081.2.0', 'AMD64', 'alpha', 1, '2025-01-30 07:38:36.044909+00', '4081.2.0', 'AMD64', 'alpha', 1, '2025-01-30 08:48:54.986841+00', '4081.2.0', 'AMD64', 'alpha', 1, '2025-01-30 09:46:39.843115+00', '4081.2.0']
);

// insert instance status history
await client.query(
`INSERT INTO public.instance_status_history (application_id, created_ts, group_id, id, instance_id, status, version) VALUES ($1, $2, $3, $4, $5, $6, $7), ($8, $9, $10, $11, $12, $13, $14), ($15, $16, $17, $18, $19, $20, $21)`,
['e96281a6-d1af-4bde-9a0a-97b76e56dc57', '2025-01-30 09:57:49.88614+00', '5b810680-e36a-4879-b98a-4f989e80b899', 1, '2c517ad881474ec6b5ab928df2a7b5f4', 2, '5261.0.0', 'e96281a6-d1af-4bde-9a0a-97b76e56dc57', '2025-01-30 09:57:54.658606+00', '5b810680-e36a-4879-b98a-4f989e80b899', 2, '2c517ad881474ec6b5ab928df2a7b5f4', 7, '5261.0.0', 'e96281a6-d1af-4bde-9a0a-97b76e56dc57', '2025-01-30 09:58:37.034879+00', '5b810680-e36a-4879-b98a-4f989e80b899', 3, '2c517ad881474ec6b5ab928df2a7b5f4', 6, '5261.0.0']
);

await client.query('COMMIT');
await client.end();
});

setup('should open application creation dialog', async ({ page }) => {
await page.goto('http://localhost:8002/');

await page.getByTestId('modal-button').click();
await page.locator('input[name="name"]').click();
await page.locator('input[name="name"]').fill("dwadwad");
await page.locator('input[name="name"]').press('Tab');
await page.locator('input[name="product_id"]').fill("wqeqwe");
await page.locator('input[name="product_id"]').press('Tab');
await page.getByLabel('Description').fill('Test Application');
await page.getByLabel('Description').press('Tab');
await page.getByLabel('Groups/Channels').click();

await expect(page).toHaveScreenshot('create-application-dialog.png');
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions frontend/e2e/global.teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test as teardown } from '@playwright/test';
import { Client } from 'pg';

teardown('delete instance entries from db', async ({ }) => {
const client = new Client({
user: 'postgres',
host: 'localhost',
database: 'nebraska_tests',
password: 'nebraska',
port: 8001,
});
await client.connect();

await client.query(
`DELETE FROM public.instance WHERE id = $1`,
['2c517ad881474ec6b5ab928df2a7b5f4']
);

await client.query(
`DELETE FROM public.instance_application WHERE instance_id = $1`,
['2c517ad881474ec6b5ab928df2a7b5f4']
);

await client.query(
`DELETE FROM public.instance_stats WHERE timestamp IN ($1, $2, $3, $4)`,
['2025-01-29 17:36:04.47415+00', '2025-01-30 07:38:36.044909+00', '2025-01-30 08:48:54.986841+00', '2025-01-30 09:46:39.843115+00']
);

await client.query(
`DELETE FROM public.instance_status_history WHERE instance_id = $1`,
['2c517ad881474ec6b5ab928df2a7b5f4']
);


await client.end();
});
Loading
Loading