Compare commits

59 Commits
main ... tests

Author SHA1 Message Date
jiaojm
c181be2292 修复了了ci的一个格式问题 2025-11-30 18:44:22 +08:00
jiaojm
49009eb9a2 润色脚本格式,添加了install.sh脚本判断网络类型的功能 2025-11-30 16:28:16 +08:00
jiaojm
0ea42c04f5 Merge branch 'main' into tests 2025-11-30 16:12:30 +08:00
jiaojm
8e5edc714f fix 2025-11-30 14:23:39 +08:00
jiaojm
ac1d22708f 修改了相关截图的导出路径 2025-11-30 14:19:51 +08:00
jiaojm
d22a2f30c2 点击安装后经常不跳转,修复这个超时问题 2025-11-30 10:25:37 +08:00
jiaojm
67d8b8b36d fix bug 2025-11-30 10:19:45 +08:00
jiaojm
8de8c86976 fix 2025-11-30 10:02:15 +08:00
jiaojm
1f16d76d96 ci机器里没有ip包,改用docker命令 2025-11-30 09:59:04 +08:00
jiaojm
decbb3336e 修改使install.sh脚本兼容DoD,修复一些错误,增加了一些关键位置的截图和超时判断 2025-11-30 09:52:48 +08:00
jiaojm
dd60ca2100 修改ci脚本和pr评论功能 2025-11-25 19:53:32 +08:00
jiaojm
2795a67741 修改挂载方式 2025-11-25 17:47:49 +08:00
孟宁
c3b8d4612b 修改了E2E用法文档及CI脚本 2025-11-25 10:35:21 +08:00
jiaojm
6ffbe8d5e4 fix a small bug 2025-11-21 14:22:53 +08:00
jiaojm
a57ed29d28 删除dockerfile,把数据挂载放在脚本里使用 2025-11-21 14:20:24 +08:00
jiaojm
50d912e652 删除了docker-compose文件,现在在run-e2e-tests.sh中构建测试容器,ci脚本也作相应修改 2025-11-21 11:22:30 +08:00
jiaojm
bab3befa45 readme的对应修改 2025-11-20 18:29:26 +08:00
jiaojm
166113b07d 解决冲突,删除不必要的代码 2025-11-20 18:25:32 +08:00
jiaojm
74b0fb4912 解决冲突,删除不必要的代码 2025-11-20 18:23:39 +08:00
jiaojm
ed573cc83b 添加了ci脚本,修改了测试脚本的默认语言为中文 2025-11-20 17:43:35 +08:00
jiaojm
96005ac630 fix a bug 2025-11-20 16:25:23 +08:00
jiaojm
e1137bd7c3 fix 2025-11-20 16:23:27 +08:00
jiaojm
04b17516e4 fix 2025-11-20 16:19:36 +08:00
jiaojm
0acecaad5f 修改了语言的问题,现在统一为中文,不会出现gitea用户为英文的情况 2025-11-20 16:16:42 +08:00
jiaojm
80276e54f9 hot fix 2025-11-20 15:22:41 +08:00
jiaojm
72dfb0aeaf 修改登陆脚本,修改docker网络模式,如果ci成功准备删构建部分 2025-11-20 15:18:39 +08:00
jiaojm
15a471f247 统一了测试脚本为中文,避免了不必要的麻烦,测试目前已经安装成功的ci的稳定性 2025-11-20 15:06:59 +08:00
jiaojm
d1187fd712 fix a bug 2025-11-19 20:48:38 +08:00
jiaojm
b3e31dcefd fix a bug 2025-11-19 20:38:47 +08:00
jiaojm
65e7506979 修改在ci中数据挂载的方式,避免写入root/devstar-data失败导致容器启动出问题 2025-11-19 20:32:06 +08:00
jiaojm
b80c358fda 增加了可选的容器网络模式,修改ci脚本尝试修复devstar启动失败的问题 2025-11-19 20:13:48 +08:00
jiaojm
3d678a982b 增加了语言更换的脚本代码,防止因gitea的语言配置导致测试失败,修改了ci流程 2025-11-19 19:40:18 +08:00
jiaojm
c03be52914 运行ci脚本的相关修改 2025-11-19 17:05:27 +08:00
孟宁
cfffaa1960 !111 基于PlayWright端到端测试,make e2e-test测试devcontainer
1. PlayWright测试脚本
2. 执行make e2e-test测试
3. 把与e2e测试相关的代码都放在了/test/e2e下
2025-11-16 01:29:35 +00:00
jiaojm
dfcf75d7d1 修改了readme,id的问题解决,E2E_NAME的命令格式,一些潜在的sudo问题 2025-11-15 22:12:47 +08:00
jiaojm
7d06daf606 last 2025-11-14 16:30:29 +08:00
jiaojm
5e2987d135 1 2025-11-14 10:07:47 +08:00
jiaojm
8d1f62028c new readme 2025-11-14 09:53:34 +08:00
jiaojm
446e343eab fix 2025-11-14 09:25:43 +08:00
jiaojm
109fdd8136 fix 2025-11-13 21:13:14 +08:00
jiaojm
b3fe3a78e0 1 2025-11-13 15:28:27 +08:00
jiaojm
c6ad67556b 1 2025-11-13 14:13:38 +08:00
jiaojm
5918d57139 1 2025-11-12 20:54:15 +08:00
jiaojm
6b4feabe81 1 2025-11-12 19:52:09 +08:00
jiaojm
fb1637a0f4 1 2025-11-12 18:18:45 +08:00
jiaojm
1fc326dbae 1 2025-11-12 17:21:18 +08:00
jiaojm
25ebc112d1 1 2025-11-12 16:05:30 +08:00
jiaojm
5cba473dd8 new 2025-11-12 13:14:18 +08:00
jiaojm
087519a372 1 2025-11-03 15:14:27 +08:00
jiaojm
a556d823e2 1 2025-10-31 16:27:23 +08:00
jiaojm
b0ec1135c0 1 2025-10-31 16:26:50 +08:00
jiaojm
257941c5e5 new test script 2025-10-31 16:18:47 +08:00
jiaojm
16c817fa9b new 2025-10-24 16:42:10 +08:00
jiaojm
d3f24edcbc new 2025-10-24 16:33:20 +08:00
jiaojm
a99c05bd6e 1 2025-10-24 12:52:58 +08:00
jiaojm
38908ac0aa 测试脚本 2025-10-24 12:50:48 +08:00
jiaojm
b33e4adbe1 test script 2025-10-24 12:03:16 +08:00
jiaojm
cc7f5ccff9 test scrpit 2025-10-24 12:01:35 +08:00
jiaojm
4d4faf5103 test scrpit 2025-10-24 11:59:33 +08:00
repo.diff.stats_desc%!(EXTRA int=12, int=585, int=164)

