mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-21 11:28:12 +01:00
Merge branch 'main' into aipolicy
This commit is contained in:
commit
1a2c01a4b0
43
.github/workflows/pull-e2e-tests.yml
vendored
Normal file
43
.github/workflows/pull-e2e-tests.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: e2e-tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
uses: ./.github/workflows/files-changed.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
test-e2e:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- run: make deps-frontend
|
||||
- run: make frontend
|
||||
- run: make deps-backend
|
||||
- run: make gitea-e2e
|
||||
- run: make playwright
|
||||
- run: make test-e2e
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
GITEA_TEST_E2E_DEBUG: 1
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -55,6 +55,7 @@ cpu.out
|
||||
*.log.*.gz
|
||||
|
||||
/gitea
|
||||
/gitea-e2e
|
||||
/gitea-vet
|
||||
/debug
|
||||
/integrations.test
|
||||
@ -67,13 +68,9 @@ cpu.out
|
||||
/indexers
|
||||
/log
|
||||
/public/assets/img/avatar
|
||||
/tests/e2e-output
|
||||
/tests/integration/gitea-integration-*
|
||||
/tests/integration/indexers-*
|
||||
/tests/e2e/gitea-e2e-*
|
||||
/tests/e2e/indexers-*
|
||||
/tests/e2e/reports
|
||||
/tests/e2e/test-artifacts
|
||||
/tests/e2e/test-snapshots
|
||||
/tests/*.ini
|
||||
/tests/**/*.git/**/*.sample
|
||||
/node_modules
|
||||
|
||||
@ -192,7 +192,14 @@ Here's how to run the test suite:
|
||||
| :------------------------------------------ | :------------------------------------------------------- | ------------------------------------------- |
|
||||
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|
||||
|``make test-sqlite[\#SpecificTestName]`` | run [integration](tests/integration) test(s) for SQLite | [More details](tests/integration/README.md) |
|
||||
|``make test-e2e-sqlite[\#SpecificTestName]`` | run [end-to-end](tests/e2e) test(s) for SQLite | [More details](tests/e2e/README.md) |
|
||||
|``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | |
|
||||
|
||||
- E2E test environment variables
|
||||
|
||||
| Variable | Description |
|
||||
| :------------------------ | :---------------------------------------------------------------- |
|
||||
| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output |
|
||||
| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` |
|
||||
|
||||
## Translation
|
||||
|
||||
|
||||
71
Makefile
71
Makefile
@ -53,9 +53,11 @@ endif
|
||||
ifeq ($(IS_WINDOWS),yes)
|
||||
GOFLAGS := -v -buildmode=exe
|
||||
EXECUTABLE ?= gitea.exe
|
||||
EXECUTABLE_E2E ?= gitea-e2e.exe
|
||||
else
|
||||
GOFLAGS := -v
|
||||
EXECUTABLE ?= gitea
|
||||
EXECUTABLE_E2E ?= gitea-e2e
|
||||
endif
|
||||
|
||||
ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu)
|
||||
@ -115,7 +117,7 @@ LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
||||
|
||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
|
||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
|
||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
|
||||
|
||||
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
|
||||
@ -153,10 +155,6 @@ GO_SOURCES := $(wildcard *.go)
|
||||
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
|
||||
GO_SOURCES += $(GENERATED_GO_DEST)
|
||||
|
||||
# Force installation of playwright dependencies by setting this flag
|
||||
ifdef DEPS_PLAYWRIGHT
|
||||
PLAYWRIGHT_FLAGS += --with-deps
|
||||
endif
|
||||
|
||||
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
||||
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
|
||||
@ -187,7 +185,7 @@ all: build
|
||||
.PHONY: help
|
||||
help: Makefile ## print Makefile help information.
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST)
|
||||
@printf " \033[36m%-46s\033[0m %s\n" "test-e2e[#TestSpecificName]" "test end to end using playwright"
|
||||
@printf " \033[36m%-46s\033[0m %s\n" "test-e2e" "test end to end using playwright"
|
||||
@printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test"
|
||||
@printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite"
|
||||
|
||||
@ -204,9 +202,8 @@ clean-all: clean ## delete backend, frontend and integration files
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## delete backend and integration files
|
||||
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \
|
||||
rm -rf $(EXECUTABLE) $(EXECUTABLE_E2E) $(DIST) $(BINDATA_DEST_WILDCARD) \
|
||||
integrations*.test \
|
||||
e2e*.test \
|
||||
tests/integration/gitea-integration-* \
|
||||
tests/integration/indexers-* \
|
||||
tests/sqlite.ini tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
|
||||
@ -535,47 +532,12 @@ test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test
|
||||
|
||||
.PHONY: playwright
|
||||
playwright: deps-frontend
|
||||
$(NODE_VARS) pnpm exec playwright install $(PLAYWRIGHT_FLAGS)
|
||||
|
||||
.PHONY: test-e2e%
|
||||
test-e2e%: TEST_TYPE ?= e2e
|
||||
# Clear display env variable. Otherwise, chromium tests can fail.
|
||||
DISPLAY=
|
||||
@# on GitHub Actions VMs, playwright's system deps are pre-installed
|
||||
@$(NODE_VARS) pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium $(if $(CI),firefox) $(PLAYWRIGHT_FLAGS)
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: test-e2e-sqlite
|
||||
|
||||
.PHONY: test-e2e-sqlite
|
||||
test-e2e-sqlite: playwright e2e.sqlite.test generate-ini-sqlite
|
||||
GITEA_TEST_CONF=tests/sqlite.ini ./e2e.sqlite.test
|
||||
|
||||
.PHONY: test-e2e-sqlite\#%
|
||||
test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite
|
||||
GITEA_TEST_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e/$*
|
||||
|
||||
.PHONY: test-e2e-mysql
|
||||
test-e2e-mysql: playwright e2e.mysql.test generate-ini-mysql
|
||||
GITEA_TEST_CONF=tests/mysql.ini ./e2e.mysql.test
|
||||
|
||||
.PHONY: test-e2e-mysql\#%
|
||||
test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql
|
||||
GITEA_TEST_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e/$*
|
||||
|
||||
.PHONY: test-e2e-pgsql
|
||||
test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql
|
||||
GITEA_TEST_CONF=tests/pgsql.ini ./e2e.pgsql.test
|
||||
|
||||
.PHONY: test-e2e-pgsql\#%
|
||||
test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql
|
||||
GITEA_TEST_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e/$*
|
||||
|
||||
.PHONY: test-e2e-mssql
|
||||
test-e2e-mssql: playwright e2e.mssql.test generate-ini-mssql
|
||||
GITEA_TEST_CONF=tests/mssql.ini ./e2e.mssql.test
|
||||
|
||||
.PHONY: test-e2e-mssql\#%
|
||||
test-e2e-mssql\#%: playwright e2e.mssql.test generate-ini-mssql
|
||||
GITEA_TEST_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$*
|
||||
test-e2e: playwright $(EXECUTABLE_E2E)
|
||||
@EXECUTABLE=$(EXECUTABLE_E2E) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS)
|
||||
|
||||
.PHONY: bench-sqlite
|
||||
bench-sqlite: integrations.sqlite.test generate-ini-sqlite
|
||||
@ -671,18 +633,6 @@ migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
|
||||
migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
|
||||
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
||||
|
||||
e2e.mysql.test: $(GO_SOURCES)
|
||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql.test
|
||||
|
||||
e2e.pgsql.test: $(GO_SOURCES)
|
||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.pgsql.test
|
||||
|
||||
e2e.mssql.test: $(GO_SOURCES)
|
||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mssql.test
|
||||
|
||||
e2e.sqlite.test: $(GO_SOURCES)
|
||||
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.sqlite.test -tags '$(TEST_TAGS)'
|
||||
|
||||
.PHONY: check
|
||||
check: test
|
||||
|
||||
@ -721,6 +671,9 @@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
||||
endif
|
||||
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
||||
|
||||
$(EXECUTABLE_E2E): $(GO_SOURCES)
|
||||
CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
||||
|
||||
.PHONY: release
|
||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
|
||||
|
||||
|
||||
@ -911,9 +911,10 @@ export default defineConfig([
|
||||
},
|
||||
{
|
||||
...playwright.configs['flat/recommended'],
|
||||
files: ['tests/e2e/**'],
|
||||
files: ['tests/e2e/**/*.test.ts'],
|
||||
rules: {
|
||||
...playwright.configs['flat/recommended'].rules,
|
||||
'playwright/expect-expect': [0],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,98 +1,35 @@
|
||||
import {devices} from '@playwright/test';
|
||||
import {env} from 'node:process';
|
||||
import type {PlaywrightTestConfig} from '@playwright/test';
|
||||
import {defineConfig, devices} from '@playwright/test';
|
||||
|
||||
const BASE_URL = env.GITEA_TEST_SERVER_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000';
|
||||
|
||||
export default {
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e/',
|
||||
testMatch: /.*\.test\.e2e\.ts/, // Match any .test.e2e.ts files
|
||||
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
|
||||
expect: {
|
||||
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 2000,
|
||||
},
|
||||
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
outputDir: './tests/e2e-output/',
|
||||
testMatch: /.*\.test\.ts/,
|
||||
forbidOnly: Boolean(env.CI),
|
||||
|
||||
/* Retry on CI only */
|
||||
retries: env.CI ? 2 : 0,
|
||||
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: env.CI ? 'list' : [['list'], ['html', {outputFolder: 'tests/e2e/reports/', open: 'never'}]],
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
headless: true, // set to false to debug
|
||||
|
||||
locale: 'en-US',
|
||||
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 1000,
|
||||
|
||||
/* Maximum time allowed for navigation, such as `page.goto()`. */
|
||||
navigationTimeout: 5 * 1000,
|
||||
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: BASE_URL,
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
|
||||
screenshot: 'only-on-failure',
|
||||
reporter: 'list',
|
||||
timeout: env.CI ? 12000 : 6000,
|
||||
expect: {
|
||||
timeout: env.CI ? 6000 : 3000,
|
||||
},
|
||||
use: {
|
||||
baseURL: env.GITEA_TEST_E2E_URL?.replace?.(/\/$/g, ''),
|
||||
locale: 'en-US',
|
||||
actionTimeout: env.CI ? 6000 : 3000,
|
||||
navigationTimeout: env.CI ? 12000 : 6000,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
|
||||
/* Project-specific settings. */
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
},
|
||||
},
|
||||
|
||||
// disabled because of https://github.com/go-gitea/gitea/issues/21355
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: {
|
||||
// ...devices['Desktop Firefox'],
|
||||
// },
|
||||
// },
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
...env.CI ? [{
|
||||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
...devices['Desktop Firefox'],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: {
|
||||
...devices['Pixel 5'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: {
|
||||
...devices['iPhone 12'],
|
||||
},
|
||||
},
|
||||
}] : [],
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
outputDir: 'tests/e2e/test-artifacts/',
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
snapshotDir: 'tests/e2e/test-snapshots/',
|
||||
} satisfies PlaywrightTestConfig;
|
||||
});
|
||||
|
||||
@ -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...)
|
||||
}
|
||||
93
tools/test-e2e.sh
Executable file
93
tools/test-e2e.sh
Executable file
@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Create isolated work directory
|
||||
WORK_DIR=$(mktemp -d)
|
||||
|
||||
# Find a random free port
|
||||
FREE_PORT=$(node -e "const s=require('net').createServer();s.listen(0,'127.0.0.1',()=>{process.stdout.write(String(s.address().port));s.close()})")
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${SERVER_PID:-}" ]; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Write config file for isolated instance
|
||||
mkdir -p "$WORK_DIR/custom/conf"
|
||||
cat > "$WORK_DIR/custom/conf/app.ini" <<EOF
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = $WORK_DIR/data/gitea.db
|
||||
|
||||
[server]
|
||||
HTTP_PORT = $FREE_PORT
|
||||
ROOT_URL = http://localhost:$FREE_PORT
|
||||
STATIC_ROOT_PATH = $(pwd)
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
|
||||
[service]
|
||||
ENABLE_CAPTCHA = false
|
||||
|
||||
[log]
|
||||
MODE = console
|
||||
LEVEL = Warn
|
||||
EOF
|
||||
|
||||
export GITEA_WORK_DIR="$WORK_DIR"
|
||||
|
||||
# Start Gitea server
|
||||
echo "Starting Gitea server on port $FREE_PORT (workdir: $WORK_DIR)..."
|
||||
if [ -n "${GITEA_TEST_E2E_DEBUG:-}" ]; then
|
||||
"./$EXECUTABLE" web &
|
||||
else
|
||||
"./$EXECUTABLE" web > "$WORK_DIR/server.log" 2>&1 &
|
||||
fi
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to be reachable
|
||||
E2E_URL="http://localhost:$FREE_PORT"
|
||||
MAX_WAIT=120
|
||||
ELAPSED=0
|
||||
while ! curl -sf --max-time 5 "$E2E_URL" > /dev/null 2>&1; do
|
||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
echo "error: Gitea server process exited unexpectedly. Server log:" >&2
|
||||
cat "$WORK_DIR/server.log" 2>/dev/null >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||
echo "error: Gitea server not reachable after ${MAX_WAIT}s. Server log:" >&2
|
||||
cat "$WORK_DIR/server.log" 2>/dev/null >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
done
|
||||
|
||||
echo "Gitea server is ready at $E2E_URL"
|
||||
|
||||
GITEA_TEST_E2E_DOMAIN="e2e.gitea.com"
|
||||
GITEA_TEST_E2E_USER="e2e-admin"
|
||||
GITEA_TEST_E2E_PASSWORD="password"
|
||||
GITEA_TEST_E2E_EMAIL="$GITEA_TEST_E2E_USER@$GITEA_TEST_E2E_DOMAIN"
|
||||
|
||||
# Create admin test user
|
||||
"./$EXECUTABLE" admin user create \
|
||||
--username "$GITEA_TEST_E2E_USER" \
|
||||
--password "$GITEA_TEST_E2E_PASSWORD" \
|
||||
--email "$GITEA_TEST_E2E_EMAIL" \
|
||||
--must-change-password=false \
|
||||
--admin
|
||||
|
||||
export GITEA_TEST_E2E_URL="$E2E_URL"
|
||||
export GITEA_TEST_E2E_DOMAIN
|
||||
export GITEA_TEST_E2E_USER
|
||||
export GITEA_TEST_E2E_PASSWORD
|
||||
export GITEA_TEST_E2E_EMAIL
|
||||
|
||||
pnpm exec playwright test "$@"
|
||||
Loading…
x
Reference in New Issue
Block a user