!111 基于PlayWright端到端测试,make e2e-test测试devcontainer

1. PlayWright测试脚本
2. 执行make e2e-test测试
3. 把与e2e测试相关的代码都放在了/test/e2e下
This commit is contained in:
孟宁
2025-11-16 01:29:35 +00:00
repo.diff.committed_by Gitee
repo.diff.parent 28adf2541d dfcf75d7d1
repo.diff.commit cfffaa1960
repo.diff.stats_desc%!(EXTRA int=15, int=606, int=217)

repo.diff.view_file

@@ -60,6 +60,7 @@ cpu.out
/tests/e2e/reports
/tests/e2e/test-artifacts
/tests/e2e/test-snapshots
/tests/e2e/test-data
/tests/*.ini
/node_modules
/yarn.lock

1
.gitignore repo.diff.vendored
repo.diff.view_file

@@ -69,6 +69,7 @@ cpu.out
/tests/e2e/gitea-e2e-*
/tests/e2e/indexers-*
/tests/e2e/reports
/tests/e2e/test-data
/tests/e2e/test-artifacts
/tests/e2e/test-snapshots
/tests/*.ini

repo.diff.view_file

@@ -939,11 +939,23 @@ devstar:
echo "Successfully build devstar.cn/devstar/devstar-runtime-container:latest"; \
fi
docker build -t devstar-studio:latest -f docker/Dockerfile.devstar .
.PHONY: docker
docker:
docker build --disable-content-trust=false -t $(DOCKER_REF) .
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify"
ifeq ($(TARGET_URL),)
E2E_DEPS = devstar
else
E2E_DEPS =
endif
.PHONY: e2e-test
e2e-test: $(E2E_DEPS)
@echo "正在启动E2E-TEST..."
@TARGET_URL=$(TARGET_URL) E2E_USERNAME=$(E2E_USERNAME) E2E_PASSWORD=$(E2E_PASSWORD) ./tests/e2e/run-e2e-tests.sh
# This endif closes the if at the top of the file
endif

repo.diff.view_file

@@ -1,98 +1,52 @@
import {devices} from '@playwright/test';
import {env} from 'node:process';
import type {PlaywrightTestConfig} from '@playwright/test';
import { devices } from '@playwright/test';
import { env } from 'node:process';
import type { PlaywrightTestConfig } from '@playwright/test';
const BASE_URL = env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000';
const BASE_URL = env.DEVSTAR_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000';
export default {
testDir: './tests/e2e/',
testMatch: /.*\.test\.e2e\.ts/, // Match any .test.e2e.ts files
/* Maximum time one test can run for. */
timeout: 30 * 1000,
const config: PlaywrightTestConfig = {
testDir: './specs',
testMatch: /specs\/.*\.test\.ts/,
timeout: 500000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 2000,
timeout: 15000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
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'}]],
reporter: env.CI ? 'list' : [['list'], ['html', {
outputFolder: 'playwright-report/html',
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
headless: true,
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 */
actionTimeout: 15000,
navigationTimeout: 15000,
baseURL: BASE_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
/* 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'],
},
},
],
outputDir: 'playwright-report/test-artifacts/',
snapshotDir: 'playwright-report/test-snapshots/',
};
/* 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;
const skipInstall = env.E2E_SKIP_INSTALL === 'true';
if (skipInstall) {
console.log(`已跳过 global-setup.`);
} else {
console.log(`[Playwright Config] 探测结果: E2E_SKIP_INSTALL is not 'true'. 已启用 global-setup.`);
config.globalSetup = require.resolve('./global-setup.ts');
}
export default config satisfies PlaywrightTestConfig;

11
tests/e2e/.dockerignore Normal file
repo.diff.view_file

@@ -0,0 +1,11 @@
# 1. 忽略 Node.js 依赖目录
/node_modules
# 2. 忽略本地的测试报告和结果
/reports
/test-data
/test-artifacts
/Readme.md
/utils_e2e.ts
/utils_e2e_test.go
/e2e_test.go

23
tests/e2e/Dockerfile Normal file
repo.diff.view_file

@@ -0,0 +1,23 @@
#
# 这是 "test-runner" 服务的 Dockerfile
#
# 1. 使用微软官方的 Playwright 镜像
# 它已经内置了所有浏览器 (Chromium, Firefox, WebKit) 和操作系统依赖
FROM mcr.microsoft.com/playwright:v1.53.2-jammy
# 2. 设置工作目录
WORKDIR /app
# 3. 复制 "依赖清单" 文件
COPY package*.json ./
# 4. 安装npm 依赖 (即 @playwright/test)
RUN npm install
# 5. 复制所有的测试代码到容器中
# (包括 playwright.config.ts, global-setup.ts 和 specs/ 目录)
COPY . .
# 6. 默认命令
CMD ["npx", "playwright", "test"]

repo.diff.view_file

@@ -1,92 +1,20 @@
# 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
```
npx playwright install-deps
```
## Run all tests via local act_runner
```
act_runner exec -W ./.github/workflows/pull-e2e-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest
```
## 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:
##E2E端到端测试流程
```
make test-e2e-sqlite#example
```
For other databases(replace `mssql` to `mysql` or `pgsql`):
make devstar
```
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example
```
public/assets/install.sh start --image=devstar-studio:latest
## Visual testing
make e2e-test TARGET_URL="..." # 使用默认账号testuser 密码12345678
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.
make e2e-test TARGET_URL="..." E2E_USERNAME="your_name" E2E_PASSWORD="your_password" # 使用你的账号和密码
``
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.
##注意url不可以是localhost这样容器无法访问也无法正常安装webterminal
##说明:目前有两种测试的用法,
1.主要的流程是通过make devstar 本地代码构建镜像public/assets/install.sh start --image=devstar-studio:latest 通过install.sh脚本创建容器并在make e2e-test TARGET_URL="..."中输入devstar容器的url如果首次安装会执行安装脚本如果已经安装过请输入你的账号密码否则按默认账号和密码登录。
2.单机CI使用make e2e-test由脚本执行容器的创建安装和测试需要docker环境和项目代码。

repo.diff.view_file

@@ -0,0 +1,5 @@
services:
test-runner:
depends_on:
devstar:
condition: service_healthy

repo.diff.view_file

@@ -0,0 +1,43 @@
version: '3.8'
services:
# 服务一: DevStar
devstar:
#现在make devstar负责构建镜像
image: devstar-studio:latest
pull_policy: never
ports:
- "80:3000"
- "2222:2222"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./test-data/devstar_data:/var/lib/gitea
- ./test-data/devstar_data:/etc/gitea
# 健康检查。test-runner 会等待这个检查通过
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/ || exit 1"]
interval: 10s
timeout: 5s
retries: 30
# 服务二: Playwright
test-runner:
# 从 'tests/' 目录下的 Dockerfile 构建
build:
context: .
# 等待 devstar 的 "healthcheck" 通过后,才启动
user: "${CURRENT_UID}:${CURRENT_GID}"
environment:
- DEVSTAR_URL=${DEVSTAR_URL}
- E2E_SKIP_INSTALL=${E2E_SKIP_INSTALL}
- E2E_USERNAME=${E2E_USERNAME}
- E2E_PASSWORD=${E2E_PASSWORD}
- E2E_MODE=${E2E_MODE}
volumes:
# 也挂载 Docker Socket
- /var/run/docker.sock:/var/run/docker.sock
- ../../playwright.config.ts:/app/playwright.config.ts
# 将测试报告写回到宿主机的 ./reports 目录
- ./reports:/app/playwright-report
# 覆盖默认命令,强制运行测试并生成我们想要的报告
command: >
npx playwright test

repo.diff.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);
});

68
tests/e2e/global-setup.ts Normal file
repo.diff.view_file

@@ -0,0 +1,68 @@
import { chromium, type FullConfig } from '@playwright/test';
import { env } from 'node:process';
import { URL } from 'url';
async function globalSetup(config: FullConfig) {
const mode = env.E2E_MODE;
const {baseURL} = config.projects[0].use;
const isInstalledMode=env.E2E_SKIP_INSTALL;
const DEFAULT_E2E_USER = 'testuser';
const DEFAULT_E2E_PASS = '12345678';
if (!baseURL) {
throw new Error('[GlobalSetup] 致命错误: baseURL 或 storageState 未定义!');
}
const browser = await chromium.launch();
const page = await browser.newPage();
if (mode === 'url') {
try {
const url1=env.DEVSTAR_URL;
await page.goto(url1, { timeout: 15000 });
console.log('[GlobalSetup] 检测到安装界面!正在开始自动化安装...');
await page.getByText('Server and Third-Party Service Settings').click();
await page.getByRole('checkbox', { name: 'Enable user sign-in via Wechat QR Code.' }).uncheck();
await page.getByRole('checkbox', { name: 'Require a CAPTCHA for user' }).uncheck();
await page.getByText('Administrator Account Settings').click();
await page.getByRole('textbox', { name: 'Administrator Username' }).fill('testuser');
await page.getByRole('textbox', { name: 'Email Address' }).fill('ilovcatlyn750314@gmail.com');
await page.getByRole('textbox', { name: 'Password', exact: true }).fill('12345678');
await page.getByRole('textbox', { name: 'Confirm Password' }).fill('12345678');
await page.getByRole('button', { name: 'Install Gitea'}).click();
await page.waitForTimeout(90000);
} catch (error) {
console.error('[GlobalSetup] "URL 模式" 登录失败:', error);
await page.screenshot({ path: 'playwright-report/global-setup-login-failure.png' });
throw error;
}
} else if (mode === 'compose') {
console.log('[GlobalSetup] 模式: 构建(Compose). 正在执行构建模式的安装脚本...');
try {
const hostGateway = env.DOCKER_HOST_GATEWAY || '172.17.0.1';
const giteaBaseURL = `http://${hostGateway}:80`;
const serverDomain = hostGateway;
const browser = await chromium.launch();
await page.goto(baseURL, { timeout: 15000 });
console.log('[GlobalSetup] 检测到安装界面!正在开始自动化安装...');
await page.getByRole('textbox', { name: 'Server Domain *' }).fill(serverDomain);
await page.getByRole('textbox', { name: 'Gitea Base URL *' }).fill(giteaBaseURL);
await page.getByText('Server and Third-Party Service Settings').click();
await page.getByRole('checkbox', { name: 'Enable user sign-in via Wechat QR Code.' }).uncheck();
await page.getByRole('checkbox', { name: 'Require a CAPTCHA for user' }).uncheck();
await page.getByText('Administrator Account Settings').click();
await page.getByRole('textbox', { name: 'Administrator Username' }).fill('testuser');
await page.getByRole('textbox', { name: 'Email Address' }).fill('ilovcatlyn750314@gmail.com');
await page.getByRole('textbox', { name: 'Password', exact: true }).fill('12345678');
await page.getByRole('textbox', { name: 'Confirm Password' }).fill('12345678');
await page.getByRole('button', { name: 'Install Gitea'}).click();
console.log("安装中,请耐心等待");
await page.waitForTimeout(90000);
} catch (error) {
console.error('[GlobalSetup] "构建模式" 安装失败:', error);
await page.screenshot({ path: 'playwright-report/global-setup-failure.png' });
throw error;
}
} else {
throw new Error(`[GlobalSetup] 未知的 E2E_MODE: "${mode}"`);
}
}
export default globalSetup;

20
tests/e2e/package.json Normal file
repo.diff.view_file

@@ -0,0 +1,20 @@
{
"name": "devstar-e2e-runner",
"version": "1.0.0",
"description": "Isolated E2E test runner for DevStar Studio",
"main": "index.js",
"scripts": {
"test": "npx playwright test"
},
"keywords": [
"playwright",
"e2e",
"devstar"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "1.53.2"
},
"dependencies": {}
}

118
tests/e2e/run-e2e-tests.sh Executable file
repo.diff.view_file

@@ -0,0 +1,118 @@
#!/bin/bash
# 这是一个“一键运行”E2E 测试的脚本
# 它会处理所有清理、权限、拉取和执行工作
# 任何命令失败立即退出
set -e
#
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
#
PROJECT_ROOT=$( cd -- "$SCRIPT_DIR/../.." &> /dev/null && pwd )
export CURRENT_UID=$(id -u)
export CURRENT_GID=$(id -g)
#
cd "$PROJECT_ROOT"
echo "===== [1/5] 清理旧的测试环境... ====="
# 彻底销毁旧的 compose 环境,-v 会删除关联的数据卷
docker compose -f tests/e2e/docker-compose.test.yml down -v --remove-orphans
docker image prune -f
# 清理并重建报告和数据目录
rm -rf ./tests/e2e/reports ./tests/e2e/test-data
mkdir -p ./tests/e2e/reports/html ./tests/e2e/test-data/devstar_data
echo "清理完成。"
echo ""
echo "===== [2/5] 设置权限... ====="
# 容器内的用户需要写入数据目录
chmod -R 777 ./tests/e2e/test-data/devstar_data
# 【关键】允许容器访问宿主机的 Docker Socket
if [ -z "$TARGET_URL" ]; then
sudo chmod 666 /var/run/docker.sock
echo "权限设置完成。"
echo ""
else
echo "跳过docker.sock权限修改 ====="
fi
if [ -z "$TARGET_URL" ]; then
echo "===== [3/5] 构建模式: 构建/拉取依赖镜像... ====="
#现在make devstar 处理镜像的构建
docker pull mengning997/webterminal:latest
docker tag mengning997/webterminal:latest devstar.cn/devstar/webterminal:latest
echo "===== 现在检测Docker宿主机网关====="
DOCKER_HOST_GATEWAY="172.17.0.1"
if [ "$OS_TYPE" = "Darwin" ]; then
DOCKER_HOST_GATEWAY="host.docker.internal"
elif [ "$OS_TYPE" = "Linux" ]; then
LINUX_IP=$(ip addr show docker0 | grep -Po 'inet \K[\d\.]+')
if [ -n "$LINUX_IP" ]; then
DOCKER_HOST_GATEWAY=$LINUX_IP
else
echo "警告: 无法动态检测 'docker0' IP, 回退到 172.17.0.1"
fi
fi
export DOCKER_HOST_GATEWAY
else
echo "===== [3/5] URL 模式: 跳过依赖镜像和网关检测。 ====="
fi
echo "===== [4/5] 启动并运行测试... ====="
# 检查从 Makefile 传来的 TARGET_URL 变量是否为空
if [ -n "$TARGET_URL" ]; then
# --- [A] URL 模式 ---
echo " 模式: [URL模式]. 目标: $TARGET_URL"
export DEVSTAR_URL=$TARGET_URL
export E2E_MODE="url"
echo " 正在检查安装状态..."
PATH_TO_CHECK="/user/login"
EXPECTED_CODE_IF_INSTALLED="200"
PROBE_URL="${TARGET_URL}${PATH_TO_CHECK}"
HTTP_CODE=$(curl -L -s -o /dev/null -w "%{http_code}" "$PROBE_URL")
if [ "$HTTP_CODE" -eq "$EXPECTED_CODE_IF_INSTALLED" ]; then
echo " 探测结果: 目标已安装 (在 ${PROBE_URL} 收到 HTTP ${HTTP_CODE})."
export E2E_SKIP_INSTALL="true"
export E2E_USERNAME
export E2E_PASSWORD
export E2E_ADMIN_ID
else
echo "没有安装,下面执行安装脚本! "
export E2E_SKIP_INSTALL="false"
fi
command docker compose \
-f tests/e2e/docker-compose.test.yml \
up \
--build \
--exit-code-from test-runner \
test-runner
else
# --- [B] 构建模式 ---
echo "==> 模式: [构建模式]. 正在本地启动 devstar..."
# 1. 导出内部 URL
export DEVSTAR_URL="http://devstar:3000"
export E2E_MODE="compose"
export E2E_SKIP_INSTALL="false"
echo "即将执行: docker compose -f tests/e2e/docker-compose.test.yml up --build --wait --exit-code-from test-runner"
# 2. 启动所有服务
command docker compose \
-f tests/e2e/docker-compose.test.yml \
-f tests/e2e/docker-compose.override.yml \
up \
--build \
--abort-on-container-exit \
--exit-code-from test-runner
fi
# 捕获 test-runner 的退出码
EXIT_CODE=$?
echo ""
echo ""
echo "===== [5/5] 测试运行完成 ====="
echo "HTML 报告已生成在: ./reports/html"
ls -l ./tests/e2e/reports/html
echo ""
# 以 test-runner 的退出码退出
exit $EXIT_CODE

repo.diff.view_file

@@ -0,0 +1,186 @@
import { test, expect } from '@playwright/test';
import { link } from 'node:fs';
import { env } from 'node:process';
import { Login } from './utils.e2e';
const DEFAULT_E2E_USER = 'testuser';
const DEFAULT_E2E_PASS = '12345678';
const DEFAULT_ADMIN_ID = '1';
const isAlreadyInstalled = env.E2E_SKIP_INSTALL === 'true';
const url1=env.DEVSTAR_URL;
const GITEA_URL = (env.E2E_MODE === 'url') ? url1 : 'http://devstar:3000';
const TEST_USER = isAlreadyInstalled ? (env.E2E_USERNAME || DEFAULT_E2E_USER) : DEFAULT_E2E_USER;
const TEST_PASS = isAlreadyInstalled ? (env.E2E_PASSWORD || DEFAULT_E2E_PASS) : DEFAULT_E2E_PASS;
const TEST_ADMIN_USER_ID = isAlreadyInstalled ? (env.E2E_ADMIN_ID || DEFAULT_ADMIN_ID) : DEFAULT_ADMIN_ID;
const repoName='e2e-test-repo';
test.describe('devcontainer 相关测试',()=>{
test.beforeEach(async ({ page }) => {
await Login(page);
});
test('DevContainer 功能和配置', async ({ page,context }) => {
console.log(`正在创建新仓库: ${repoName}`);
await page.goto(GITEA_URL + '/repo/create');
await page.fill('input[name="repo_name"]', repoName);
await page.getByRole('button', { name: 'Create Repository' }).click();
await expect(page).toHaveURL(GITEA_URL + '/' + TEST_USER + '/' + repoName);
console.log("仓库创建成功.");
await expect(page).toHaveURL(GITEA_URL + '/' + TEST_USER + '/' + repoName);
console.log("正在点击 'Dev Container' 标签页...");
await page.getByRole('link', { name: 'Dev Container' }).click();
console.log("正在点击 'Create' (创建模板) 按钮...");
await page.getByRole('button', { name: /Create/i }).click();
await page.waitForTimeout(10000);
console.log("模板已创建. 正在点击 'Edit' 按钮...");
await expect(page.getByText('devcontainer.json')).toBeVisible();
await page.getByRole('link', { name: 'Edit' }).click();
await page.waitForTimeout(5000);
console.log("已跳转到编辑器. 正在修改内容");
const newJsonAsObject = {
"name": "Gitea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "lts"
},
"ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
"ghcr.io/devcontainers-extra/features/poetry:2": {},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.12"
},
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
},
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"golang.go",
"stylelint.vscode-stylelint",
"DavidAnson.vscode-markdownlint",
"Vue.volar",
"ms-azuretools.vscode-docker",
"vitest.explorer",
"cweijan.vscode-database-client2",
"GitHub.vscode-pull-request-github",
"Azurite.azurite"
]
}
},
"portsAttributes": {
"3000": {
"label": "Gitea Web",
"onAutoForward": "notify"
}
},
"postCreateCommand": "make deps"
};
//转换为JSON 字符串
const newJsonString = JSON.stringify(newJsonAsObject);
// 设置焦点
await page.locator('.view-lines > div:nth-child(20)').click();
console.log("正在手动删除模板内容 ");
for (let i = 0; i < 500; i++) {
await page.keyboard.press('Backspace');
}
//粘贴字符串
console.log("正在粘贴JSON 内容...");
await page.keyboard.insertText(newJsonString);
await page.getByRole('button', { name: 'Commit Changes' }).click();
console.log("devcontainer.json 修改并提交成功.");
console.log("正在导航回 Dev Container 标签页进行验证...");
await page.getByRole('link', { name: 'Dev Container' }).click();
console.log("创建开发容器");
await page.getByRole('button', { name: 'Create Dev Container' }).click();
console.log("正在创建");
await expect(page.getByRole('button', { name: 'Stop Dev Container' }))
.toBeVisible({ timeout: 180000 });
console.log("Dev container 创建成功!");
await page.getByRole('button',{ name: 'Stop Dev Container'}).click();
console.log("正在停止");
await expect(page.getByRole('button',{ name: 'Start Dev Container'})).toBeVisible({ timeout: 180000});
console.log("成功停止开发容器!");
await page.getByRole('button',{name: 'Start Dev Container'}).click();
console.log("正在启动开发容器");
await expect(page.getByRole('button',{name:'Stop Dev Container'})).toBeVisible({ timeout: 180000});
console.log("成功启动!");
console.log("保存开发容器");
const pagePromise = context.waitForEvent('page');
console.log("打开webterminal");
await page.getByRole('link',{name: 'open with WebTerminal'}).click();
const newPage = await pagePromise;
await newPage.waitForLoadState(); // 等待新页面加载完成
console.log("Web Terminal: 新标签页已打开!");
//await expect(newPage.getByText('Successfully connected to the container')).toBeVisible(); //这里ttyd里的信息PlayWright看不见容器的交互没办法自动化测试
await page.getByRole('link', { name: 'Delete Dev Container' }).click();
await page.locator('#delete-repo-devcontainer-of-user-modal')
.getByRole('button', { name: 'Yes' })
.click();
console.log('正在删除!');
await expect(page.getByRole('button', { name: 'Create Dev Container' })).toBeVisible();
console.log('成功删除!');
console.log("test1 检查通过!");
//console.log("test2仓库");
//await page.goto(GITEA_URL + '/'+ TEST_REPO_EMPTY);
//await page.getByRole('link', { name: 'Dev Container' }).click();
//await expect(page.getByText('Oops, it looks like there is no Dev Container Setting in this repository.')).toBeVisible();
//console.log("test2 检查通过");
//console.log("test3仓库");
//await page.goto(GITEA_URL+ '/' + TEST_REPO_INVALID);
//await page.getByRole('link', { name: 'Dev Container' }).click();
//await expect(page.getByText(' Invalid Dev Container Configuration')).toBeVisible();
//console.log("test3检查通过");
});
test('权限修改相关', async ({ page }) => {
console.log('权限配置');
await page.getByRole('link', { name: 'Site Administration' }).click();
await page.getByText('Identity & Access').click();
await page.getByRole('link', { name: 'User Accounts' }).click();
await page.getByRole('row')
.filter({ hasText: TEST_USER })
.getByRole('link', { name: 'Edit' })
.click();
const devContainerCheckbox = page.getByLabel(/May Create Devcontainers/i);
await devContainerCheckbox.uncheck();
await page.getByRole('button', { name: 'Update User Account' }).click();
await page.goto(GITEA_URL + '/' + "e2e-devcontainer-test");
const devContainerLink = page.getByRole('link', { name: 'Dev Container' });
await expect(devContainerLink).toBeHidden({ timeout: 10000 });
console.log('权限设置成功!');
console.log('现在恢复原环境');
await page.goto(GITEA_URL+ '/-/admin/users/' + TEST_ADMIN_USER_ID + '/edit');
await devContainerCheckbox.check();
await page.getByRole('button', { name: 'Update User Account' }).click();
console.log('现在清理测试仓库');
console.log("正在导航到仓库设置页面...");
await page.goto(GITEA_URL + '/' + TEST_USER + '/' + repoName + '/settings');
console.log("正在点击 'Delete This Repository' 按钮...");
await page.getByRole('button', { name: 'Delete This Repository' }).click();
await page.locator('#delete-repo-modal').waitFor();
console.log(`正在输入 '${repoName}' 进行确认...`);
await page.locator('#repo_name_to_delete').fill(repoName);
console.log("正在点击最终的删除确认按钮...");
await page.getByRole('button', { name: 'Delete Repository' }).click();
});
})

repo.diff.view_file

@@ -0,0 +1,75 @@
import { type Page, expect } from '@playwright/test';
import { env } from 'node:process';
const DEFAULT_E2E_USER = 'testuser';
const DEFAULT_E2E_PASS = '12345678';
const mode=env.E2E_MODE;
export async function Login(page: Page) {
const isInstalled = env.E2E_SKIP_INSTALL === 'true';
let username: string | undefined;
let password: string | undefined;
let wasUsingDefault = false;
if(mode === 'url'){
const url1=env.DEVSTAR_URL;
if (isInstalled) {
// "已安装" 模式
username = env.E2E_USERNAME || DEFAULT_E2E_USER;
password = env.E2E_PASSWORD || DEFAULT_E2E_PASS;
if (!env.E2E_USERNAME) {
wasUsingDefault = true;
}
console.log(`"已安装"模式, 尝试用 ${username} 登录...`);
} else {
// "未安装" 模式
username = DEFAULT_E2E_USER;
password = DEFAULT_E2E_PASS;
wasUsingDefault=true;
console.log(` "刚安装"模式, 尝试用 ${username} 登录...`);
}
try {
await page.goto(url1 + '/user/login');
const captchaInput = page.locator('input[name="captcha"]');
if (await captchaInput.isVisible()) {
throw new Error('检测到验证码 (CAPTCHA)! E2E 测试无法继续。');
}
await page.fill('#user_name',username);
await page.fill('#password', password);
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL(url1+ '/');
console.log(`[LoginHelper] 用户 [${username}] 登录成功!`);
} catch (error) {
console.error(`[LoginHelper] 登录失败! 原始错误: ${error.message}`);
let hint: string;
if (error.message.includes('CAPTCHA')) {
// 提示 1: 验证码
hint = `请禁用验证码!\n` ;
} else if (wasUsingDefault) {
// 提示 2: 你没输入, 且默认值失败了
hint = `1. 登录失败, 且你没有提供 README.md 里描述的环境变量。\n` +
`2. 脚本自动尝试了默认用户 (${DEFAULT_E2E_USER}),但失败了。\n` +
`3. 请检查默认用户 (${DEFAULT_E2E_USER}) 在该目标上是否存在, 且密码是否正确。`;
} else {
hint = `1. 登录失败, 你提供了 E2E_USERNAME (${username})。\n` +
`2. 请检查你传入的 E2E_USERNAME 和 E2E_PASSWORD 环境变量是否正确。`;
}
throw new Error(
`[LoginHelper] 登录失败。\n\n${hint}\n`
);
}
}
else if(mode=='compose'){
await page.fill('#user_name',username);
await page.fill('#password', password);
await page.getByRole('button', { name: 'Sign In' }).click();
await expect(page).toHaveURL(env.DEVSTAR_URL+ '/');
console.log(`[LoginHelper] 登录成功!`);
}
}