repo.diff.view_file

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

repo.diff.view_file

@@ -0,0 +1,82 @@
name: DevStar E2E Test
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
e2e-test:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: build Devstar Image
run: |
make devstar
- name: Install Network Tools
run: |
echo "正在安装 ip 命令..."
sudo apt-get update && sudo apt-get install -y iproute2
- name: start DevStar Container
run: |
# 启动容器,这里的话需要预先的创建宿主机的对应文件夹
LOGS=$(public/assets/install.sh start \
--port=8082 \
--ssh-port=2224 \
--data-dir=/tmp/devstar_ci \
--image=devstar-studio:latest 2>&1)
echo "$LOGS"
TARGET_URL=$(echo "$LOGS" | grep -o 'http://[^ ]*' | tail -1 | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g")
echo "TARGET_URL=$TARGET_URL" >> $GITHUB_ENV
- name: Run E2E Tests
run: |
make e2e-test TARGET_URL="$TARGET_URL"
env:
GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT: "true"
- name: Upload E2E Test Report
if: always()
uses: actions/upload-artifact@v3
with:
name: e2e-test-report
path: tests/e2e/reports/
- name: Post Test Results
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v6
env:
# 传入任务状态success 或 failure
TEST_RESULT: ${{ job.status }}
with:
script: |
const testResult = process.env.TEST_RESULT || '未知';
const isSuccess = testResult === 'success';
// 动态生成构建链接
const runUrl = `${context.payload.repository.html_url}/actions/runs/${context.runId}`;
const comment = `
## 📋 E2E 测试结果报告
### 测试状态
${isSuccess ? '✅ **通过 (Passed)**' : '❌ **失败 (Failed)**'}
### 🔍 详细报告
HTML 报告已上传至 Artifacts请点击下方链接查看。
👉 [查看运行日志与下载报告](${runUrl})
### 🏗️ 构建信息
- **工作流**: ${context.workflow}
- **提交**: ${context.payload.pull_request.head.sha.slice(0, 7)}
- **触发者**: @${context.actor}
---
> *此评论由 DevStar Actions 自动生成,用于 PR 质量检查。*
`;
// 发送评论
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});

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

