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:
silverwind
2026-02-21 01:26:47 +01:00
committed by GitHub
parent 86d102494b
commit 18e0746b7b
22 changed files with 419 additions and 524 deletions

View File

@@ -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.

View File

@@ -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
View 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;
}
}

View File

@@ -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
View 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
View 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);
});

View 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
View 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
View 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);
});

View 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
View 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);
});

View 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
View 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();
}

View File

@@ -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'),
],
});
}
}

View File

@@ -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...)
}