Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c181be2292 | ||
|
|
49009eb9a2 | ||
|
|
0ea42c04f5 | ||
|
|
8e5edc714f | ||
|
|
ac1d22708f | ||
|
|
d22a2f30c2 | ||
|
|
67d8b8b36d | ||
|
|
8de8c86976 | ||
|
|
1f16d76d96 | ||
|
|
decbb3336e | ||
|
|
dd60ca2100 | ||
|
|
2795a67741 | ||
|
|
c3b8d4612b | ||
|
|
6ffbe8d5e4 | ||
|
|
a57ed29d28 | ||
|
|
50d912e652 | ||
|
|
bab3befa45 | ||
|
|
166113b07d | ||
|
|
74b0fb4912 | ||
|
|
ed573cc83b | ||
|
|
96005ac630 | ||
|
|
e1137bd7c3 | ||
|
|
04b17516e4 | ||
|
|
0acecaad5f | ||
|
|
80276e54f9 | ||
|
|
72dfb0aeaf | ||
|
|
15a471f247 | ||
|
|
d1187fd712 | ||
|
|
b3e31dcefd | ||
|
|
65e7506979 | ||
|
|
b80c358fda | ||
|
|
3d678a982b | ||
|
|
c03be52914 | ||
|
|
cfffaa1960 | ||
|
|
dfcf75d7d1 | ||
|
|
7d06daf606 | ||
|
|
5e2987d135 | ||
|
|
8d1f62028c | ||
|
|
446e343eab | ||
|
|
109fdd8136 | ||
|
|
b3fe3a78e0 | ||
|
|
c6ad67556b | ||
|
|
5918d57139 | ||
|
|
6b4feabe81 | ||
|
|
fb1637a0f4 | ||
|
|
1fc326dbae | ||
|
|
25ebc112d1 | ||
|
|
5cba473dd8 | ||
|
|
087519a372 | ||
|
|
a556d823e2 | ||
|
|
b0ec1135c0 | ||
|
|
257941c5e5 | ||
|
|
16c817fa9b | ||
|
|
d3f24edcbc | ||
|
|
a99c05bd6e | ||
|
|
38908ac0aa | ||
|
|
b33e4adbe1 | ||
|
|
cc7f5ccff9 | ||
|
|
4d4faf5103 |
@@ -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
|
||||
|
||||
82
.gitea/workflows/devstar-studio-e2e.yaml
Normal file
82
.gitea/workflows/devstar-studio-e2e.yaml
Normal 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
3
.gitignore
repo.diff.vendored
@@ -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
|
||||
|
||||
9
Makefile
9
Makefile
@@ -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
|
||||
|
||||
@@ -2,97 +2,52 @@ 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('/')`. */
|
||||
headless: true,
|
||||
locale: 'zh-CN',
|
||||
timezoneId: 'Asia/Shanghai',
|
||||
actionTimeout: 15000,
|
||||
navigationTimeout: 15000,
|
||||
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',
|
||||
},
|
||||
|
||||
/* 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;
|
||||
|
||||
@@ -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
|
||||
@@ -257,4 +270,3 @@ case "$1" in
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -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
|
||||
* 所有的测试按照功能分组:devcontainer,appstore,runner等等,每一个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
48
tests/e2e/global-setup.ts
Normal 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
20
tests/e2e/package.json
Normal 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
126
tests/e2e/run-e2e-tests.sh
Executable 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
|
||||
164
tests/e2e/specs/devcontainer.e2e.test.ts
Normal file
164
tests/e2e/specs/devcontainer.e2e.test.ts
Normal 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();
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
68
tests/e2e/specs/utils.e2e.ts
Normal file
68
tests/e2e/specs/utils.e2e.ts
Normal 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`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user