!111 基于PlayWright端到端测试,make e2e-test测试devcontainer
1. PlayWright测试脚本 2. 执行make e2e-test测试 3. 把与e2e测试相关的代码都放在了/test/e2e下
This commit is contained in:
@@ -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
1
.gitignore
repo.diff.vendored
@@ -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
|
||||
|
||||
16
Makefile
16
Makefile
@@ -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
|
||||
|
||||
@@ -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
11
tests/e2e/.dockerignore
Normal 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
23
tests/e2e/Dockerfile
Normal 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"]
|
||||
@@ -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环境和项目代码。
|
||||
|
||||
|
||||
|
||||
5
tests/e2e/docker-compose.override.yml
Normal file
5
tests/e2e/docker-compose.override.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
test-runner:
|
||||
depends_on:
|
||||
devstar:
|
||||
condition: service_healthy
|
||||
43
tests/e2e/docker-compose.test.yml
Normal file
43
tests/e2e/docker-compose.test.yml
Normal 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
|
||||
@@ -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
68
tests/e2e/global-setup.ts
Normal 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
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": {}
|
||||
}
|
||||
118
tests/e2e/run-e2e-tests.sh
Executable file
118
tests/e2e/run-e2e-tests.sh
Executable 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
|
||||
186
tests/e2e/specs/devcontainer.e2e.test.ts
Normal file
186
tests/e2e/specs/devcontainer.e2e.test.ts
Normal 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();
|
||||
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
75
tests/e2e/specs/utils.e2e.ts
Normal file
75
tests/e2e/specs/utils.e2e.ts
Normal 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] 登录成功!`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user