From f76c0acf597e57bbeb5a8a64d69e92093a3c8eb7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Feb 2026 10:58:11 +0100 Subject: [PATCH] Rework e2e tests Remove the Go test harness that compiled test binaries and bootstrapped a full Gitea server with fixtures. Replace with a bash script that runs Playwright directly against an already-running Gitea instance. - Remove Go e2e test files (e2e_test.go, utils_e2e_test.go) - Add tools/test-e2e.sh that detects server URL, creates e2e user, runs Playwright - Simplify Makefile to single test-e2e target - Rewrite playwright.config.ts with chromium-only, no file outputs - Rewrite tests using semantic Playwright locators (getByLabel, getByRole, getByText) - Add login/logout utilities in tests/e2e/utils.ts - Add CI workflow for e2e tests (.github/workflows/pull-e2e-tests.yml) - Install only chromium in playwright install step Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pull-e2e-tests.yml | 56 +++++++++++++ .gitignore | 5 -- CONTRIBUTING.md | 2 +- Makefile | 66 ++------------- eslint.config.ts | 2 +- playwright.config.ts | 101 ++++------------------- tests/e2e/README.md | 86 -------------------- tests/e2e/e2e_test.go | 115 --------------------------- tests/e2e/example.test.e2e.ts | 56 ------------- tests/e2e/login.test.ts | 20 +++++ tests/e2e/utils.ts | 24 ++++++ tests/e2e/utils_e2e.ts | 62 --------------- tests/e2e/utils_e2e_test.go | 56 ------------- tools/test-e2e.sh | 47 +++++++++++ 14 files changed, 170 insertions(+), 528 deletions(-) create mode 100644 .github/workflows/pull-e2e-tests.yml delete mode 100644 tests/e2e/README.md delete mode 100644 tests/e2e/e2e_test.go delete mode 100644 tests/e2e/example.test.e2e.ts create mode 100644 tests/e2e/login.test.ts create mode 100644 tests/e2e/utils.ts delete mode 100644 tests/e2e/utils_e2e.ts delete mode 100644 tests/e2e/utils_e2e_test.go create mode 100755 tools/test-e2e.sh diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml new file mode 100644 index 0000000000..1d255abb46 --- /dev/null +++ b/.github/workflows/pull-e2e-tests.yml @@ -0,0 +1,56 @@ +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-backend + - run: make backend + env: + TAGS: bindata sqlite sqlite_unlock_notify + - run: make deps-frontend + - run: make frontend + - run: make playwright + - run: | + mkdir -p custom/conf + cat <<'EOF' > custom/conf/app.ini + [database] + DB_TYPE = sqlite3 + + [server] + HTTP_PORT = 3000 + ROOT_URL = http://localhost:3000 + + [security] + INSTALL_LOCK = true + EOF + - run: ./gitea web & + - run: GITEA_URL=http://localhost:3000 make test-e2e + timeout-minutes: 10 diff --git a/.gitignore b/.gitignore index aa08e47aec..4da2d797eb 100644 --- a/.gitignore +++ b/.gitignore @@ -69,11 +69,6 @@ cpu.out /public/assets/img/avatar /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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c64d91a7eb..91d9f53dd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -178,7 +178,7 @@ 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 | | ## Translation diff --git a/Makefile b/Makefile index 93d87fc139..1e34b8f2a9 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(G 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) @@ -173,10 +173,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 @@ -207,7 +203,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" @@ -226,13 +222,9 @@ clean-all: clean ## delete backend, frontend and integration files clean: ## delete backend and integration files rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \ integrations*.test \ - e2e*.test \ tests/integration/gitea-integration-* \ tests/integration/indexers-* \ tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \ - tests/e2e/gitea-e2e-*/ \ - tests/e2e/indexers-*/ \ - tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ .PHONY: fmt fmt: ## format the Go and template code @@ -563,47 +555,11 @@ 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= + $(NODE_VARS) pnpm exec playwright install --with-deps chromium $(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=$(EXECUTABLE) bash tools/test-e2e.sh $(E2E_FLAGS) .PHONY: bench-sqlite bench-sqlite: integrations.sqlite.test generate-ini-sqlite @@ -699,18 +655,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 diff --git a/eslint.config.ts b/eslint.config.ts index 5815702c89..3020c757ca 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -910,7 +910,7 @@ export default defineConfig([ }, { ...playwright.configs['flat/recommended'], - files: ['tests/e2e/**'], + files: ['tests/e2e/*.test.ts'], rules: { ...playwright.configs['flat/recommended'].rules, }, diff --git a/playwright.config.ts b/playwright.config.ts index 9e3396465a..79979ad931 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,98 +1,29 @@ -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. */ + 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 ? 30000 : 10000, + expect: { + timeout: env.CI ? 15000 : 5000, + }, + use: { + baseURL: env.GITEA_TEST_SERVER_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000', + locale: 'en-US', + trace: 'off', + screenshot: 'off', + video: 'off', }, - - /* Configure projects for major browsers */ projects: [ { name: 'chromium', - - /* Project-specific settings. */ use: { ...devices['Desktop Chrome'], - }, - }, - - // disabled because of https://github.com/go-gitea/gitea/issues/21355 - // { - // name: 'firefox', - // use: { - // ...devices['Desktop Firefox'], - // }, - // }, - - { - name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, - }, - - /* Test against mobile viewports. */ - { - name: 'Mobile Chrome', - use: { - ...devices['Pixel 5'], - }, - }, - { - name: 'Mobile Safari', - use: { - ...devices['iPhone 12'], + permissions: ['clipboard-read', 'clipboard-write'], }, }, ], - - /* 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; +}); diff --git a/tests/e2e/README.md b/tests/e2e/README.md deleted file mode 100644 index ea3805ab95..0000000000 --- a/tests/e2e/README.md +++ /dev/null @@ -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. diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go deleted file mode 100644 index 95093ffd29..0000000000 --- a/tests/e2e/e2e_test.go +++ /dev/null @@ -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(false) - 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()) - }) - }) - } -} diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts deleted file mode 100644 index 1689f1b8ef..0000000000 --- a/tests/e2e/example.test.e2e.ts +++ /dev/null @@ -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); -}); diff --git a/tests/e2e/login.test.ts b/tests/e2e/login.test.ts new file mode 100644 index 0000000000..307f64c52c --- /dev/null +++ b/tests/e2e/login.test.ts @@ -0,0 +1,20 @@ +import {test, expect} from '@playwright/test'; +import {login, logout, login_user} from './utils.ts'; + +test('homepage', async ({page}) => { + const response = await page.goto('/'); + expect(response?.status()).toBe(200); + await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg'); +}); + +test('logged in user', async ({browser}) => { + const context = await login_user(browser, 'e2e'); + const page = await context.newPage(); + const response = await page.goto('/'); + expect(response?.status()).toBe(200); +}); + +test('login and logout', async ({page}) => { // eslint-disable-line playwright/expect-expect + await login(page); + await logout(page); +}); diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts new file mode 100644 index 0000000000..3262c3bf87 --- /dev/null +++ b/tests/e2e/utils.ts @@ -0,0 +1,24 @@ +import {expect} from '@playwright/test'; +import type {Browser, Page} from '@playwright/test'; + +const LOGIN_PASSWORD = 'password'; + +export async function login(page: Page, user: string = 'e2e') { + await page.goto('/user/login'); + await page.getByLabel('Username or Email Address').fill(user); + await page.getByLabel('Password').fill(LOGIN_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.getByText('Sign Out').dispatchEvent('click'); + await expect(page.getByRole('link', {name: 'Sign In'})).toBeVisible(); +} + +export async function login_user(browser: Browser, user: string) { + const context = await browser.newContext(); + const page = await context.newPage(); + await login(page, user); + return context; +} diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts deleted file mode 100644 index 0973f0838c..0000000000 --- a/tests/e2e/utils_e2e.ts +++ /dev/null @@ -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'), - ], - }); - } -} diff --git a/tests/e2e/utils_e2e_test.go b/tests/e2e/utils_e2e_test.go deleted file mode 100644 index 5ba05f3453..0000000000 --- a/tests/e2e/utils_e2e_test.go +++ /dev/null @@ -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...) -} diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh new file mode 100755 index 0000000000..c59ecce458 --- /dev/null +++ b/tools/test-e2e.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -euo pipefail + +# Determine the Gitea server URL, either from GITEA_URL env var or from custom/conf/app.ini +if [ -n "${GITEA_URL:-}" ]; then + GITEA_TEST_SERVER_URL="$GITEA_URL" +else + INI_FILE="custom/conf/app.ini" + if [ ! -f "$INI_FILE" ]; then + echo "error: $INI_FILE not found and GITEA_URL not set" >&2 + echo "Either start Gitea with a config or set GITEA_URL explicitly:" >&2 + echo " GITEA_URL=http://localhost:3000 make test-e2e" >&2 + exit 1 + fi + ROOT_URL=$(sed -n 's/^ROOT_URL\s*=\s*//p' "$INI_FILE" | tr -d '[:space:]') + if [ -z "$ROOT_URL" ]; then + echo "error: ROOT_URL not found in $INI_FILE" >&2 + exit 1 + fi + GITEA_TEST_SERVER_URL="$ROOT_URL" +fi + +echo "Using Gitea server: $GITEA_TEST_SERVER_URL" + +# Verify server is reachable +if ! curl -sf --max-time 5 "$GITEA_TEST_SERVER_URL" > /dev/null 2>&1; then + echo "error: Gitea server at $GITEA_TEST_SERVER_URL is not reachable" >&2 + echo "Start Gitea first: ${EXECUTABLE:-./gitea}" >&2 + exit 1 +fi + +# Create e2e test user if it does not already exist +E2E_USER="e2e" +E2E_EMAIL="e2e@test.gitea.io" +E2E_PASSWORD="password" +if ! curl -sf --max-time 5 "$GITEA_TEST_SERVER_URL/api/v1/users/$E2E_USER" > /dev/null 2>&1; then + echo "Creating e2e test user..." + if ${EXECUTABLE:-./gitea} admin user create --username "$E2E_USER" --email "$E2E_EMAIL" --password "$E2E_PASSWORD" --must-change-password=false 2>/dev/null; then + echo "User '$E2E_USER' created" + else + echo "error: failed to create user '$E2E_USER'" >&2 + exit 1 + fi +fi + +export GITEA_TEST_SERVER_URL +exec pnpm exec playwright test "$@"