@@ -69,6 +69,9 @@ cpu.out
/tests/e2e/gitea-e2e-*
/tests/e2e/indexers-*
/tests/e2e/reports
/tests/e2e/node_modules/
/tests/e2e/package-lock.json
/tests/e2e/test-data
/tests/e2e/test-artifacts
/tests/e2e/test-snapshots
/tests/*.ini

repo.diff.view_file

@@ -963,11 +963,16 @@ 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"
.PHONY: e2e-test
e2e-test:
@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,53 @@
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: [['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
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 */
headless: true,
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
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;

repo.diff.view_file

@@ -110,7 +110,20 @@ function start {
mkdir -p $DATA_DIR
sudo chown 1000:1000 $DATA_DIR
sudo chmod 666 /var/run/docker.sock
DOMAIN_NAME=$(hostname -I | awk '{print $1}')
DOMAIN_NAME=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
if [[ -f "/.dockerenv" ]]; then
if [[ -S "/var/run/docker.sock" ]] && command -v docker >/dev/null 2>&1; then
if docker info 2>/dev/null | grep -q "Storage Driver: vfs"; then
# DinD 环境 - 保持原来的容器IP
: # 什么也不做,使用初始值
else
# DooD 环境 - 获取宿主机IP
DOMAIN_NAME=$(ip route | grep default | awk '{print $3}' 2>/dev/null)
fi
fi
# 普通容器环境保持原来的容器IP
fi
if [ ! -f "${DATA_DIR}/app.ini" ]; then
echo "DOMAIN_NAME=$DOMAIN_NAME"
else
@@ -146,7 +159,7 @@ function stop {
fi
if [ $(docker ps -a --filter "name=^/runner-" -q | wc -l) -gt 0 ]; then
sudo docker stop $(docker ps -a --filter "name=^/runner-" -q) && sudo docker rm -f $(docker ps -a --filter "name=^/runner-" -q)
fi
fi
}
# Function to logs
@@ -256,5 +269,4 @@ case "$1" in
devstar help
fi
;;
esac
esac

repo.diff.view_file

@@ -1,92 +1,27 @@
# End to end tests
# E2E端到端测试
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
```
## E2E端到端测试的用法
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:
在项目根目录下:
```
make test-e2e-sqlite#example
make devstar
public/assets/install.sh clean # 清理已有的安装,警告:会删除已有全部数据!!!
public/assets/install.sh start --image=devstar-studio:latest
make e2e-test TARGET_URL="..." # 使用默认账号testuser 密码12345678
make e2e-test TARGET_URL="..." E2E_USERNAME="your_name" E2E_PASSWORD="your_password" # 使用已有的账号和密码
```
For other databases(replace `mssql` to `mysql` or `pgsql`):
* 通过make devstar 本地代码构建镜像devstar-studio:latest
* public/assets/install.sh start --image=devstar-studio:latest 脚本创建容器并输出devstar的URL比如http://192.168.234.210:80
* make e2e-test TARGET_URL="..."中输入devstar的URL如果首次安装会进入安装页面自动设置管理员账号密码如果已经安装过可以使用已有的账号密码否则按默认账号和密码登录。
* 注意URL不可以是localhost否则devcontainer容器及webterminal无法正常工作
```
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example
```
## 添加E2E端到端测试用例的方法
## Visual testing
* 所有的测试按照功能分组devcontainerappstorerunner等等每一个test函数对应一组或一个测试用例, 按照流程增加对应的测试用例和测试脚本。
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.
todo

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

@@ -0,0 +1,48 @@
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 context = await browser.newContext({
locale: 'zh-CN', // 强制中文
timezoneId: 'Asia/Shanghai', // 强制时区
});
const page = await context.newPage();
if (mode === 'url') {
try {
const url1=env.DEVSTAR_URL;
await page.goto(url1, { timeout: 15000 });
console.log('[GlobalSetup] 检测到安装界面!正在开始自动化安装...');
await page.getByText('服务器和第三方服务设置').click();
await page.getByRole('checkbox', { name: '启用通过 微信二维码 登录' }).uncheck();
await page.getByRole('checkbox', { name: '要求在用户注册时输入预验证码' }).uncheck();
await page.getByText('管理员帐号设置').click();
await page.getByRole('textbox', { name: '管理员用户名' }).fill('testuser');
await page.getByRole('textbox', { name: '邮箱地址' }).fill('ilovcatlyn750314@gmail.com');
await page.getByRole('textbox', { name: '管理员密码', exact: true }).fill('12345678');
await page.getByRole('textbox', { name: '确认密码' }).fill('12345678');
await page.getByRole('button', { name: '立即安装'}).click({ timeout: 10000, noWaitAfter: true });
console.log("安装中,请耐心等待");
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 {
throw new Error(`[GlobalSetup] 未知的 E2E_MODE: "${mode}"`);
}
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": {}
}

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

@@ -0,0 +1,126 @@
#!/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/3] 清理旧的测试环境... ====="
# 如果容器已存在,强制删除
if [ "$(docker ps -aq -f name=e2e-test-runner-container)" ]; then
docker rm -f e2e-test-runner-container
fi
# 清理悬空镜像
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
chmod -R 777 ./tests/e2e/reports
#这里添加的代码是因为需要执行npm install,我们以当前用户启动测试容器避免root权限冲突所以先预构建文件夹也作为缓存缓存npm install.
mkdir -p ./tests/e2e/node_modules
chmod 777 ./tests/e2e/node_modules
LOCK_FILE="./tests/e2e/package-lock.json"
# 确保 lock 文件存在且可写
if [ ! -f "$LOCK_FILE" ]; then
echo "{}" > "$LOCK_FILE"
fi
chmod 666 "$LOCK_FILE"
echo "===== [2/3] 准备环境变量... ====="
export DEVSTAR_URL=$TARGET_URL
export E2E_MODE="url"
if [ -n "$CI" ] || [ "$CI" = "true" ]; then
echo " [CI环境] 检测到 CI 环境,跳过 curl 安装状态检查..."
export E2E_SKIP_INSTALL="false"
else
echo " 正在检查安装状态..."
PATH_TO_CHECK="/user/login"
EXPECTED_CODE_IF_INSTALLED="200"
PROBE_URL="${TARGET_URL}${PATH_TO_CHECK}"
# 使用 curl 获取 HTTP 状态码
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"
else
echo " 探测结果: 目标未安装 (HTTP $HTTP_CODE),将执行安装脚本!"
export E2E_SKIP_INSTALL="false"
fi
fi
echo ""
echo "===== [3/3] 启动容器并运行测试... ====="
set +e
docker run -d --rm --init --ipc=host \
--name e2e-test-runner-container \
-u "root" \
-e DEVSTAR_URL="$DEVSTAR_URL" \
-e E2E_SKIP_INSTALL="$E2E_SKIP_INSTALL" \
-e E2E_USERNAME="$E2E_USERNAME" \
-e E2E_PASSWORD="$E2E_PASSWORD" \
-e E2E_MODE="$E2E_MODE" \
-e CI="$CI" \
-e npm_config_cache=/tmp/npm-cache \
-e HOME=/tmp \
-w /app \
-v /var/run/docker.sock:/var/run/docker.sock \
mcr.microsoft.com/playwright:v1.53.2-jammy \
tail -f /dev/null
echo "容器已启动,正在使用 docker cp 注入代码..."
# 注入代码文件
docker cp "$(pwd)/tests/e2e/package.json" e2e-test-runner-container:/app/package.json
docker cp "$(pwd)/playwright.config.ts" e2e-test-runner-container:/app/playwright.config.ts
docker cp "$(pwd)/tests/e2e/global-setup.ts" e2e-test-runner-container:/app/global-setup.ts
docker cp "$(pwd)/tests/e2e/specs" e2e-test-runner-container:/app/specs
# 在容器内执行安装与测试
docker exec e2e-test-runner-container bash -c "
# 确保 node_modules 目录存在
mkdir -p /app/node_modules
echo '正在安装依赖...'
npm install --no-package-lock
echo '依赖安装完成,开始测试...'
npx playwright test
"
EXIT_CODE=$?
set -e
# 导出测试报告
docker cp e2e-test-runner-container:/app/playwright-report/. tests/e2e/reports/html-report/
# 清理测试容器
docker rm -f e2e-test-runner-container
echo "========================================"
if [ $EXIT_CODE -eq 0 ]; then
echo "测试执行成功!"
else
echo "测试执行失败!"
fi
echo "========================================"
REPORT_DIR="./tests/e2e/reports/"
echo "HTML 报告已生成: $REPORT_DIR"
echo ""
exit $EXIT_CODE

repo.diff.view_file

@@ -0,0 +1,164 @@
import { test, expect } from '@playwright/test';
import { link } from 'node:fs';
import { env } from 'node:process';
import { Login } from './utils.e2e';
import { time } from 'node:console';
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: '创建仓库' }).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: '开发容器' }).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();
//转换为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: '开发容器' }).click();
console.log("创建开发容器");
await page.getByRole('button', { name: '创建开发容器' }).click();
console.log("正在创建");
const stopButton = page.getByRole('button', { name: '停止开发容器' });
try {
console.log('正在等待容器创建');
//第一阶段刷新页面
await expect(stopButton).toBeVisible({ timeout: 60000 });
} catch (error) {
//刷新第一次
await page.reload({ waitUntil: 'domcontentloaded' });
console.log('60s正在刷新页面...');
//加载service可能比较慢
await page.waitForTimeout(180000);
console.log('刷新页面完成,继续等待 (阶段2: 60s)...');
//第二次刷新,容器应该正常启动了
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(stopButton).toBeVisible({ timeout: 30000 });
await page.screenshot({ path: 'playwright-report/screenshot1.png', fullPage: true });
}
console.log("Dev container 创建成功!");
await page.getByRole('button',{ name: '停止开发容器'}).click();
console.log("正在停止");
await expect(page.getByRole('button',{ name: '启动开发容器'})).toBeVisible({ timeout: 180000});
console.log("成功停止开发容器!");
await page.getByRole('button',{name: '启动开发容器'}).click();
console.log("正在启动开发容器");
await expect(page.getByRole('button',{name:'停止开发容器'})).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(); // 等待新页面加载完成
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(30000);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.screenshot({ path: 'playwright-report/screenshot2.png', fullPage: true });
console.log("Web Terminal: 新标签页已打开!");
//await expect(newPage.getByText('Successfully connected to the container')).toBeVisible(); //这里ttyd里的信息PlayWright看不见容器的交互没办法自动化测试
await page.getByRole('link', { name: '删除开发容器' }).click();
await page.locator('#delete-repo-devcontainer-of-user-modal')
.getByRole('button', { name: '确认操作' })
.click();
console.log('正在删除!,可能较慢请等待');
await page.waitForTimeout(3000);
await expect(page.getByRole('button', { name: '创建开发容器' })).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: '管理后台' }).click();
await page.getByText('身份及认证').click();
await page.getByRole('link', { name: '帐户管理' }).click();
await page.getByRole('row')
.filter({ hasText: TEST_USER })
.getByRole('link', { name: '编辑' })
.click();
const devContainerCheckbox = page.getByLabel(/允许创建开发容器/i);
await devContainerCheckbox.uncheck();
await page.getByRole('button', { name: '更新帐户' }).click();
await page.goto(GITEA_URL + '/' + "e2e-devcontainer-test");
const devContainerLink = page.getByRole('link', { name: '开发容器' });
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: '更新帐户' }).click();
console.log('现在清理测试仓库');
console.log("正在导航到仓库设置页面...");
await page.goto(GITEA_URL + '/' + TEST_USER + '/' + repoName + '/settings');
console.log("正在点击 'Delete This Repository' 按钮...");
await page.getByRole('button', { name: '删除本仓库' }).click();
await page.locator('#delete-repo-modal').waitFor();
console.log(`正在输入 '${repoName}' 进行确认...`);
await page.locator('#repo_name_to_delete').fill(repoName);
console.log("正在点击最终的删除确认按钮...");
await page.locator('#delete-repo-modal').getByRole('button', { name: '删除本仓库' }).click();
});
})

repo.diff.view_file

@@ -0,0 +1,68 @@
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="验证码"]');
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: '登录' }).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`
);
}
}
}