diff --git a/.dockerignore b/.dockerignore index 843f12a7be..477b8cfd52 100644 --- a/.dockerignore +++ b/.dockerignore @@ -60,6 +60,7 @@ cpu.out /tests/e2e/reports /tests/e2e/test-artifacts /tests/e2e/test-snapshots +/tests/e2e/test-data /tests/*.ini /node_modules /yarn.lock @@ -69,6 +70,8 @@ cpu.out /public/assets/css /public/assets/fonts /public/assets/img/avatar +/reports/ +/test-data/ /vendor /VERSION /.air diff --git a/.env.e2e b/.env.e2e deleted file mode 100644 index 0ae935f566..0000000000 --- a/.env.e2e +++ /dev/null @@ -1,6 +0,0 @@ -export GITEA_URL="" - -export TEST_USER="" -export TEST_PASS="" -export TEST_ADMIN_USER_ID="1" - diff --git a/.gitignore b/.gitignore index 0791a17c71..870e9b3619 100644 --- a/.gitignore +++ b/.gitignore @@ -69,11 +69,14 @@ cpu.out /tests/e2e/gitea-e2e-* /tests/e2e/indexers-* /tests/e2e/reports +/tests/e2e/test-data /tests/e2e/test-artifacts /tests/e2e/test-snapshots /tests/*.ini /tests/**/*.git/**/*.sample /node_modules +/test-data +/reports /.venv /yarn.lock /yarn-error.log diff --git a/Makefile b/Makefile index a1c3c961e8..11633f84ab 100644 --- a/Makefile +++ b/Makefile @@ -925,6 +925,11 @@ docker: docker build --disable-content-trust=false -t $(DOCKER_REF) . # support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" . +.PHONY: e2e-test +e2e-test: + @echo "正在启动E2E-TEST..." + @./run-e2e-tests.sh + # This endif closes the if at the top of the file endif diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000000..abe4a73aee --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + # 服务一: DevStar + devstar: + # 我们不再拉取镜像 + # image: mengning997/devstar-studio:latest + # pull_policy: always + # 我们告诉 Compose 在本地构建 + build: + context: . + dockerfile: docker/Dockerfile.devstar # + + image: devstar-e2e-test:latest # + pull_policy: never + ports: + - "80:3000" + - "2222:2222" + volumes: + # 挂载 Docker Socket,允许 DevStar 创建 Devcontainer + - /var/run/docker.sock:/var/run/docker.sock + # 挂载数据卷,使用相对路径,保证测试环境可移植 + - ./tests/e2e/test-data/devstar_data:/var/lib/gitea + - ./tests/e2e/test-data/devstar_data:/etc/gitea + # 健康检查。test-runner 会等待这个检查通过 + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/ || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + # 服务二: Playwright + test-runner: + # 从 'tests/' 目录下的 Dockerfile 构建 + build: + context: ./tests/e2e + # 等待 devstar 的 "healthcheck" 通过后,才启动 + depends_on: + devstar: + condition: service_healthy + environment: + # 将 DevStar 的 URL 传给 Playwright + - DEVSTAR_URL=http://devstar:3000 + volumes: + # 也挂载 Docker Socket + - /var/run/docker.sock:/var/run/docker.sock + # 将测试报告写回到宿主机的 ./reports 目录 + - ./tests/e2e/reports:/app/playwright-report + # 覆盖默认命令,强制运行测试并生成我们想要的报告 + command: > + npx playwright test diff --git a/run-e2e-tests.sh b/run-e2e-tests.sh new file mode 100755 index 0000000000..debe96fc29 --- /dev/null +++ b/run-e2e-tests.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# 这是一个“一键运行”E2E 测试的脚本 +# 它会处理所有清理、权限、拉取和执行工作 +# 任何命令失败立即退出 +set -e + +echo "===== [1/5] 清理旧的测试环境... =====" +# 彻底销毁旧的 compose 环境,-v 会删除关联的数据卷 +docker compose -f docker-compose.test.yml down -v --remove-orphans +docker image prune -f +docker builder prune -f +# 清理并重建报告和数据目录 +sudo rm -rf ./tests/e2e/reports ./tests/e2e/test-data +mkdir -p ./tests/e2e/reports/html ./tests/e2e/test-data/devstar_data +echo "清理完成。" +echo "" + +echo "===== [2/5] 设置权限... =====" +# 容器内的用户(通常 UID 1000)需要写入数据目录 +chmod -R 777 ./tests/e2e/test-data/devstar_data + +# 【关键】允许容器访问宿主机的 Docker Socket +sudo chmod 666 /var/run/docker.sock +echo "权限设置完成。" +echo "" + +echo "===== [3/5] 构建/拉取依赖镜像... =====" +# 根据 Dockerfile 里的注释 ,我们必须先在本地构建这两个“地基” +echo "正在构建 dev-container 基础镜像..." +docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer . + +echo "正在构建 runtime-container 基础镜像..." +docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer . + +echo "===== [4/5] 启动并运行测试... =====" +# --build: 确保 test-runner 镜像是最新的 +# --abort-on-container-exit: 如果 devstar 挂了, 测试立即停止 +# --exit-code-from test-runner: 运行结束后,将 test-runner 的退出码(0=成功, 1=失败)作为本命令的退出码 +docker compose -f docker-compose.test.yml up \ + --build \ + --abort-on-container-exit \ + --exit-code-from test-runner + +# 捕获 test-runner 的退出码 +EXIT_CODE=$? +echo "" +echo "" + +echo "===== [5/5] 测试运行完成 =====" +echo "HTML 报告已生成在: ./reports/html" +ls -l ./reports/html +echo "" + +# 以 test-runner 的退出码退出 +# 这将告诉 CI (或你自己) 测试是成功还是失败 +exit $EXIT_CODE diff --git a/tests/e2e/Dockerfile b/tests/e2e/Dockerfile new file mode 100644 index 0000000000..5732261019 --- /dev/null +++ b/tests/e2e/Dockerfile @@ -0,0 +1,24 @@ +# +# 这是 "test-runner" 服务的 Dockerfile +# 它构建了一个包含所有浏览器和我们测试代码的镜像 +# + +# 1. 使用微软官方的 Playwright 镜像 +# 它已经内置了所有浏览器 (Chromium, Firefox, WebKit) 和操作系统依赖 +FROM mcr.microsoft.com/playwright:v1.53.2-jammy + +# 2. 设置工作目录 +WORKDIR /app + +# 3. 复制 "依赖清单" 文件 +COPY package*.json ./ + +# 4. 安装我们的 npm 依赖 (即 @playwright/test) +RUN npm install + +# 5. 复制我们所有的测试代码到容器中 +# (包括 playwright.config.ts, global-setup.ts 和 specs/ 目录) +COPY . . + +# 6. 默认命令 +CMD ["npx", "playwright", "test"] diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 3c05fe120f..85cdd596da 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,22 +1,6 @@ # End to end tests 使用PLAYWRIGHT测试框架执行test/e2e下的自动测试脚本 - -1.在custom/conf下的app.ini中关闭验证码:ENABLE_CAPTCHA = false -2.在执行脚本前请下载相关依赖:npx playwright install -3.请在.env.e2e文件中配置相关项: -GITEA_URL:devstar实例的url,TEST_USER:测试用户,TEST_USER_ADMIN:管理员在实例里的id,现在这个测试用户需要是管理员,测试结束时会清理掉所有痕迹 -4.在项目根目录执行命令source .env.e2e && npx playwright test tests/e2e/devcontainer.test.e2e.ts ,生成的报告在test/e2e下 - - - - - - - - - - - +1.执行make e2e-test执行自动化测试 E2e tests largely follow the same syntax as [integration tests](../integration). diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts new file mode 100644 index 0000000000..42ec6d0ee2 --- /dev/null +++ b/tests/e2e/global-setup.ts @@ -0,0 +1,78 @@ + + import { chromium, type FullConfig } from '@playwright/test';async function globalSetup(config: FullConfig) { + + const { baseURL } = config.projects[0].use; + + if (!baseURL) { + + throw new Error('baseURL is not defined in playwright.config.ts'); + + } + + + + const browser = await chromium.launch(); + + const page = await browser.newPage(); + try { + + await page.goto(baseURL!, { timeout: 15000 }); + + + + console.log('[GlobalSetup] 检测到安装界面!正在开始自动化安装...'); + + + + await page.getByRole('textbox', { name: 'Server Domain *' }).click(); + await page.getByRole('textbox', { name: 'Server Domain *' }).fill('172.17.0.1'); + await page.getByRole('textbox', { name: 'Gitea Base URL *' }).click(); + await page.getByRole('textbox', { name: 'Gitea Base URL *' }).fill('http://172.17.0.1:80'); + await page.getByText('Server and Third-Party Service Settings').click(); + await page.getByRole('checkbox', { name: 'Enable user sign-in via Wechat QR Code.' }).uncheck(); + + await page.getByRole('checkbox', { name: 'Require a CAPTCHA for user' }).uncheck(); + + + await page.getByText('Administrator Account Settings').click(); + await page.getByRole('textbox', { name: 'Administrator Username' }).click(); + await page.getByRole('textbox', { name: 'Administrator Username' }).fill('testuser'); + await page.getByRole('textbox', { name: 'Email Address' }).click(); + await page.getByRole('textbox', { name: 'Email Address' }).fill('ilovcatlyn750314@gmail.com'); + await page.getByRole('textbox', { name: 'Password', exact: true }).click(); + await page.getByRole('textbox', { name: 'Password', exact: true }).fill('12345678'); + await page.getByRole('textbox', { name: 'Confirm Password' }).click(); + await page.getByRole('textbox', { name: 'Confirm Password' }).fill('12345678'); + + + + + + await page.getByRole('button', { name: 'Install Gitea'}).click(); // 'Install' (英文) 或 '安装' (中文) 都能匹配 + + + + // 4. 等待安装完成 + + await page.waitForTimeout(240*1000); + + console.log('[GlobalSetup] 安装成功!'); + + + + } catch (error) { + + // 5. 截图并报错 + + console.error('[GlobalSetup] 自动化安装失败!'); + + await page.screenshot({ path: 'playwright-report/global-setup-failure.png' }); + + console.error('[GlobalSetup] 失败截图已保存到: ./reports/global-setup-failure.png'); + + throw new Error(`[GlobalSetup] 自动化安装失败。查看截图。 \n原始错误: ${error}`); + + } + await browser.close(); + +}export default globalSetup; diff --git a/tests/e2e/package.json b/tests/e2e/package.json new file mode 100644 index 0000000000..b8ace7087a --- /dev/null +++ b/tests/e2e/package.json @@ -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": {} +} diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts new file mode 100644 index 0000000000..5b4fd553dc --- /dev/null +++ b/tests/e2e/playwright.config.ts @@ -0,0 +1,47 @@ +import { devices } from '@playwright/test'; +import { env } from 'node:process'; +import type { PlaywrightTestConfig } from '@playwright/test'; + +const BASE_URL = env.DEVSTAR_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000'; + +export default { + testDir: './specs', + + + testMatch: /specs\/.*\.ts/, + + timeout: 500000, // + expect: { + timeout: 15000, // + }, + forbidOnly: Boolean(env.CI), + retries: env.CI ? 2 : 0, + + reporter: env.CI ? 'list' : [['list'], ['html', { + outputFolder: 'playwright-report/html', // 写入 /app/playwright-report/html + open: 'never' + }]], + + use: { + headless: true, + locale: 'en-US', + actionTimeout: 15000, + navigationTimeout: 15000, + baseURL: BASE_URL, + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + + outputDir: 'playwright-report/test-artifacts/', + snapshotDir: 'playwright-report/test-snapshots/', + globalSetup: require.resolve('./global-setup.ts'), + +} satisfies PlaywrightTestConfig; diff --git a/tests/e2e/devcontainer.test.e2e.ts b/tests/e2e/specs/devcontainer.test.e2e.ts similarity index 70% rename from tests/e2e/devcontainer.test.e2e.ts rename to tests/e2e/specs/devcontainer.test.e2e.ts index e804983251..1095462619 100644 --- a/tests/e2e/devcontainer.test.e2e.ts +++ b/tests/e2e/specs/devcontainer.test.e2e.ts @@ -1,19 +1,12 @@ import { test, expect } from '@playwright/test'; import { link } from 'node:fs'; -const { - GITEA_URL, - TEST_USER, - TEST_PASS, - TEST_ADMIN_USER_ID, -} = process.env; +const GITEA_URL= `http://devstar:3000`; +const TEST_USER= `testuser`; +const TEST_PASS= `12345678`; +const TEST_ADMIN_USER_ID=`1`; const repoName = `e2e-devcontainer-test`; -// 检查关键配置是否存在 -if (!GITEA_URL || !TEST_USER || !TEST_PASS ) { -  throw new Error("请确保 .env.e2e 配置文件已加载,并包含 GITEA_URL, TEST_USER, TEST_PASS, TEST_REPO_VALID"); -} test('DevContainer 功能和配置', async ({ page,context }) => { - console.log("正在登陆"); await page.goto(GITEA_URL + '/user/login'); await page.fill('#user_name',TEST_USER); @@ -35,69 +28,70 @@ test('DevContainer 功能和配置', async ({ page,context }) => { console.log("正在点击 'Create' (创建模板) 按钮..."); await page.getByRole('button', { name: /Create/i }).click(); + await page.waitForTimeout(10000); console.log("模板已创建. 正在点击 'Edit' 按钮..."); await expect(page.getByText('devcontainer.json')).toBeVisible(); - await page.getByRole('link', { name: 'Edit' }).click(); - - console.log("已跳转到编辑器. 正在修改内容"); - const newJsonAsObject = { - "name": "Gitea DevContainer", - "image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm", - "features": { - "ghcr.io/devcontainers/features/node:1": { - "version": "lts" - }, - "ghcr.io/devcontainers/features/git-lfs:1.2.2": {}, - "ghcr.io/devcontainers-extra/features/poetry:2": {}, - "ghcr.io/devcontainers/features/python:1": { - "version": "3.12" - }, - "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {} - }, - "customizations": { - "vscode": { - "settings": {}, - "extensions": [ - "editorconfig.editorconfig", - "dbaeumer.vscode-eslint", - "golang.go", - "stylelint.vscode-stylelint", - "DavidAnson.vscode-markdownlint", - "Vue.volar", - "ms-azuretools.vscode-docker", - "vitest.explorer", - "cweijan.vscode-database-client2", - "GitHub.vscode-pull-request-github", - "Azurite.azurite" - ] - } - }, - "portsAttributes": { - "3000": { - "label": "Gitea Web", - "onAutoForward": "notify" - } - }, - "postCreateCommand": "make deps" - }; + //await page.getByRole('link', { name: 'Edit' }).click(); + await page.waitForTimeout(5000); + //console.log("已跳转到编辑器. 正在修改内容"); + //const newJsonAsObject = { + //"name": "Gitea DevContainer", + //"image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm", + //"features": { + //"ghcr.io/devcontainers/features/node:1": { + //"version": "lts" + //}, + // "ghcr.io/devcontainers/features/git-lfs:1.2.2": {}, + // "ghcr.io/devcontainers-extra/features/poetry:2": {}, + // "ghcr.io/devcontainers/features/python:1": { + // "version": "3.12" + // }, + //"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {} + //}, + //"customizations": { + // "vscode": { + // "settings": {}, + // "extensions": [ + // "editorconfig.editorconfig", + // "dbaeumer.vscode-eslint", + // "golang.go", + // "stylelint.vscode-stylelint", + /// "DavidAnson.vscode-markdownlint", + // "Vue.volar", + // "ms-azuretools.vscode-docker", + //// "vitest.explorer", + // "cweijan.vscode-database-client2", + // "GitHub.vscode-pull-request-github", + // "Azurite.azurite" + //] + // } + //}, + //"portsAttributes": { + //"3000": { + // "label": "Gitea Web", + // "onAutoForward": "notify" + // } + // }, + //"postCreateCommand": "make deps" + // }; // 转换为JSON 字符串 - const newJsonString = JSON.stringify(newJsonAsObject); + //const newJsonString = JSON.stringify(newJsonAsObject); // 设置焦点 - await page.locator('.view-lines > div:nth-child(20)').click(); - console.log("正在手动删除模板内容 "); - for (let i = 0; i < 500; i++) { - await page.keyboard.press('Backspace'); - } - // 粘贴字符串 - console.log("正在粘贴JSON 内容..."); - await page.keyboard.insertText(newJsonString); - await page.getByRole('button', { name: 'Commit Changes' }).click(); - console.log("devcontainer.json 修改并提交成功."); - console.log("正在导航回 Dev Container 标签页进行验证..."); - await page.getByRole('link', { name: 'Dev Container' }).click(); + // await page.locator('.view-lines > div:nth-child(20)').click(); + // console.log("正在手动删除模板内容 "); + //for (let i = 0; i < 500; i++) { + // await page.keyboard.press('Backspace'); + // } + /// 粘贴字符串 + //console.log("正在粘贴JSON 内容..."); + // await page.keyboard.insertText(newJsonString); + //await page.getByRole('button', { name: 'Commit Changes' }).click(); + //console.log("devcontainer.json 修改并提交成功."); + //console.log("正在导航回 Dev Container 标签页进行验证..."); + //await page.getByRole('link', { name: 'Dev Container' }).click(); console.log("创建开发容器"); await page.getByRole('button', { name: 'Create Dev Container' }).click();