Repository navigation
feat: adding basic end-to-end testing and ci workflow #225
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
Changes from 2 commits
1d1d92a
f876d8e
5d4a124
e575ea1
014ad35
67d9de6
a2193eb
b901e8b
6ade15d
a9fa976
495a1c6
73c664b
9aaac4d
19ffac1
d0ff0b0
30e053c
cda0a44
8e99504
66bc0c4
50b3557
eedb704
dc97de0
a781e0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| name: End-to-end testing | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [main] | ||
|
|
||
| jobs: | ||
| build-and-test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| services: | ||
| postgres: | ||
| image: postgres:17 | ||
|
KillianLarcher marked this conversation as resolved.
Resolved
|
||
| env: | ||
| POSTGRES_USER: devuser | ||
| POSTGRES_PASSWORD: changeme | ||
| POSTGRES_DB: devdb | ||
| ports: | ||
| - 5432:5432 | ||
| options: >- | ||
| --health-cmd "pg_isready -U devuser -d devdb" | ||
| --health-interval 10s | ||
| --health-timeout 5s | ||
| --health-retries 5 | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Build Docker image | ||
| uses: docker/build-push-action@v6 | ||
| with: | ||
| context: . | ||
| file: ./docker/dockerfile/Dockerfile | ||
| push: false | ||
| load: true | ||
| tags: portabase/portabase:test | ||
|
|
||
| - name: Run app container | ||
| run: | | ||
| docker run -d --name myapp \ | ||
| -p 8887:8887 \ | ||
| -e DATABASE_URL=postgres://devuser:changeme@postgres:5432/devdb \ | ||
| -e NODE_ENV=production \ | ||
| -e PROJECT_URL=http://localhost:8887 \ | ||
| portabase/portabase:test | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Wait for app port to be listening | ||
| run: | | ||
| for i in {1..40}; do | ||
| if nc -z localhost 8887; then | ||
| echo "Port 8887 is open → app is likely listening" | ||
| exit 0 | ||
| fi | ||
| echo "Waiting for port 8887... ($i/40)" | ||
| sleep 2 | ||
| done | ||
| echo "Timeout: port 8887 never opened" | ||
| docker logs myapp | ||
| exit 1 | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Setup Node + Playwright | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 | ||
|
|
||
| - run: npm ci | ||
| - run: npx playwright install --with-deps | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Run Playwright tests | ||
| run: npx playwright test | ||
| env: | ||
| BASE_URL: http://localhost:8887 | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| - name: Upload report if failed | ||
| if: failure() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: playwright-report | ||
| path: playwright-report/ | ||
| retention-days: 7 | ||
|
|
||
| - name: Cleanup | ||
| if: always() | ||
| run: docker stop myapp && docker rm myapp || true | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import {test, expect, Page} from '@playwright/test'; | ||
|
|
||
|
|
||
| type UserCredentials = { | ||
| username: string | ||
| email: string | ||
| password: string | ||
| } | ||
|
|
||
|
|
||
| const users: Record<string, UserCredentials> = { | ||
| admin: {username: 'Admin', email: 'admin5@example.com', password: 'testPASS123456!'}, | ||
| normal: {username: 'John Doe', email: 'john5@example.comm', password: 'testPASS123456!'}, | ||
| } | ||
|
KillianLarcher marked this conversation as resolved.
Resolved
|
||
|
|
||
| async function register(page: Page, name: string, email: string, password: string, confirmPassword: string) { | ||
| await page.locator('input[name="name"]').fill(name) | ||
| await page.locator('input[name="email"]').fill(email) | ||
| await page.locator('input[name="password"]').fill(password) | ||
| await page.locator('input[name="confirmPassword"]').fill(confirmPassword) | ||
|
|
||
| await page.click('button[type="submit"]') | ||
| } | ||
|
|
||
| async function login(page: Page, email: string, password: string) { | ||
| await page.locator('input[name="email"]').fill(email) | ||
| await page.locator('input[name="password"]').fill(password) | ||
|
|
||
| // await page.click('button[type="submit"]') | ||
| await page.locator('button:has-text("Login")').click() | ||
| // await page.click('text=Login') | ||
| } | ||
|
|
||
| async function logout() { | ||
|
KillianLarcher marked this conversation as resolved.
Dismissed
|
||
| } | ||
|
|
||
| test.describe.serial('User signup and login flows', () => { | ||
|
|
||
| test('Redirect to login if not connected', async ({page}) => { | ||
| await page.goto('dashboard/projects'); | ||
| await expect(page).toHaveURL("login?redirect=%2Fdashboard%2Fprojects"); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| test('Password too short', async ({page}) => { | ||
| await page.goto('') | ||
| await page.click('text=Sign up') | ||
| await expect(page).toHaveURL('register') | ||
|
|
||
| let password = '123456' | ||
| await register(page, users["admin"].username, users["admin"].email, password, password) | ||
|
|
||
| const toast = page.locator('text=Must have at least 8 character') | ||
| await expect(toast).toBeVisible() | ||
| }) | ||
|
|
||
| test('Password too simple', async ({page}) => { | ||
| await page.goto('') | ||
| await page.click('text=Sign up') | ||
| await expect(page).toHaveURL('register') | ||
|
|
||
| let password = '12345678' | ||
| await register(page, users["admin"].username, users["admin"].email, password, password) | ||
|
|
||
| const toast = page.locator('text=Your password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.') | ||
| await expect(toast).toBeVisible() | ||
| }) | ||
|
|
||
| test('Password and confirm password mismatch', async ({page}) => { | ||
| await page.goto('') | ||
| await page.click('text=Sign up') | ||
| await expect(page).toHaveURL('register') | ||
|
|
||
| let password = 'testPASS123456!' | ||
| let confirmPassword = 'testPASS123456!!' | ||
| await register(page, users["admin"].username, users["admin"].email, password, confirmPassword) | ||
|
|
||
| const toast = page.locator('text=The passwords did not match') | ||
| await expect(toast).toBeVisible() | ||
| }) | ||
|
|
||
| test('Successful register for admin', async ({page}) => { | ||
| await page.goto('') | ||
|
|
||
| await page.click('text=Sign up') | ||
| await expect(page).toHaveURL('register') | ||
|
|
||
| await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) | ||
|
|
||
| await expect(page).toHaveURL('login') | ||
| }) | ||
|
|
||
| test('User already exists.', async ({page}) => { | ||
| await page.goto('') | ||
|
|
||
| await page.click('text=Sign up') | ||
| await expect(page).toHaveURL('register') | ||
|
|
||
| await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password) | ||
|
|
||
| const toast = page.locator('text=User already exists. Use another email.') | ||
| await expect(toast).toBeVisible() | ||
| }) | ||
|
|
||
| test('Successful register for normal', async ({page}) => { | ||
| await page.goto('') | ||
|
|
||
| await page.click('text=Sign up') | ||
| await expect(page).toHaveURL('register') | ||
|
|
||
| await register(page, users["normal"].username, users["normal"].email, users["normal"].password, users["normal"].password) | ||
|
|
||
| await expect(page).toHaveURL('login') | ||
| }) | ||
|
|
||
| test('Failed login because account not active', async ({page}) => { | ||
| await page.goto('login') | ||
| await login(page, users["normal"].email, users["normal"].password) | ||
|
|
||
| const toast = page.locator('text=Your account is not active.') | ||
| await expect(toast).toBeVisible() | ||
| }) | ||
|
|
||
| test('Successful login', async ({page}) => { | ||
| await page.goto('login') | ||
| await login(page, users["admin"].email, users["admin"].password) | ||
|
|
||
| await expect(page).toHaveURL('dashboard') | ||
| }) | ||
|
|
||
| }) | ||
|
|
||
|
|
||
| test('Successful logout', async ({page}) => { | ||
| await logout() | ||
| await expect(page).toHaveURL('login') | ||
| }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test will fail: The 🐛 Proposed fix-async function logout() {
-}
+async function logout(page: Page) {
+ // Navigate to a page with logout functionality
+ await page.goto('/dashboard');
+ // Click logout button - adjust selector as needed
+ await page.locator('button:has-text("Logout")').click();
+}
-test('Successful logout', async ({page}) => {
- await logout()
- await expect(page).toHaveURL('login')
-})
+test('Successful logout', async ({page}) => {
+ // First, log in
+ await page.goto('login');
+ await login(page, users["admin"].email, users["admin"].password);
+ await expect(page).toHaveURL('dashboard');
+
+ // Then log out
+ await logout(page);
+ await expect(page).toHaveURL(/login/);
+})🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||||||||||||||||||||||||||
| import { defineConfig, devices } from '@playwright/test'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Read environment variables from file. | ||||||||||||||||||||||||||||||
| * https://github.com/motdotla/dotenv | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| // import dotenv from 'dotenv'; | ||||||||||||||||||||||||||||||
| // import path from 'path'; | ||||||||||||||||||||||||||||||
| // dotenv.config({ path: path.resolve(__dirname, '.env') }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * See https://playwright.dev/docs/test-configuration. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| export default defineConfig({ | ||||||||||||||||||||||||||||||
| testDir: './e2e', | ||||||||||||||||||||||||||||||
| /* Run tests in files in parallel */ | ||||||||||||||||||||||||||||||
| fullyParallel: true, | ||||||||||||||||||||||||||||||
| /* Fail the build on CI if you accidentally left test.only in the source code. */ | ||||||||||||||||||||||||||||||
| forbidOnly: !!process.env.CI, | ||||||||||||||||||||||||||||||
| /* Retry on CI only */ | ||||||||||||||||||||||||||||||
| retries: process.env.CI ? 2 : 0, | ||||||||||||||||||||||||||||||
| /* Opt out of parallel tests on CI. */ | ||||||||||||||||||||||||||||||
| workers: process.env.CI ? 1 : undefined, | ||||||||||||||||||||||||||||||
| /* Reporter to use. See https://playwright.dev/docs/test-reporters */ | ||||||||||||||||||||||||||||||
| reporter: 'html', | ||||||||||||||||||||||||||||||
| /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | ||||||||||||||||||||||||||||||
| use: { | ||||||||||||||||||||||||||||||
| /* Base URL to use in actions like `await page.goto('')`. */ | ||||||||||||||||||||||||||||||
| baseURL: 'http://localhost:8887', | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ | ||||||||||||||||||||||||||||||
| trace: 'on-first-retry', | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider using environment variable for The ♻️ Proposed fix use: {
/* Base URL to use in actions like `await page.goto('')`. */
- baseURL: 'http://localhost:8887',
+ baseURL: process.env.BASE_URL || 'http://localhost:8887',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /* Configure projects for major browsers */ | ||||||||||||||||||||||||||||||
| projects: [ | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| name: 'chromium', | ||||||||||||||||||||||||||||||
| use: { ...devices['Desktop Chrome'] }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| name: 'firefox', | ||||||||||||||||||||||||||||||
| use: { ...devices['Desktop Firefox'] }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| name: 'webkit', | ||||||||||||||||||||||||||||||
| use: { ...devices['Desktop Safari'] }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /* Test against mobile viewports. */ | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| name: 'Mobile Chrome', | ||||||||||||||||||||||||||||||
| use: { ...devices['Pixel 5'] }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| name: 'Mobile Safari', | ||||||||||||||||||||||||||||||
| use: { ...devices['iPhone 12'] }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /* Test against branded browsers. */ | ||||||||||||||||||||||||||||||
| // { | ||||||||||||||||||||||||||||||
| // name: 'Microsoft Edge', | ||||||||||||||||||||||||||||||
| // use: { ...devices['Desktop Edge'], channel: 'msedge' }, | ||||||||||||||||||||||||||||||
| // }, | ||||||||||||||||||||||||||||||
| // { | ||||||||||||||||||||||||||||||
| // name: 'Google Chrome', | ||||||||||||||||||||||||||||||
| // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, | ||||||||||||||||||||||||||||||
| // }, | ||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /* Run your local dev server before starting the tests */ | ||||||||||||||||||||||||||||||
| // webServer: { | ||||||||||||||||||||||||||||||
| // command: 'docker compose up -d && make up', | ||||||||||||||||||||||||||||||
| // url: 'http://localhost:8887', | ||||||||||||||||||||||||||||||
| // reuseExistingServer: !process.env.CI, | ||||||||||||||||||||||||||||||
| // }, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.