Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .github/scripts/check_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ def validate_configuration(config: dict[str, Any], framework: str, engine: str,

def main() -> None:
"""
使用两种框架解析全部生产和测试 Compose,无需启动容器。
使用全部框架解析生产和测试 Compose,无需启动容器。

:return: None
"""
parser = argparse.ArgumentParser()
parser.add_argument('--docker', default='docker')
args = parser.parse_args()
for framework in ('vue2', 'vue3'):
for framework in ('vue2', 'vue3', 'react'):
for env_file, compose_file, engine in CONFIGURATIONS:
result = subprocess.run(
[args.docker, 'compose', '--env-file', env_file, '-f', compose_file, 'config', '--format', 'json'],
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/ci_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Any

ROOT = Path(__file__).resolve().parents[2]
FRAMEWORKS = ('vue2', 'vue3')
FRAMEWORKS = ('vue2', 'vue3', 'react')
PYTHON_VERSIONS = ('3.10', '3.11', '3.12', '3.13')
CATEGORIES = ('quality', 'backend', 'frontend', 'integration', 'e2e', 'native')
FRONTEND_FILE_PATH_PARTS = 4
Expand Down
13 changes: 8 additions & 5 deletions .github/scripts/mobile_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def api(route: Route) -> None:
:return: None
"""
request = route.request
path = urlparse(request.url).path
path = urlparse(request.url).path.removeprefix('/dev-api')
headers = {'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*'}
if request.method == 'OPTIONS':
await route.fulfill(status=204, headers=headers)
Expand Down Expand Up @@ -100,7 +100,7 @@ async def run_smoke(framework: str, base_url: str, output: Path) -> None:
"""
使用真实 H5 构建验证登录、持久化身份和个人资料页。

:param framework: vue2 或 vue3
:param framework: vue2、vue3 或 react
:param base_url: 仅用于本次测试的静态文件服务器
:param output: 浏览器报告目录
:return: None
Expand All @@ -118,6 +118,7 @@ async def run_smoke(framework: str, base_url: str, output: Path) -> None:
context = await browser.new_context(viewport={'width': 390, 'height': 844}, timezone_id='UTC')
await context.tracing.start(screenshots=True, snapshots=True, sources=True)
await context.route('http://localhost:9099/**', api)
await context.route(f'{base_url}/dev-api/**', api)
page = await context.new_page()
page.on('pageerror', lambda error: errors.append(str(error)))
page.on('console', lambda message: logs.append(f'{message.type}: {message.text}'))
Expand All @@ -128,7 +129,8 @@ async def run_smoke(framework: str, base_url: str, output: Path) -> None:
await expect(page.locator('input')).to_have_count(2, timeout=15000)
await page.locator('input[type="text"]').fill('ci-user')
await page.locator('input[type="password"]').fill('ci-test-password')
await page.locator('uni-button').filter(has_text=re.compile(r'登\s*录')).click()
button = page.get_by_role('button') if framework == 'react' else page.locator('uni-button')
await button.filter(has_text=re.compile(r'登\s*录')).click()
await expect(page).to_have_url(re.compile(r'/pages/index'), timeout=30000)
await page.goto(f'{base_url}/#/pages/mine/info/index')
await expect(page.get_by_text(USER['nickName'], exact=True)).to_be_visible(timeout=30000)
Expand Down Expand Up @@ -162,9 +164,10 @@ def main() -> None:
:return: None
"""
parser = argparse.ArgumentParser()
parser.add_argument('--framework', choices=('vue2', 'vue3'), required=True)
parser.add_argument('--framework', choices=('vue2', 'vue3', 'react'), required=True)
args = parser.parse_args()
distribution = ROOT / 'ruoyi-fastapi-frontend' / args.framework / 'mobile/dist/build/h5'
build_path = 'dist/h5' if args.framework == 'react' else 'dist/build/h5'
distribution = ROOT / 'ruoyi-fastapi-frontend' / args.framework / 'mobile' / build_path
if not (distribution / 'index.html').is_file():
raise SystemExit(f'请先构建 H5:{distribution}')
output = ROOT / 'target/ci/mobile-smoke' / args.framework
Expand Down
26 changes: 24 additions & 2 deletions .github/tests/test_ci_contracts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import importlib.util
import json
from copy import deepcopy
from pathlib import Path
from types import ModuleType
Expand Down Expand Up @@ -43,9 +44,30 @@ def configuration() -> dict[str, Any]:
}


def test_compose_accepts_consistent_framework(configuration: dict[str, Any]) -> None:
@pytest.mark.parametrize('framework', ['vue2', 'vue3', 'react'])
def test_compose_accepts_consistent_framework(configuration: dict[str, Any], framework: str) -> None:
"""框架一致的 Compose 配置可以通过校验。"""
compose.validate_configuration(configuration, 'vue2', 'my', ROOT)
frontend = ROOT / 'ruoyi-fastapi-frontend' / framework / 'web'
configuration['services']['ruoyi-frontend']['build']['context'] = str(frontend)
configuration['services']['ruoyi-frontend']['volumes'][0]['source'] = str(frontend / 'bin/nginx.dockermy.conf')
configuration['services']['ruoyi-backend-my']['environment']['RUOYI_PLUGIN_FRONTEND_FRAMEWORK'] = framework
compose.validate_configuration(configuration, framework, 'my', ROOT)


@pytest.mark.parametrize('framework', ['vue2', 'vue3', 'react'])
@pytest.mark.parametrize('target', ['web', 'mobile'])
def test_frontend_ci_commands_exist_in_every_project(framework: str, target: str) -> None:
"""矩阵中的工程必须实际提供工作流调用的测试和构建命令。"""
directory = ROOT / 'ruoyi-fastapi-frontend' / framework / target
package = json.loads((directory / 'package.json').read_text(encoding='utf-8'))
required = {'test:unit', 'test:contract'}
required.update({'build:prod', 'build:docker'} if target == 'web' else {'build:h5', 'build:mp-weixin'})
if framework == 'react':
assert package['packageManager'].startswith('pnpm@')
assert (directory / 'pnpm-workspace.yaml').is_file()
if target == 'mobile':
required.update({'check:platforms', 'build:rn:android', 'build:rn:ios'})
assert required <= package['scripts'].keys()


@pytest.mark.parametrize('component', ['build', 'nginx', 'backend'])
Expand Down
42 changes: 24 additions & 18 deletions .github/tests/test_ci_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
SPEC = importlib.util.spec_from_file_location('ci_plan', SCRIPT)
ci = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(ci)
FRONTEND_PROJECT_COUNT = 4
STANDARD_E2E_COMBINATION_COUNT = 4
FULL_E2E_COMBINATION_COUNT = 16
FRONTEND_PROJECT_COUNT = 6
STANDARD_E2E_COMBINATION_COUNT = 6
FULL_E2E_COMBINATION_COUNT = 24


def test_readme_change_only_runs_quality() -> None:
Expand Down Expand Up @@ -48,11 +48,12 @@ def test_native_builder_change_runs_native_verification() -> None:
assert plan['required']['native']


def test_bundle_example_change_runs_both_web_consumers() -> None:
"""示例开发代理由两版 Web 测试直接引用,变更时均应执行。"""
def test_bundle_example_change_runs_all_web_consumers() -> None:
"""示例开发代理由全部 Web 测试直接引用,变更时均应执行。"""
plan = ci.build_plan(['ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/dev/mockRequest.js'])
assert plan['frontend_matrix'] == {
'include': [
{'framework': 'react', 'target': 'web'},
{'framework': 'vue2', 'target': 'web'},
{'framework': 'vue3', 'target': 'web'},
]
Expand All @@ -67,12 +68,12 @@ def test_bundle_example_change_runs_both_web_consumers() -> None:
'ruoyi-fastapi-backend/tests/plugins/core/runtime/test_browser_session.py',
],
)
def test_shared_auth_and_test_helpers_keep_both_network_suites(path: str) -> None:
"""共享鉴权和测试夹具变更触发两版网络验收。"""
def test_shared_auth_and_test_helpers_keep_all_network_suites(path: str) -> None:
"""共享鉴权和测试夹具变更触发全部框架的网络验收。"""
plan = ci.build_plan([path])
assert plan['required']['backend']
assert plan['required']['integration']
assert plan['network_frameworks'] == ['vue2', 'vue3']
assert plan['network_frameworks'] == ['react', 'vue2', 'vue3']


@pytest.mark.parametrize(
Expand All @@ -86,11 +87,13 @@ def test_ci_and_dependency_changes_select_every_category(path: str) -> None:
assert len(plan['e2e_matrix']['include']) == STANDARD_E2E_COMBINATION_COUNT


def test_mobile_change_does_not_build_other_projects_or_web_e2e() -> None:
@pytest.mark.parametrize('framework', ci.FRAMEWORKS)
def test_mobile_change_does_not_build_other_projects_or_web_e2e(framework: str) -> None:
"""单个移动端变更只选中对应工程。"""
plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js'])
assert plan['frontend_matrix'] == {'include': [{'framework': 'vue2', 'target': 'mobile'}]}
plan = ci.build_plan([f'ruoyi-fastapi-frontend/{framework}/mobile/src/utils/request.js'])
assert plan['frontend_matrix'] == {'include': [{'framework': framework, 'target': 'mobile'}]}
assert not plan['required']['e2e']
assert not plan['required']['native']


@pytest.mark.parametrize(
Expand All @@ -100,26 +103,29 @@ def test_mobile_change_does_not_build_other_projects_or_web_e2e() -> None:
'ruoyi-fastapi-test/frontend/mobile-behavior.mjs',
],
)
def test_shared_mobile_harness_runs_both_mobile_projects(path: str) -> None:
"""共享移动端测试工具变更同时验证两版工程。"""
def test_shared_mobile_harness_runs_all_mobile_projects(path: str) -> None:
"""共享移动端测试工具变更同时验证全部移动工程。"""
plan = ci.build_plan([path])
assert plan['required']['frontend']
assert plan['frontend_matrix'] == {
'include': [
{'framework': 'react', 'target': 'mobile'},
{'framework': 'vue2', 'target': 'mobile'},
{'framework': 'vue3', 'target': 'mobile'},
]
}
assert not plan['required']['e2e']


def test_web_change_selects_matching_framework_and_both_databases() -> None:
@pytest.mark.parametrize('framework', ci.FRAMEWORKS)
def test_web_change_selects_matching_framework_and_both_databases(framework: str) -> None:
"""Web 变更使用对应框架并覆盖两类数据库。"""
plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/web/src/views/login.vue'])
assert plan['network_frameworks'] == ['vue2']
plan = ci.build_plan([f'ruoyi-fastapi-frontend/{framework}/web/src/views/login.jsx'])
assert plan['frontend_matrix'] == {'include': [{'framework': framework, 'target': 'web'}]}
assert plan['network_frameworks'] == [framework]
assert {(row['framework'], row['database'], row['python']) for row in plan['e2e_matrix']['include']} == {
('vue2', 'mysql', '3.12'),
('vue2', 'postgresql', '3.12'),
(framework, 'mysql', '3.12'),
(framework, 'postgresql', '3.12'),
}


Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
docker compose up --detach --no-build --wait --wait-timeout 240
echo "TEST_FRONTEND_URL=http://$(docker compose port ruoyi-frontend 80)" >> "$GITHUB_ENV"
echo "TEST_BACKEND_URL=http://$(docker compose port ruoyi-backend-${{ steps.database.outputs.suffix }} 9099)" >> "$GITHUB_ENV"
- name: Run all application pages and functional E2E checks
- name: Run application pages and full functional E2E checks
working-directory: ruoyi-fastapi-test
run: |
python -m pytest -m e2e -v --durations=20 --junitxml=target/e2e/junit.xml
Expand Down
44 changes: 33 additions & 11 deletions .github/workflows/ci-frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
required: true
type: string
full:
description: 'Reserved for callers requesting the full project matrix'
description: 'Also build every additional React Mobile mini-program and Harmony target'
required: false
default: false
type: boolean
Expand All @@ -20,14 +20,15 @@ jobs:
frontend:
name: ${{ matrix.framework }} / ${{ matrix.target }}
runs-on: ubuntu-latest
timeout-minutes: 35
timeout-minutes: ${{ matrix.framework == 'react' && matrix.target == 'mobile' && 60 || 35 }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(inputs.matrix_json) }}
env:
NODE_OPTIONS: ${{ matrix.framework == 'vue2' && '--openssl-legacy-provider' || '' }}
defaults:
run:
shell: bash
working-directory: ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}
steps:
- uses: actions/checkout@v5
Expand All @@ -38,16 +39,18 @@ jobs:
if: matrix.framework == 'vue2' && matrix.target == 'mobile'
run: npm install --global yarn@1.22.22
- uses: pnpm/action-setup@v5
if: matrix.framework == 'vue3' && matrix.target == 'mobile'
if: matrix.framework == 'react' || (matrix.framework == 'vue3' && matrix.target == 'mobile')
with:
package_json_file: ruoyi-fastapi-frontend/vue3/mobile/package.json
package_json_file: ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}/package.json
- name: Locate the package cache
id: package-cache
env:
FRAMEWORK: ${{ matrix.framework }}
TARGET: ${{ matrix.target }}
run: |
if [ "$TARGET" = web ]; then
if [ "$FRAMEWORK" = react ]; then
echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
elif [ "$TARGET" = web ]; then
echo "path=$(npm config get cache)" >> "$GITHUB_OUTPUT"
elif [ "$FRAMEWORK" = vue2 ]; then
echo "path=$(yarn cache dir --silent)" >> "$GITHUB_OUTPUT"
Expand All @@ -58,17 +61,17 @@ jobs:
with:
path: ${{ steps.package-cache.outputs.path }}
# Lockfiles are intentionally not tracked in these projects.
key: ${{ runner.os }}-node22-${{ matrix.framework }}-${{ matrix.target }}-${{ hashFiles(format('ruoyi-fastapi-frontend/{0}/{1}/package.json', matrix.framework, matrix.target)) }}
key: ${{ runner.os }}-node22-${{ matrix.framework }}-${{ matrix.target }}-${{ hashFiles(format('ruoyi-fastapi-frontend/{0}/{1}/package.json', matrix.framework, matrix.target), format('ruoyi-fastapi-frontend/{0}/{1}/pnpm-workspace.yaml', matrix.framework, matrix.target)) }}
restore-keys: |
${{ runner.os }}-node22-${{ matrix.framework }}-${{ matrix.target }}-
- name: Install Web dependencies
if: matrix.target == 'web'
if: matrix.target == 'web' && matrix.framework != 'react'
run: npm install --no-audit --no-fund --package-lock=false
- name: Install Vue 2 Mobile dependencies
if: matrix.framework == 'vue2' && matrix.target == 'mobile'
run: yarn install --non-interactive
- name: Install Vue 3 Mobile dependencies
if: matrix.framework == 'vue3' && matrix.target == 'mobile'
- name: Install pnpm project dependencies
if: matrix.framework == 'react' || (matrix.framework == 'vue3' && matrix.target == 'mobile')
run: pnpm install --no-frozen-lockfile
- name: Run unit and component behavior tests
run: npm run test:unit
Expand All @@ -86,8 +89,11 @@ jobs:
npm run build:prod
npm run build:docker
- name: Build Mobile H5
if: matrix.target == 'mobile'
if: matrix.target == 'mobile' && matrix.framework != 'react'
run: npm run build:h5
- name: Build and verify React Mobile H5 entry
if: matrix.target == 'mobile' && matrix.framework == 'react'
run: pnpm check:platforms h5
- uses: actions/setup-python@v6
if: matrix.target == 'mobile'
with:
Expand All @@ -102,8 +108,22 @@ jobs:
working-directory: .
run: python .github/scripts/mobile_smoke.py --framework ${{ matrix.framework }}
- name: Build Mobile WeChat target
if: matrix.target == 'mobile'
if: matrix.target == 'mobile' && matrix.framework != 'react'
run: npm run build:mp-weixin
- name: Build and verify React Mobile WeChat target
if: matrix.target == 'mobile' && matrix.framework == 'react'
run: pnpm check:platforms weapp
- name: Build the additional React Mobile platform matrix
if: inputs.full && matrix.target == 'mobile' && matrix.framework == 'react'
run: pnpm check:platforms alipay swan tt qq jd kwai lark xhs harmony-hybrid
- name: Build Android and iOS JavaScript bundles
if: matrix.target == 'mobile' && matrix.framework == 'react'
run: |
mkdir -p test-results
pnpm build:rn:android --reset-cache 2>&1 | tee test-results/rn-android.log
test -s dist/rn/android/index.bundle
pnpm build:rn:ios --reset-cache 2>&1 | tee test-results/rn-ios.log
test -s dist/rn/ios/main.jsbundle
- name: Upload frontend diagnostics
if: always()
uses: actions/upload-artifact@v6
Expand All @@ -112,5 +132,7 @@ jobs:
path: |
target/ci/mobile-smoke/${{ matrix.framework }}/
ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}/test-results/
ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}/.cache/platform-check/
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
24 changes: 23 additions & 1 deletion .github/workflows/ci-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,31 @@ jobs:
node-version: '22'
cache: npm
cache-dependency-path: ruoyi-fastapi-frontend/${{ matrix.framework }}/web/package.json
- uses: pnpm/action-setup@v5
if: matrix.framework == 'react'
with:
package_json_file: ruoyi-fastapi-frontend/react/web/package.json
- name: Locate the React package cache
if: matrix.framework == 'react'
id: react-cache
working-directory: ruoyi-fastapi-frontend/react/web
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Cache React dependencies
if: matrix.framework == 'react'
uses: actions/cache@v5
with:
path: ${{ steps.react-cache.outputs.path }}
key: ${{ runner.os }}-network-react-${{ hashFiles('ruoyi-fastapi-frontend/react/web/package.json', 'ruoyi-fastapi-frontend/react/web/pnpm-workspace.yaml') }}
restore-keys: ${{ runner.os }}-network-react-
- run: python -m pip install -r requirements.txt -r requirements-test.txt playwright==1.62.0
- run: python -m playwright install --with-deps chromium
- run: npm --prefix ../ruoyi-fastapi-frontend/${{ matrix.framework }}/web install --no-audit --no-fund --package-lock=false
- name: Install Vue Web dependencies
if: matrix.framework != 'react'
run: npm --prefix ../ruoyi-fastapi-frontend/${{ matrix.framework }}/web install --no-audit --no-fund --package-lock=false
- name: Install React Web dependencies
if: matrix.framework == 'react'
working-directory: ruoyi-fastapi-frontend/react/web
run: pnpm install --no-frozen-lockfile
- name: Verify HTTPS proxy, iframe sessions and long connections
run: python -m pytest tests/plugins/integration/test_network_smoke.py -m browser --strict-markers -v --junitxml=target/plugin-network-smoke/${{ matrix.framework }}/results.xml
- run: python ../.github/scripts/check_test_report.py target/plugin-network-smoke/${{ matrix.framework }}/results.xml
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ jobs:
with:
node-version: '22'
cache: npm
cache-dependency-path: ruoyi-fastapi-frontend/*/web/package.json
- name: Check Web formatting with the existing project configurations
cache-dependency-path: ruoyi-fastapi-frontend/*/*/package.json
- name: Check frontend formatting
run: |
for framework in vue2 vue3; do
(cd "ruoyi-fastapi-frontend/$framework/web" && npx --yes prettier@3.6.2 --check .)
for framework in vue2 vue3 react; do
for target in web mobile; do
(cd "ruoyi-fastapi-frontend/$framework/$target" && npx --yes prettier@3.6.2 --check .)
done
done
- uses: actions/upload-artifact@v6
if: always()
Expand Down
Loading
Loading