mirror of
https://gitea.com/gitea/gitea-mirror.git
synced 2026-03-20 11:50:27 +00:00
Rework e2e tests (#36634)
- Replace the e2e tests initialization with a simple bash script, removing the previous Go harness. - `make test-e2e` is the single entry point. It always starts a fully isolated ephemeral Gitea instance with its own temp directory, SQLite database, and config — no interference with the developer's running instance. - A separate `gitea-e2e` binary is built via `EXECUTABLE_E2E` using `TEST_TAGS` (auto-includes sqlite with `CGO_ENABLED=1`), keeping the developer's regular `gitea` binary untouched. - No more split into database-specific e2e tests. Test timeouts are strict, can be relaxed later if needed. - Simplified and streamlined the playwright config and test files. - Remove all output generation of playwright and all references to visual testing. - Tests run on Chrome locally, Chrome + Firefox on CI. - Simplified CI workflow — visible separate steps for frontend, backend, and test execution. - All exported env vars use `GITEA_TEST_E2E_*` prefix. - Use `GITEA_TEST_E2E_FLAGS` to pass flags to playwright, e.g. `GITEA_TEST_E2E_FLAGS="--ui" make test-e2e` for UI mode or `GITEA_TEST_E2E_FLAGS="--headed" make test-e2e` for headed mode. - Use `GITEA_TEST_E2E_DEBUG=1 make test-e2e` to show Gitea server output. --------- Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
# End to end tests
|
||||
|
||||
E2e tests largely follow the same syntax as [integration tests](../integration).
|
||||
Whereas integration tests are intended to mock and stress the back-end, server-side code, e2e tests the interface between front-end and back-end, as well as visual regressions with both assertions and visual comparisons.
|
||||
They can be run with make commands for the appropriate backends, namely:
|
||||
```shell
|
||||
make test-sqlite
|
||||
make test-pgsql
|
||||
make test-mysql
|
||||
make test-mssql
|
||||
```
|
||||
|
||||
Make sure to perform a clean front-end build before running tests:
|
||||
```
|
||||
make clean frontend
|
||||
```
|
||||
|
||||
## Install playwright system dependencies
|
||||
```
|
||||
pnpm exec playwright install-deps
|
||||
```
|
||||
|
||||
## Run sqlite e2e tests
|
||||
Start tests
|
||||
```
|
||||
make test-e2e-sqlite
|
||||
```
|
||||
|
||||
## Run MySQL e2e tests
|
||||
Setup a MySQL database inside docker
|
||||
```
|
||||
docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest #(just ctrl-c to stop db and clean the container)
|
||||
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --rm --name elasticsearch elasticsearch:7.6.0 #(in a second terminal, just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
Start tests based on the database container
|
||||
```
|
||||
TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-e2e-mysql
|
||||
```
|
||||
|
||||
## Run pgsql e2e tests
|
||||
Setup a pgsql database inside docker
|
||||
```
|
||||
docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:latest #(just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
Start tests based on the database container
|
||||
```
|
||||
TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-e2e-pgsql
|
||||
```
|
||||
|
||||
## Run mssql e2e tests
|
||||
Setup a mssql database inside docker
|
||||
```
|
||||
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
Start tests based on the database container
|
||||
```
|
||||
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql
|
||||
```
|
||||
|
||||
## Running individual tests
|
||||
|
||||
Example command to run `example.test.e2e.ts` test file:
|
||||
|
||||
_Note: unlike integration tests, this filtering is at the file level, not function_
|
||||
|
||||
For SQLite:
|
||||
|
||||
```
|
||||
make test-e2e-sqlite#example
|
||||
```
|
||||
|
||||
For other databases(replace `mssql` to `mysql` or `pgsql`):
|
||||
|
||||
```
|
||||
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example
|
||||
```
|
||||
|
||||
## Visual testing
|
||||
|
||||
Although the main goal of e2e is assertion testing, we have added a framework for visual regress testing. If you are working on front-end features, please use the following:
|
||||
- Check out `main`, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1` to generate outputs. This will initially fail, as no screenshots exist. You can run the e2e tests again to assert it passes.
|
||||
- Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert you front-end changes don't break any other tests unintentionally.
|
||||
|
||||
VISUAL_TEST=1 will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
|
||||
|
||||
ACCEPT_VISUAL=1 will overwrite the snapshot images with new images.
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This is primarily coped from /tests/integration/integration_test.go
|
||||
// TODO: Move common functions to shared file
|
||||
|
||||
//nolint:forbidigo // use of print functions is allowed in tests
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/tests"
|
||||
)
|
||||
|
||||
var testE2eWebRoutes *web.Router
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
defer log.GetManager().Close()
|
||||
|
||||
managerCtx, cancel := context.WithCancel(context.Background())
|
||||
graceful.InitManager(managerCtx)
|
||||
defer cancel()
|
||||
|
||||
tests.InitTest()
|
||||
testE2eWebRoutes = routers.NormalRoutes()
|
||||
|
||||
err := unittest.InitFixtures(
|
||||
unittest.FixturesOptions{
|
||||
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing test database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
exitVal := m.Run()
|
||||
|
||||
testlogger.WriterCloser.Reset()
|
||||
|
||||
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
|
||||
fmt.Printf("util.RemoveAll: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
|
||||
fmt.Printf("Unable to remove repo indexer: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(exitVal)
|
||||
}
|
||||
|
||||
// TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.ts" files in this directory and build a test for each.
|
||||
func TestE2e(t *testing.T) {
|
||||
// Find the paths of all e2e test files in test directory.
|
||||
searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.ts")
|
||||
paths, err := filepath.Glob(searchGlob)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(paths) == 0 {
|
||||
t.Fatal(fmt.Errorf("No e2e tests found in %s", searchGlob))
|
||||
}
|
||||
|
||||
runArgs := []string{"npx", "playwright", "test"}
|
||||
|
||||
// To update snapshot outputs
|
||||
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
|
||||
runArgs = append(runArgs, "--update-snapshots")
|
||||
}
|
||||
|
||||
// Create new test for each input file
|
||||
for _, path := range paths {
|
||||
_, filename := filepath.Split(path)
|
||||
testname := filename[:len(filename)-len(filepath.Ext(path))]
|
||||
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
// Default 2 minute timeout
|
||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||
cmd := exec.Command(runArgs[0], runArgs...)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "GITEA_TEST_SERVER_URL="+setting.AppURL)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
// Currently colored output is conflicting. Using Printf until that is resolved.
|
||||
fmt.Printf("%v", stdout.String())
|
||||
fmt.Printf("%v", stderr.String())
|
||||
log.Fatal("Playwright Failed: %s", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%v", stdout.String())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
9
tests/e2e/env.d.ts
vendored
Normal file
9
tests/e2e/env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
GITEA_TEST_E2E_DOMAIN: string;
|
||||
GITEA_TEST_E2E_USER: string;
|
||||
GITEA_TEST_E2E_EMAIL: string;
|
||||
GITEA_TEST_E2E_PASSWORD: string;
|
||||
GITEA_TEST_E2E_URL: string;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
|
||||
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
test('homepage', async ({page}) => {
|
||||
const response = await page.goto('/');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
await expect(page).toHaveTitle(/^Gitea: Git with a cup of tea\s*$/);
|
||||
await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||
});
|
||||
|
||||
test('register', async ({page}, workerInfo) => {
|
||||
const response = await page.goto('/user/sign_up');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
await page.locator('input[name=user_name]').fill(`e2e-test-${workerInfo.workerIndex}`);
|
||||
await page.locator('input[name=email]').fill(`e2e-test-${workerInfo.workerIndex}@test.com`);
|
||||
await page.locator('input[name=password]').fill('test123test123');
|
||||
await page.locator('input[name=retype]').fill('test123test123');
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
// Make sure we routed to the home page. Else login failed.
|
||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
|
||||
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
|
||||
|
||||
save_visual(page);
|
||||
});
|
||||
|
||||
test('login', async ({page}, workerInfo) => {
|
||||
const response = await page.goto('/user/login');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
|
||||
await page.locator('input[name=user_name]').fill(`user2`);
|
||||
await page.locator('input[name=password]').fill(`password`);
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
|
||||
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
|
||||
|
||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
|
||||
save_visual(page);
|
||||
});
|
||||
|
||||
test('logged in user', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Make sure we routed to the home page. Else login failed.
|
||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
|
||||
save_visual(page);
|
||||
});
|
||||
17
tests/e2e/explore.test.ts
Normal file
17
tests/e2e/explore.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
|
||||
test('explore repositories', async ({page}) => {
|
||||
await page.goto('/explore/repos');
|
||||
await expect(page.getByPlaceholder('Search repos…')).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Repositories'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('explore users', async ({page}) => {
|
||||
await page.goto('/explore/users');
|
||||
await expect(page.getByPlaceholder('Search users…')).toBeVisible();
|
||||
});
|
||||
|
||||
test('explore organizations', async ({page}) => {
|
||||
await page.goto('/explore/organizations');
|
||||
await expect(page.getByPlaceholder('Search orgs…')).toBeVisible();
|
||||
});
|
||||
12
tests/e2e/login.test.ts
Normal file
12
tests/e2e/login.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, logout} from './utils.ts';
|
||||
|
||||
test('homepage', async ({page}) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||
});
|
||||
|
||||
test('login and logout', async ({page}) => {
|
||||
await login(page);
|
||||
await logout(page);
|
||||
});
|
||||
14
tests/e2e/milestone.test.ts
Normal file
14
tests/e2e/milestone.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiDeleteRepo} from './utils.ts';
|
||||
|
||||
test('create a milestone', async ({page}) => {
|
||||
const repoName = `e2e-milestone-${Date.now()}`;
|
||||
await login(page);
|
||||
await apiCreateRepo(page.request, {name: repoName});
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`);
|
||||
await page.getByPlaceholder('Title').fill('Test Milestone');
|
||||
await page.getByRole('button', {name: 'Create Milestone'}).click();
|
||||
await expect(page.locator('.milestone-list')).toContainText('Test Milestone');
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
});
|
||||
13
tests/e2e/org.test.ts
Normal file
13
tests/e2e/org.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiDeleteOrg} from './utils.ts';
|
||||
|
||||
test('create an organization', async ({page}) => {
|
||||
const orgName = `e2e-org-${Date.now()}`;
|
||||
await login(page);
|
||||
await page.goto('/org/create');
|
||||
await page.getByLabel('Organization Name').fill(orgName);
|
||||
await page.getByRole('button', {name: 'Create Organization'}).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/org/${orgName}`));
|
||||
// delete via API because of issues related to form-fetch-action
|
||||
await apiDeleteOrg(page.request, orgName);
|
||||
});
|
||||
11
tests/e2e/readme.test.ts
Normal file
11
tests/e2e/readme.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateRepo, apiDeleteRepo} from './utils.ts';
|
||||
|
||||
test('repo readme', async ({page}) => {
|
||||
const repoName = `e2e-readme-${Date.now()}`;
|
||||
await apiCreateRepo(page.request, {name: repoName});
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`);
|
||||
await expect(page.locator('#readme')).toContainText(repoName);
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
});
|
||||
73
tests/e2e/register.test.ts
Normal file
73
tests/e2e/register.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, logout} from './utils.ts';
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
await page.goto('/user/sign_up');
|
||||
});
|
||||
|
||||
test('register page has form', async ({page}) => {
|
||||
await expect(page.getByLabel('Username')).toBeVisible();
|
||||
await expect(page.getByLabel('Email Address')).toBeVisible();
|
||||
await expect(page.getByLabel('Password', {exact: true})).toBeVisible();
|
||||
await expect(page.getByLabel('Confirm Password')).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Register Account'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('register with empty fields shows error', async ({page}) => {
|
||||
// HTML5 required attribute prevents submission, so verify the fields are required
|
||||
await expect(page.locator('input[name="user_name"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="email"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="retype"][required]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('register with mismatched passwords shows error', async ({page}) => {
|
||||
await page.getByLabel('Username').fill('e2e-register-mismatch');
|
||||
await page.getByLabel('Email Address').fill(`e2e-register-mismatch@${env.GITEA_TEST_E2E_DOMAIN}`);
|
||||
await page.getByLabel('Password', {exact: true}).fill('password123!');
|
||||
await page.getByLabel('Confirm Password').fill('different123!');
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
await expect(page.locator('.ui.negative.message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('register then login', async ({page}) => {
|
||||
const username = `e2e-register-${Date.now()}`;
|
||||
const email = `${username}@${env.GITEA_TEST_E2E_DOMAIN}`;
|
||||
const password = 'password123!';
|
||||
|
||||
await page.getByLabel('Username').fill(username);
|
||||
await page.getByLabel('Email Address').fill(email);
|
||||
await page.getByLabel('Password', {exact: true}).fill(password);
|
||||
await page.getByLabel('Confirm Password').fill(password);
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
|
||||
// After successful registration, should be redirected away from sign_up
|
||||
await expect(page).not.toHaveURL(/sign_up/);
|
||||
|
||||
// Logout then login with the newly created account
|
||||
await logout(page);
|
||||
await login(page, username, password);
|
||||
|
||||
// delete via API because of issues related to form-fetch-action
|
||||
const response = await page.request.delete(`/api/v1/admin/users/${username}?purge=true`, {
|
||||
headers: {Authorization: `Basic ${btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`},
|
||||
});
|
||||
expect(response.ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('register with existing username shows error', async ({page}) => {
|
||||
await page.getByLabel('Username').fill(env.GITEA_TEST_E2E_USER);
|
||||
await page.getByLabel('Email Address').fill(`e2e-duplicate@${env.GITEA_TEST_E2E_DOMAIN}`);
|
||||
await page.getByLabel('Password', {exact: true}).fill('password123!');
|
||||
await page.getByLabel('Confirm Password').fill('password123!');
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
await expect(page.locator('.ui.negative.message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('sign in link exists', async ({page}) => {
|
||||
const signInLink = page.getByText('Sign in now!');
|
||||
await expect(signInLink).toBeVisible();
|
||||
await signInLink.click();
|
||||
await expect(page).toHaveURL(/\/user\/login$/);
|
||||
});
|
||||
13
tests/e2e/repo.test.ts
Normal file
13
tests/e2e/repo.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {env} from 'node:process';
|
||||
import {test} from '@playwright/test';
|
||||
import {login, apiDeleteRepo} from './utils.ts';
|
||||
|
||||
test('create a repository', async ({page}) => {
|
||||
const repoName = `e2e-repo-${Date.now()}`;
|
||||
await login(page);
|
||||
await page.goto('/repo/create');
|
||||
await page.locator('input[name="repo_name"]').fill(repoName);
|
||||
await page.getByRole('button', {name: 'Create Repository'}).click();
|
||||
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
});
|
||||
14
tests/e2e/user-settings.test.ts
Normal file
14
tests/e2e/user-settings.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login} from './utils.ts';
|
||||
|
||||
test('update profile biography', async ({page}) => {
|
||||
const bio = `e2e-bio-${Date.now()}`;
|
||||
await login(page);
|
||||
await page.goto('/user/settings');
|
||||
await page.getByLabel('Biography').fill(bio);
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||
await page.getByLabel('Biography').fill('');
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||
});
|
||||
63
tests/e2e/utils.ts
Normal file
63
tests/e2e/utils.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect} from '@playwright/test';
|
||||
import type {APIRequestContext, Locator, Page} from '@playwright/test';
|
||||
|
||||
export function apiBaseUrl() {
|
||||
return env.GITEA_TEST_E2E_URL?.replace(/\/$/g, '');
|
||||
}
|
||||
|
||||
export function apiHeaders() {
|
||||
return {Authorization: `Basic ${globalThis.btoa(`${env.GITEA_TEST_E2E_USER}:${env.GITEA_TEST_E2E_PASSWORD}`)}`};
|
||||
}
|
||||
|
||||
async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => number; text: () => Promise<string>}>, label: string) {
|
||||
const maxAttempts = 5;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const response = await fn();
|
||||
if (response.ok()) return;
|
||||
if ([500, 502, 503].includes(response.status()) && attempt < maxAttempts - 1) {
|
||||
const jitter = Math.random() * 500;
|
||||
await new Promise((resolve) => globalThis.setTimeout(resolve, 1000 * (attempt + 1) + jitter));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`${label} failed: ${response.status()} ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true}: {name: string; autoInit?: boolean}) {
|
||||
await apiRetry(() => requestContext.post(`${apiBaseUrl()}/api/v1/user/repos`, {
|
||||
headers: apiHeaders(),
|
||||
data: {name, auto_init: autoInit},
|
||||
}), 'apiCreateRepo');
|
||||
}
|
||||
|
||||
export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) {
|
||||
await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/repos/${owner}/${name}`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteRepo');
|
||||
}
|
||||
|
||||
export async function apiDeleteOrg(requestContext: APIRequestContext, name: string) {
|
||||
await apiRetry(() => requestContext.delete(`${apiBaseUrl()}/api/v1/orgs/${name}`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteOrg');
|
||||
}
|
||||
|
||||
export async function clickDropdownItem(page: Page, trigger: Locator, itemText: string) {
|
||||
await trigger.click();
|
||||
await page.getByText(itemText).click();
|
||||
}
|
||||
|
||||
export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) {
|
||||
await page.goto('/user/login');
|
||||
await page.getByLabel('Username or Email Address').fill(username);
|
||||
await page.getByLabel('Password').fill(password);
|
||||
await page.getByRole('button', {name: 'Sign In'}).click();
|
||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden();
|
||||
}
|
||||
|
||||
export async function logout(page: Page) {
|
||||
await page.context().clearCookies(); // workaround issues related to fomantic dropdown
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeVisible();
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import {expect} from '@playwright/test';
|
||||
import {env} from 'node:process';
|
||||
import type {Browser, Page, WorkerInfo} from '@playwright/test';
|
||||
|
||||
const ARTIFACTS_PATH = `tests/e2e/test-artifacts`;
|
||||
const LOGIN_PASSWORD = 'password';
|
||||
|
||||
// log in user and store session info. This should generally be
|
||||
// run in test.beforeAll(), then the session can be loaded in tests.
|
||||
export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: string) {
|
||||
// Set up a new context
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Route to login page
|
||||
// Note: this could probably be done more quickly with a POST
|
||||
const response = await page.goto('/user/login');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
|
||||
// Fill out form
|
||||
await page.locator('input[name=user_name]').fill(user);
|
||||
await page.locator('input[name=password]').fill(LOGIN_PASSWORD);
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
|
||||
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
|
||||
|
||||
expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
|
||||
// Save state
|
||||
await context.storageState({path: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) {
|
||||
try {
|
||||
return await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function save_visual(page: Page) {
|
||||
// Optionally include visual testing
|
||||
if (env.VISUAL_TEST) {
|
||||
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
|
||||
// Mock page/version string
|
||||
await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK');
|
||||
await expect(page).toHaveScreenshot({
|
||||
fullPage: true,
|
||||
timeout: 20000,
|
||||
mask: [
|
||||
page.locator('.secondary-nav span>img.ui.avatar'),
|
||||
page.locator('.ui.dropdown.jump.item span>img.ui.avatar'),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func onGiteaRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
|
||||
if len(prepare) == 0 || prepare[0] {
|
||||
defer tests.PrepareTestEnv(t, 1)()
|
||||
}
|
||||
s := http.Server{
|
||||
Handler: testE2eWebRoutes,
|
||||
}
|
||||
|
||||
u, err := url.Parse(setting.AppURL)
|
||||
assert.NoError(t, err)
|
||||
listener, err := net.Listen("tcp", u.Host)
|
||||
i := 0
|
||||
for err != nil && i <= 10 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
listener, err = net.Listen("tcp", u.Host)
|
||||
i++
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
u.Host = listener.Addr().String()
|
||||
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
|
||||
s.Shutdown(ctx)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
go s.Serve(listener)
|
||||
// Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
|
||||
|
||||
callback(t, u)
|
||||
}
|
||||
|
||||
func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...bool) {
|
||||
onGiteaRunTB(t, func(t testing.TB, u *url.URL) {
|
||||
callback(t.(*testing.T), u)
|
||||
}, prepare...)
|
||||
}
|
||||
Reference in New Issue
Block a user