This commit is contained in:
jiaojm
2025-11-12 13:14:18 +08:00
repo.diff.parent 087519a372
repo.diff.commit 5cba473dd8
repo.diff.stats_desc%!(EXTRA int=12, int=348, int=90)

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
@@ -69,6 +70,8 @@ cpu.out
/public/assets/css
/public/assets/fonts
/public/assets/img/avatar
/reports/
/test-data/
/vendor
/VERSION
/.air

repo.diff.view_file

@@ -1,6 +0,0 @@
export GITEA_URL=""
export TEST_USER=""
export TEST_PASS=""
export TEST_ADMIN_USER_ID="1"

3
.gitignore repo.diff.vendored
repo.diff.view_file

@@ -69,11 +69,14 @@ 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
/tests/**/*.git/**/*.sample
/node_modules
/test-data
/reports
/.venv
/yarn.lock
/yarn-error.log

repo.diff.view_file

@@ -925,6 +925,11 @@ 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" .
.PHONY: e2e-test
e2e-test:
@echo "正在启动E2E-TEST..."
@./run-e2e-tests.sh
# This endif closes the if at the top of the file
endif

50
docker-compose.test.yml Normal file
repo.diff.view_file

@@ -0,0 +1,50 @@
version: '3.8'
services:
# 服务一: DevStar
devstar:
# 我们不再拉取镜像
# image: mengning997/devstar-studio:latest
# pull_policy: always
# 我们告诉 Compose 在本地构建
build:
context: .
dockerfile: docker/Dockerfile.devstar #
image: devstar-e2e-test:latest #
pull_policy: never
ports:
- "80:3000"
- "2222:2222"
volumes:
# 挂载 Docker Socket允许 DevStar 创建 Devcontainer
- /var/run/docker.sock:/var/run/docker.sock
# 挂载数据卷,使用相对路径,保证测试环境可移植
- ./tests/e2e/test-data/devstar_data:/var/lib/gitea
- ./tests/e2e/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: ./tests/e2e
# 等待 devstar 的 "healthcheck" 通过后,才启动
depends_on:
devstar:
condition: service_healthy
environment:
# 将 DevStar 的 URL 传给 Playwright
- DEVSTAR_URL=http://devstar:3000
volumes:
# 也挂载 Docker Socket
- /var/run/docker.sock:/var/run/docker.sock
# 将测试报告写回到宿主机的 ./reports 目录
- ./tests/e2e/reports:/app/playwright-report
# 覆盖默认命令,强制运行测试并生成我们想要的报告
command: >
npx playwright test

56
run-e2e-tests.sh Executable file
repo.diff.view_file

@@ -0,0 +1,56 @@
#!/bin/bash
# 这是一个“一键运行”E2E 测试的脚本
# 它会处理所有清理、权限、拉取和执行工作
# 任何命令失败立即退出
set -e
echo "===== [1/5] 清理旧的测试环境... ====="
# 彻底销毁旧的 compose 环境,-v 会删除关联的数据卷
docker compose -f docker-compose.test.yml down -v --remove-orphans
docker image prune -f
docker builder prune -f
# 清理并重建报告和数据目录
sudo 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] 设置权限... ====="
# 容器内的用户(通常 UID 1000)需要写入数据目录
chmod -R 777 ./tests/e2e/test-data/devstar_data
# 【关键】允许容器访问宿主机的 Docker Socket
sudo chmod 666 /var/run/docker.sock
echo "权限设置完成。"
echo ""
echo "===== [3/5] 构建/拉取依赖镜像... ====="
# 根据 Dockerfile 里的注释 ,我们必须先在本地构建这两个“地基”
echo "正在构建 dev-container 基础镜像..."
docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer .
echo "正在构建 runtime-container 基础镜像..."
docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer .
echo "===== [4/5] 启动并运行测试... ====="
# --build: 确保 test-runner 镜像是最新的
# --abort-on-container-exit: 如果 devstar 挂了, 测试立即停止
# --exit-code-from test-runner: 运行结束后,将 test-runner 的退出码(0=成功, 1=失败)作为本命令的退出码
docker compose -f docker-compose.test.yml up \
--build \
--abort-on-container-exit \
--exit-code-from test-runner
# 捕获 test-runner 的退出码
EXIT_CODE=$?
echo ""
echo ""
echo "===== [5/5] 测试运行完成 ====="
echo "HTML 报告已生成在: ./reports/html"
ls -l ./reports/html
echo ""
# 以 test-runner 的退出码退出
# 这将告诉 CI (或你自己) 测试是成功还是失败
exit $EXIT_CODE

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

