name: E2E Test PR
run-name: E2E Test PR--${{ github.event.pull_request.title }}
on:
  pull_request:
    types: [opened, reopened, synchronize, edited]

# 并发控制,确保相同PR的工作流不会同时运行多个实例
concurrency:
  group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
  cancel-in-progress: true

jobs:
  detect-changed-files:
    name: Detect Changed Files
    runs-on: ubuntu-latest
    outputs:
      # 输出三个变量供后续任务使用
      changed_components: ${{ steps.find-changed-components.outputs.changed_components }} # 自动检测到的变更组件
      manual_components: ${{ steps.parse-title.outputs.manual_components }} # 从PR标题手动指定的组件
      testclis: ${{ steps.parse-test-cli.outputs.testClis }} # 测试命令列表
    steps:
      # 检出代码,需要完整历史以便比较变更
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 0 # 获取完整的git历史,用于检测文件变更

      # 使用tj-actions/changed-files获取PR中所有变更的文件
      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v41

      # 根据变更的文件自动识别受影响的组件
      - name: Find changed components
        id: find-changed-components
        uses: actions/github-script@v6
        with:
          script: |
            const changedFiles = '${{ steps.changed-files.outputs.all_changed_files }}'.split(' ').filter(file => file.trim() !== '');
            console.log('变更的文件列表:', changedFiles);

            // 增强组件路径匹配规则,确保能正确识别各种路径格式
            const componentPathRules = [
              // 匹配 packages/renderless/src/{组件名} 路径,支持更深层次的子目录
              { 
                pattern: /^packages\/renderless\/src\/([^/]+)\//, 
                group: 1,
                type: 'renderless'
              },
              // 匹配 packages/vue/src/{组件名} 路径,支持更深层次的子目录
              { 
                pattern: /^packages\/vue\/src\/([^/]+)\//, 
                group: 1,
                type: 'vue'
              },
              // 匹配示例目录下的组件路径 - 只匹配目录,不匹配package.json等文件
              { 
                pattern: /^examples\/sites\/demos\/pc\/app\/([^/]+)\//, 
                group: 1,
                type: 'demo'
              },
              // 匹配 packages/theme/src/{组件名} 样式路径
              {
                pattern: /^packages\/theme\/src\/([^/]+)\//,
                group: 1,
                type: 'theme'
              }
            ];

            // 定义子组件到父组件的映射关系
            const subComponentMapping = {
              'calendar-bar': 'calendar',
              'carousel-item': 'carousel',
              'cascader-menu': 'cascader',
              'cascader-node': 'cascader', 
              'cascader-panel': 'cascader',
              'checkbox-button': 'checkbox',
              'checkbox-group': 'checkbox',
              'col': 'layout',
              'collapse-item': 'collapse',
              'date-range': 'date-picker',
              'date-table': 'date-picker',
              'dropdown-item': 'dropdown',
              'dropdown-menu': 'dropdown',
              'form-item': 'form',
              'grid-toolbar': 'grid',
              'image-viewer': 'image',
              'month-range': 'date-picker',
              'month-table': 'date-picker',
              'option': 'select',
              'option-group': 'select',
              'pager-item': 'pager',
              'picker': 'date-picker',
              'quarter-panel': 'date-picker',
              'radio-button': 'radio',
              'radio-group': 'radio',
              'row': 'layout',
              'select-dropdown': 'select',
              'skeleton-item': 'skeleton',
              'svgs': 'icon',
              'tab-item': 'tabs',
              'textarea': 'input',
              'time-panel': 'time-picker',
              'time-range': 'time-picker',
              'time-spinner': 'time-picker',
              'timeline-item': 'timeline',
              'transfer-panel': 'transfer',
              'tree-node': 'tree',
              'upload': 'file-upload',
              'upload-dragger': 'file-upload',
              'upload-list': 'file-upload',
              'year-range': 'date-picker',
              'year-table': 'date-picker'
            };

            // 从文件路径中提取组件名称
            const components = new Set();
            const detectedComponents = {}; // 用于记录检测到的组件和相关文件
            const unmatchedFiles = []; // 记录未匹配的文件

            changedFiles.forEach(file => {
              // 跳过工作流文件等非组件相关文件
              if (file.startsWith('.github/') || 
                  file.includes('README') || 
                  file.includes('.md') ||
                  file.includes('theme-saas') || // 跳过theme-saas相关文件
                  file.includes('theme/src/common/')) { // 跳过组件通用样式文件
                console.log(`跳过非测试相关文件: ${file}`);
                return;
              }
              
              let matched = false;
              
              for (const rule of componentPathRules) {
                const match = file.match(rule.pattern);
                if (match && match[rule.group]) {
                  matched = true;
                  let componentName = match[rule.group];
                  
                  // 检查是否是需要映射的子组件
                  if (subComponentMapping[componentName]) {
                    console.log(`检测到子组件 ${componentName},映射到父组件 ${subComponentMapping[componentName]}`);
                    componentName = subComponentMapping[componentName];
                  }
                  
                  // 记录找到的组件
                  components.add(componentName);
                  
                  // 记录组件来源和相关文件,用于调试
                  if (!detectedComponents[componentName]) {
                    detectedComponents[componentName] = {
                      type: rule.type,
                      files: []
                    };
                  }
                  detectedComponents[componentName].files.push(file);
                  console.log(`成功匹配组件 ${componentName}: ${file} (类型: ${rule.type})`);
                  break; // 一个文件只匹配一个组件
                }
              }
              
              // 检查theme目录中的特殊路径格式
              if (!matched && file.includes('/theme/src/') && !file.includes('/common/')) {
                // 尝试从theme路径中提取组件名
                const themePath = file.split('/');
                // 常见的组件样式文件命名模式
                const possibleComponentNames = [];
                
                for (let i = 0; i < themePath.length; i++) {
                  // 跳过明显不是组件名的路径部分
                  if (['src', 'lib', 'theme', 'packages'].includes(themePath[i])) {
                    continue;
                  }
                  // 检查是否像是组件名(小写字母,可能包含连字符)
                  if (/^[a-z][a-z0-9\-]*$/.test(themePath[i])) {
                    possibleComponentNames.push(themePath[i]);
                  }
                }
                
                if (possibleComponentNames.length > 0) {
                  // 使用最后一个可能的组件名(通常最具体)
                  let componentName = possibleComponentNames[possibleComponentNames.length - 1];
                  
                  // 检查是否是需要映射的子组件
                  if (subComponentMapping[componentName]) {
                    console.log(`检测到子组件 ${componentName},映射到父组件 ${subComponentMapping[componentName]}`);
                    componentName = subComponentMapping[componentName];
                  }
                  
                  components.add(componentName);
                  
                  if (!detectedComponents[componentName]) {
                    detectedComponents[componentName] = {
                      type: 'theme',
                      files: []
                    };
                  }
                  detectedComponents[componentName].files.push(file);
                  console.log(`通过主题样式路径分析匹配到组件 ${componentName}: ${file}`);
                  matched = true;
                }
              }
              
              // 记录未匹配到的文件,用于调试
              if (!matched) {
                unmatchedFiles.push(file);
                console.log(`未能匹配组件: ${file}`);
              }
            });

            // 打印详细的检测结果,方便调试
            console.log('检测结果详情:', JSON.stringify(detectedComponents, null, 2));
            console.log('未匹配文件列表:', unmatchedFiles);

            // 构建E2E测试的组件过滤器表达式
            if (components.size > 0) {
              // 构建符合E2E测试命令需要的过滤表达式格式
              const componentFilters = Array.from(components)
                .map(comp => `"\\/app\\/${comp}\\/"`)
                .join(' ');
              
              console.log(`检测到变更的组件: ${Array.from(components).join(', ')}`);
              core.setOutput('changed_components', componentFilters);
            } else {
              console.log('没有检测到变更的组件');
              core.setOutput('changed_components', '');
            }

      # 从PR标题中解析手动指定的组件列表
      - name: Parse Title for Manual Components
        id: parse-title
        uses: actions/github-script@v6
        with:
          script: |
            const prTitle = context.payload.pull_request.title
            // 匹配PR标题中的 [component1, component2] 格式
            const regex = /\[(.*?)\]/
            const matches = prTitle.match(regex)
            if (matches && matches.length > 1 && matches[1]) {
              // 处理和格式化手动指定的组件列表
              let components = matches[1]
                .split(',')
                .map((c) => c.trim())
                .filter((c) => /^[a-z\-\/]+$/.test(c)) // 确保组件名符合规范
                .map((c) => `"\\/app\\/${c}\\/"`)
              components = [...new Set(components)].join(' ') // 去重并转为字符串
              core.setOutput('manual_components', components)
            } else {
              core.setOutput('manual_components', '')
            }

      # 当没有检测到任何组件时,生成警告提示
      - name: Generate warning if no components detected
        id: warning
        if: ${{ steps.find-changed-components.outputs.changed_components == '' && steps.parse-title.outputs.manual_components == '' }}
        run: |
          cat << EOF > user-tip.txt
          **[e2e-test-warn]**
          没有检测到要测试的组件。

          系统会自动检测PR中变更的组件文件,或者您可以在PR标题中使用[component1, component2]格式手动指定要测试的组件。
          例如: "fix(vue-renderless): [action-menu, alert] fix xxx bug"

          请确保您已阅读我们的[贡献指南](https://github.com/opentiny/tiny-vue/blob/dev/CONTRIBUTING.md)
          EOF
          echo "warning=true" >> $GITHUB_OUTPUT

      # 上传警告信息作为工作流制品
      - name: Upload warning
        if: ${{ steps.warning.outputs.warning }}
        uses: actions/upload-artifact@v4
        with:
          name: user-tip
          path: user-tip.txt
          retention-days: 1 # 保留1天

      # 解析测试命令配置
      - name: Parse Test Cli
        id: parse-test-cli
        uses: actions/github-script@v6
        with:
          script: |
            // 从GitHub变量中获取测试命令列表,如果未配置则使用默认命令
            const testClis = '${{ vars.PLAYWRIGHT_CLIS }}' ? '${{ vars.PLAYWRIGHT_CLIS }}'.split(',') : ['pnpm test:e2e3']
            core.setOutput('testClis', JSON.stringify(testClis))

  # PR测试任务,运行实际的E2E测试
  pr-test:
    name: PR E2E Test
    needs: detect-changed-files # 依赖前一个任务的输出
    runs-on: ubuntu-latest
    # 只有当检测到变更组件或手动指定组件时才运行测试
    if: ${{ needs.detect-changed-files.outputs.changed_components != '' || needs.detect-changed-files.outputs.manual_components != '' }}
    strategy:
      matrix:
        testcli: ${{ fromJson(needs.detect-changed-files.outputs.testclis) }} # 使用矩阵策略运行多个测试命令
    env:
      # 合并自动检测和手动指定的组件列表
      TEST_COMPONENTS: ${{ needs.detect-changed-files.outputs.changed_components }} ${{ needs.detect-changed-files.outputs.manual_components }}
    steps:
      # 检出代码
      - uses: actions/checkout@v3

      # 设置pnpm
      - name: Setup pnpm
        uses: pnpm/action-setup@v2

      # 设置Node.js环境
      - name: Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 20

      # 缓存Playwright浏览器安装,加速工作流
      - name: Cache Playwright Installation
        uses: actions/cache@v3
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}

      # 获取pnpm缓存目录
      - name: Get pnpm store directory
        id: pnpm-cache
        run: |
          echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT

      # 设置pnpm缓存
      - uses: actions/cache@v3
        name: Setup pnpm cache
        with:
          path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      # 安装依赖
      - name: Install dependencies
        run: pnpm i --no-frozen-lockfile

      # 安装Playwright浏览器
      - name: Install Playwright browsers
        run: pnpm install:browser --with-deps chromium

      # 显示要测试的组件列表,便于调试
      - name: Show detected components
        run: |
          echo "Testing components: $TEST_COMPONENTS"

      # 运行E2E测试
      - name: E2E Test
        run: ${{ matrix.testcli }} ${{ env.TEST_COMPONENTS }} --retries=1 --workers=2 --pass-with-no-tests # 带重试和并行工作进程,无测试时不报错