@@ -0,0 +1,24 @@
#
# 这是 "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,22 +1,6 @@
# End to end tests
使用PLAYWRIGHT测试框架执行test/e2e下的自动测试脚本
1.在custom/conf下的app.ini中关闭验证码ENABLE_CAPTCHA = false
2.在执行脚本前请下载相关依赖npx playwright install
3.请在.env.e2e文件中配置相关项
GITEA_URLdevstar实例的urlTEST_USER测试用户TEST_USER_ADMIN:管理员在实例里的id现在这个测试用户需要是管理员测试结束时会清理掉所有痕迹
4.在项目根目录执行命令source .env.e2e && npx playwright test tests/e2e/devcontainer.test.e2e.ts 生成的报告在test/e2e下
1.执行make e2e-test执行自动化测试
E2e tests largely follow the same syntax as [integration tests](../integration).

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

@@ -0,0 +1,78 @@
import { chromium, type FullConfig } from '@playwright/test';async function globalSetup(config: FullConfig) {
const { baseURL } = config.projects[0].use;
if (!baseURL) {
throw new Error('baseURL is not defined in playwright.config.ts');
}
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.goto(baseURL!, { timeout: 15000 });
console.log('[GlobalSetup] 检测到安装界面!正在开始自动化安装...');
await page.getByRole('textbox', { name: 'Server Domain *' }).click();
await page.getByRole('textbox', { name: 'Server Domain *' }).fill('172.17.0.1');
await page.getByRole('textbox', { name: 'Gitea Base URL *' }).click();
await page.getByRole('textbox', { name: 'Gitea Base URL *' }).fill('http://172.17.0.1:80');
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' }).click();
await page.getByRole('textbox', { name: 'Administrator Username' }).fill('testuser');
await page.getByRole('textbox', { name: 'Email Address' }).click();
await page.getByRole('textbox', { name: 'Email Address' }).fill('ilovcatlyn750314@gmail.com');
await page.getByRole('textbox', { name: 'Password', exact: true }).click();
await page.getByRole('textbox', { name: 'Password', exact: true }).fill('12345678');
await page.getByRole('textbox', { name: 'Confirm Password' }).click();
await page.getByRole('textbox', { name: 'Confirm Password' }).fill('12345678');
await page.getByRole('button', { name: 'Install Gitea'}).click(); // 'Install' (英文) 或 '安装' (中文) 都能匹配
// 4. 等待安装完成
await page.waitForTimeout(240*1000);
console.log('[GlobalSetup] 安装成功!');
} catch (error) {
// 5. 截图并报错
console.error('[GlobalSetup] 自动化安装失败!');
await page.screenshot({ path: 'playwright-report/global-setup-failure.png' });
console.error('[GlobalSetup] 失败截图已保存到: ./reports/global-setup-failure.png');
throw new Error(`[GlobalSetup] 自动化安装失败。查看截图。 \n原始错误: ${error}`);
}
await browser.close();
}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": {}
}

repo.diff.view_file

@@ -0,0 +1,47 @@
import { devices } from '@playwright/test';
import { env } from 'node:process';
import type { PlaywrightTestConfig } from '@playwright/test';
const BASE_URL = env.DEVSTAR_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000';
export default {
testDir: './specs',
testMatch: /specs\/.*\.ts/,
timeout: 500000, //
expect: {
timeout: 15000, //
},
forbidOnly: Boolean(env.CI),
retries: env.CI ? 2 : 0,
reporter: env.CI ? 'list' : [['list'], ['html', {
outputFolder: 'playwright-report/html', // 写入 /app/playwright-report/html
open: 'never'
}]],
use: {
headless: true,
locale: 'en-US',
actionTimeout: 15000,
navigationTimeout: 15000,
baseURL: BASE_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
outputDir: 'playwright-report/test-artifacts/',
snapshotDir: 'playwright-report/test-snapshots/',
globalSetup: require.resolve('./global-setup.ts'),
} satisfies PlaywrightTestConfig;

repo.diff.view_file

@@ -1,19 +1,12 @@
import { test, expect } from '@playwright/test';
import { link } from 'node:fs';
const {
GITEA_URL,
TEST_USER,
TEST_PASS,
TEST_ADMIN_USER_ID,
} = process.env;
const GITEA_URL= `http://devstar:3000`;
const TEST_USER= `testuser`;
const TEST_PASS= `12345678`;
const TEST_ADMIN_USER_ID=`1`;
const repoName = `e2e-devcontainer-test`;
// 检查关键配置是否存在
if (!GITEA_URL || !TEST_USER || !TEST_PASS ) {
  throw new Error("请确保 .env.e2e 配置文件已加载,并包含 GITEA_URL, TEST_USER, TEST_PASS, TEST_REPO_VALID");
}
test('DevContainer 功能和配置', async ({ page,context }) => {
console.log("正在登陆");
await page.goto(GITEA_URL + '/user/login');
await page.fill('#user_name',TEST_USER);
@@ -35,69 +28,70 @@ test('DevContainer 功能和配置', async ({ page,context }) => {
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();
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"
};
//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);
//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();
// 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();