第66天:Element Plus 开发流程与代码规范 
学习目标 
- 了解 Element Plus 的开发流程和贡献指南
- 掌握项目的代码规范和最佳实践
- 学习如何设置开发环境和调试工具
- 理解组件开发的标准流程
知识点概览 
1. Element Plus 项目结构 
1.1 项目架构概览 
element-plus/
├── packages/                    # 核心包目录
│   ├── components/             # 组件源码
│   │   ├── button/            # 按钮组件
│   │   ├── input/             # 输入框组件
│   │   └── ...
│   ├── directives/            # 指令
│   ├── hooks/                 # 组合式函数
│   ├── locale/                # 国际化
│   ├── theme-chalk/           # 样式主题
│   ├── tokens/                # 设计令牌
│   └── utils/                 # 工具函数
├── docs/                       # 文档源码
├── play/                       # 开发调试环境
├── scripts/                    # 构建脚本
├── typings/                    # 类型定义
├── .github/                    # GitHub 配置
├── .vscode/                    # VS Code 配置
├── package.json
├── pnpm-workspace.yaml        # pnpm 工作空间配置
├── tsconfig.json              # TypeScript 配置
├── vite.config.ts             # Vite 配置
└── vitest.config.ts           # 测试配置1.2 开发环境配置 
bash
# 克隆项目
git clone https://github.com/element-plus/element-plus.git
cd element-plus
# 安装依赖(使用 pnpm)
pnpm install
# 启动开发服务器
pnpm dev
# 构建项目
pnpm build
# 运行测试
pnpm test
# 代码检查
pnpm lint
# 类型检查
pnpm typecheck2. 代码规范和风格指南 
2.1 TypeScript 代码规范 
typescript
// types/button.ts
// 组件类型定义规范
// 1. 导入顺序:第三方库 -> 内部模块 -> 类型导入
import { ExtractPropTypes, PropType } from 'vue'
import { isString } from '@element-plus/utils'
import type { Component } from 'vue'
// 2. 使用 const assertion 定义常量
export const buttonTypes = ['default', 'primary', 'success', 'warning', 'info', 'danger', 'text'] as const
export const buttonSizes = ['large', 'default', 'small'] as const
export const buttonNativeTypes = ['button', 'submit', 'reset'] as const
// 3. Props 定义使用 buildProps 工具函数
export const buttonProps = buildProps({
  /**
   * @description button size
   */
  size: {
    type: String as PropType<ButtonSize>,
    validator: (val: string): val is ButtonSize => buttonSizes.includes(val as ButtonSize)
  },
  /**
   * @description disable the button
   */
  disabled: Boolean,
  /**
   * @description button type
   */
  type: {
    type: String as PropType<ButtonType>,
    values: buttonTypes,
    default: 'default'
  },
  /**
   * @description icon component
   */
  icon: {
    type: iconPropType
  },
  /**
   * @description determine whether it's loading
   */
  loading: Boolean,
  /**
   * @description customize loading icon component
   */
  loadingIcon: {
    type: iconPropType,
    default: () => Loading
  },
  /**
   * @description native button type
   */
  nativeType: {
    type: String as PropType<ButtonNativeType>,
    values: buttonNativeTypes,
    default: 'button'
  },
  /**
   * @description determine whether it's a plain button
   */
  plain: Boolean,
  /**
   * @description determine whether it's a text button
   */
  text: Boolean,
  /**
   * @description determine whether it's a link button
   */
  link: Boolean,
  /**
   * @description determine whether the button is round
   */
  round: Boolean,
  /**
   * @description determine whether the button is circle
   */
  circle: Boolean,
  /**
   * @description custom button color, automatically calculate `hover` and `active` color
   */
  color: String,
  /**
   * @description dark mode, which automatically switches styles
   */
  dark: Boolean,
  /**
   * @description native autofocus attribute
   */
  autofocus: Boolean,
  /**
   * @description custom element tag
   */
  tag: {
    type: definePropType<string | Component>(String),
    default: 'button'
  }
} as const)
// 4. 导出类型
export type ButtonProps = ExtractPropTypes<typeof buttonProps>
export type ButtonType = typeof buttonTypes[number]
export type ButtonSize = typeof buttonSizes[number]
export type ButtonNativeType = typeof buttonNativeTypes[number]
// 5. Emits 定义
export const buttonEmits = {
  click: (evt: MouseEvent) => evt instanceof MouseEvent
}
export type ButtonEmits = typeof buttonEmits
// 6. 组件实例类型
export type ButtonInstance = InstanceType<typeof Button>2.2 Vue 组件规范 
vue
<!-- Button.vue -->
<!-- 1. 模板结构清晰,使用语义化标签 -->
<template>
  <component
    :is="tag"
    ref="_ref"
    :class="[
      ns.b(),
      ns.m(buttonType),
      ns.m(buttonSize),
      ns.is('disabled', buttonDisabled),
      ns.is('loading', loading),
      ns.is('plain', plain),
      ns.is('round', round),
      ns.is('circle', circle),
      ns.is('text', text),
      ns.is('link', link),
      ns.is('has-bg', hasBackground)
    ]"
    :aria-disabled="buttonDisabled || loading ? 'true' : 'false'"
    :disabled="buttonDisabled || loading || undefined"
    :autofocus="autofocus"
    :type="tag === 'button' ? nativeType : undefined"
    :style="buttonStyle"
    @click="handleClick"
  >
    <template v-if="loading">
      <slot v-if="$slots.loading" name="loading" />
      <el-icon v-else :class="ns.is('loading')">
        <component :is="loadingIcon" />
      </el-icon>
    </template>
    <el-icon v-else-if="icon || $slots.icon">
      <component :is="icon" v-if="icon" />
      <slot v-else name="icon" />
    </el-icon>
    <span
      v-if="$slots.default"
      :class="{ [ns.em('text', 'expand')]: shouldAddSpace }"
    >
      <slot />
    </span>
  </component>
</template>
<script lang="ts" setup>
// 2. 导入顺序规范
import { computed, inject, ref, useSlots } from 'vue'
import { ElIcon } from '@element-plus/components/icon'
import {
  useDisabled,
  useFormItem,
  useGlobalConfig,
  useNamespace,
  useSize
} from '@element-plus/hooks'
import { buttonGroupContextKey } from '@element-plus/tokens'
import { TinyColor } from '@ctrl/tinycolor'
import { buttonEmits, buttonProps } from './button'
import type { ButtonEmits, ButtonProps } from './button'
// 3. 定义组件选项
defineOptions({
  name: 'ElButton',
  inheritAttrs: false
})
// 4. Props 和 Emits
const props = defineProps(buttonProps)
const emit = defineEmits(buttonEmits)
// 5. 组合式函数使用
const slots = useSlots()
const buttonGroupContext = inject(buttonGroupContextKey, undefined)
const globalConfig = useGlobalConfig('button')
const ns = useNamespace('button')
const { form } = useFormItem()
const _size = useSize(computed(() => buttonGroupContext?.size))
const _disabled = useDisabled()
const _ref = ref<HTMLButtonElement>()
// 6. 计算属性
const buttonSize = computed(() => props.size || _size.value || 'default')
const buttonDisabled = computed(() => props.disabled || _disabled.value || false)
const buttonType = computed(() => props.type || buttonGroupContext?.type || 'default')
const autoInsertSpace = computed(
  () => props.autoInsertSpace ?? globalConfig.value?.autoInsertSpace ?? false
)
// 检查是否需要在文字两侧添加空格
const shouldAddSpace = computed(() => {
  const defaultSlot = slots.default?.()
  if (autoInsertSpace.value && defaultSlot?.length === 1) {
    const slot = defaultSlot[0]
    if (slot?.type === Text) {
      const text = slot.children as string
      return /^\p{Unified_Ideograph}{2}$/u.test(text.trim())
    }
  }
  return false
})
// 7. 样式计算
const buttonStyle = computed(() => {
  let styles: CSSProperties = {}
  
  const buttonColor = props.color
  if (buttonColor) {
    const color = new TinyColor(buttonColor)
    const activeBgColor = props.dark
      ? color.tint(20).toString()
      : color.shade(10).toString()
    
    if (props.plain) {
      styles = {
        '--el-button-bg-color': props.dark
          ? color.tint(90).toString()
          : color.tint(90).toString(),
        '--el-button-text-color': buttonColor,
        '--el-button-border-color': props.dark
          ? color.tint(50).toString()
          : color.tint(50).toString(),
        '--el-button-hover-text-color': `var(--el-color-white)`,
        '--el-button-hover-bg-color': buttonColor,
        '--el-button-hover-border-color': buttonColor,
        '--el-button-active-bg-color': activeBgColor,
        '--el-button-active-text-color': `var(--el-color-white)`,
        '--el-button-active-border-color': activeBgColor
      }
    } else {
      const hoverBgColor = props.dark
        ? color.tint(20).toString()
        : color.tint(20).toString()
      
      styles = {
        '--el-button-bg-color': buttonColor,
        '--el-button-border-color': buttonColor,
        '--el-button-hover-bg-color': hoverBgColor,
        '--el-button-hover-border-color': hoverBgColor,
        '--el-button-active-bg-color': activeBgColor,
        '--el-button-active-border-color': activeBgColor
      }
    }
    
    if (buttonDisabled.value) {
      const disabledButtonColor = props.dark
        ? color.tint(50).toString()
        : color.tint(50).toString()
      
      styles['--el-button-disabled-bg-color'] = disabledButtonColor
      styles['--el-button-disabled-border-color'] = disabledButtonColor
    }
  }
  
  return styles
})
const hasBackground = computed(() => {
  return (
    props.type !== 'text' &&
    props.type !== 'link' &&
    !props.plain
  )
})
// 8. 事件处理
const handleClick = (evt: MouseEvent) => {
  if (props.nativeType === 'reset') {
    form?.resetFields()
  }
  emit('click', evt)
}
// 9. 暴露给父组件的方法和属性
defineExpose({
  /** @description button html element */
  ref: _ref,
  /** @description button size */
  size: buttonSize,
  /** @description button type */
  type: buttonType,
  /** @description button disabled */
  disabled: buttonDisabled
})
</script>2.3 样式规范 (SCSS) 
scss
// button.scss
// 1. 使用 BEM 命名规范
@use 'sass:map';
@use 'mixins/mixins' as *;
@use 'mixins/utils' as *;
@use 'mixins/var' as *;
@use 'common/var' as *;
// 2. 组件变量定义
$button-font-weight: getCssVar('font-weight-primary') !default;
$button-border-width: getCssVar('border-width') !default;
$button-border-style: getCssVar('border-style') !default;
$button-border-color: getCssVar('border-color') !default;
$button-border-radius: getCssVar('border-radius-base') !default;
$button-bg-color: getCssVar('color-white') !default;
$button-font-size: getCssVar('font-size-base') !default;
$button-outline-color: getCssVar('color-primary-light-5') !default;
$button-active-color: getCssVar('color-primary-dark-2') !default;
// 3. 主要样式块
@include b(button) {
  // 基础样式
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 1;
  min-height: getCssVar('component-size');
  white-space: nowrap;
  cursor: pointer;
  color: getCssVar('button-text-color');
  text-align: center;
  box-sizing: border-box;
  outline: none;
  transition: 0.1s;
  font-weight: $button-font-weight;
  user-select: none;
  vertical-align: middle;
  -webkit-appearance: none;
  background-color: getCssVar('button-bg-color');
  border: $button-border-width $button-border-style
    getCssVar('button-border-color');
  border-radius: $button-border-radius;
  font-size: $button-font-size;
  padding: getCssVar('button-padding-vertical')
    getCssVar('button-padding-horizontal');
  // 悬停状态
  &:hover,
  &:focus {
    color: getCssVar('button-hover-text-color');
    border-color: getCssVar('button-hover-border-color');
    background-color: getCssVar('button-hover-bg-color');
    outline: none;
  }
  // 激活状态
  &:active {
    color: getCssVar('button-active-text-color');
    border-color: getCssVar('button-active-border-color');
    background-color: getCssVar('button-active-bg-color');
    outline: none;
  }
  // 焦点状态
  &:focus-visible {
    outline: 2px solid getCssVar('button-outline-color');
    outline-offset: 1px;
  }
  // 4. 修饰符样式
  @include m(primary) {
    @include button-variant(
      getCssVar('color-white'),
      getCssVar('color-primary'),
      getCssVar('color-primary')
    );
  }
  @include m(success) {
    @include button-variant(
      getCssVar('color-white'),
      getCssVar('color-success'),
      getCssVar('color-success')
    );
  }
  @include m(warning) {
    @include button-variant(
      getCssVar('color-white'),
      getCssVar('color-warning'),
      getCssVar('color-warning')
    );
  }
  @include m(danger) {
    @include button-variant(
      getCssVar('color-white'),
      getCssVar('color-danger'),
      getCssVar('color-danger')
    );
  }
  @include m(info) {
    @include button-variant(
      getCssVar('color-white'),
      getCssVar('color-info'),
      getCssVar('color-info')
    );
  }
  // 5. 尺寸变体
  @include m(large) {
    @include set-css-var-value('button-size', getCssVar('component-size-large'));
    height: getCssVar('button-size');
    padding: getCssVar('button-large-padding-vertical')
      getCssVar('button-large-padding-horizontal');
    font-size: getCssVar('button-large-font-size');
    border-radius: getCssVar('button-large-border-radius');
  }
  @include m(small) {
    @include set-css-var-value('button-size', getCssVar('component-size-small'));
    height: getCssVar('button-size');
    padding: getCssVar('button-small-padding-vertical')
      getCssVar('button-small-padding-horizontal');
    font-size: getCssVar('button-small-font-size');
    border-radius: getCssVar('button-small-border-radius');
  }
  // 6. 状态样式
  @include when(disabled) {
    &,
    &:hover,
    &:focus {
      color: getCssVar('button-disabled-text-color');
      cursor: not-allowed;
      background-image: none;
      background-color: getCssVar('button-disabled-bg-color');
      border-color: getCssVar('button-disabled-border-color');
    }
  }
  @include when(loading) {
    position: relative;
    pointer-events: none;
    &:before {
      // loading 遮罩
      z-index: 1;
      pointer-events: none;
      content: '';
      position: absolute;
      left: -1px;
      top: -1px;
      right: -1px;
      bottom: -1px;
      border-radius: inherit;
      background-color: getCssVar('mask-color-extra-light');
    }
  }
  @include when(plain) {
    @include css-var-from-global('button-hover-text-color', 'color-primary');
    @include css-var-from-global('button-hover-bg-color', 'color-primary-light-9');
    @include css-var-from-global('button-hover-border-color', 'color-primary');
  }
  @include when(round) {
    border-radius: getCssVar('border-radius-round');
  }
  @include when(circle) {
    border-radius: 50%;
    padding: getCssVar('button-padding-vertical');
    width: getCssVar('button-size');
    height: getCssVar('button-size');
  }
  @include when(text) {
    color: getCssVar('button-text-color');
    border: 0 solid transparent;
    background-color: transparent;
    @include when(disabled) {
      color: getCssVar('button-disabled-text-color');
      background-color: transparent !important;
    }
    &:not(.is-disabled) {
      &:hover,
      &:focus {
        background-color: getCssVar('button-hover-bg-color');
      }
      &:focus-visible {
        outline: 2px solid getCssVar('button-outline-color');
        outline-offset: 1px;
        border-radius: getCssVar('border-radius-base');
      }
      &:active {
        background-color: getCssVar('button-active-bg-color');
      }
    }
  }
  @include when(link) {
    border-color: transparent;
    color: getCssVar('button-text-color');
    background: transparent;
    padding: 2px;
    height: auto;
    &:hover,
    &:focus {
      color: getCssVar('button-hover-text-color');
    }
    @include when(disabled) {
      color: getCssVar('button-disabled-text-color');
      background-color: transparent !important;
      border-color: transparent !important;
    }
  }
  // 7. 元素样式
  @include e(text) {
    @include m(expand) {
      letter-spacing: 0.3em;
      margin-right: -0.3em;
    }
  }
  // 8. 图标样式
  .#{$namespace}-icon {
    & + span {
      margin-left: getCssVar('button-icon-span-gap');
    }
    svg {
      vertical-align: bottom;
    }
    @include when(loading) {
      animation: rotating 2s linear infinite;
    }
  }
  // 9. 响应式设计
  @include res(phone) {
    @include m(large) {
      padding: getCssVar('button-padding-vertical')
        getCssVar('button-padding-horizontal');
      font-size: getCssVar('button-font-size');
    }
  }
}
// 10. 按钮组样式
@include b(button-group) {
  @include utils-clearfix;
  display: inline-block;
  vertical-align: middle;
  &::before,
  &::after {
    display: table;
    content: '';
  }
  &::after {
    clear: both;
  }
  & > .#{$namespace}-button {
    float: left;
    position: relative;
    margin-left: 0;
    &:first-child {
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
      border-right-color: getCssVar('button-group-border-color');
    }
    &:last-child {
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
      border-left-color: getCssVar('button-group-border-color');
    }
    &:not(:first-child):not(:last-child) {
      border-radius: 0;
      border-left-color: getCssVar('button-group-border-color');
      border-right-color: getCssVar('button-group-border-color');
    }
    &:not(:last-child) {
      margin-right: -1px;
    }
    &:hover,
    &:focus,
    &:active {
      z-index: 1;
    }
    @include when(active) {
      z-index: 1;
    }
  }
  & > .#{$namespace}-dropdown {
    & > .#{$namespace}-button {
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
      border-left-color: getCssVar('button-group-border-color');
    }
  }
  @each $type in (primary, success, warning, danger, info) {
    .#{$namespace}-button--#{$type} {
      &:first-child {
        border-right-color: getCssVar('button', $type, 'border-color');
      }
      &:last-child {
        border-left-color: getCssVar('button', $type, 'border-color');
      }
      &:not(:first-child):not(:last-child) {
        border-left-color: getCssVar('button', $type, 'border-color');
        border-right-color: getCssVar('button', $type, 'border-color');
      }
    }
  }
}3. 测试规范 
3.1 单元测试规范 
typescript
// __tests__/button.test.tsx
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it, test, vi } from 'vitest'
import { Loading, Search } from '@element-plus/icons-vue'
import Button from '../src/button.vue'
import ButtonGroup from '../src/button-group.vue'
// 1. 测试组织结构
describe('Button', () => {
  // 2. 基础功能测试
  describe('basic functionality', () => {
    it('should render correctly', () => {
      const wrapper = mount(Button, {
        slots: {
          default: 'Test Button'
        }
      })
      
      expect(wrapper.text()).toBe('Test Button')
      expect(wrapper.classes()).toContain('el-button')
    })
    it('should handle click events', async () => {
      const handleClick = vi.fn()
      const wrapper = mount(Button, {
        props: {
          onClick: handleClick
        },
        slots: {
          default: 'Click Me'
        }
      })
      await wrapper.trigger('click')
      expect(handleClick).toHaveBeenCalledTimes(1)
    })
    it('should be disabled when disabled prop is true', async () => {
      const wrapper = mount(Button, {
        props: {
          disabled: true
        }
      })
      expect(wrapper.classes()).toContain('is-disabled')
      expect(wrapper.attributes('disabled')).toBeDefined()
    })
  })
  // 3. Props 测试
  describe('props', () => {
    it('should render different types correctly', () => {
      const types = ['primary', 'success', 'warning', 'danger', 'info']
      
      types.forEach(type => {
        const wrapper = mount(Button, {
          props: { type }
        })
        expect(wrapper.classes()).toContain(`el-button--${type}`)
      })
    })
    it('should render different sizes correctly', () => {
      const sizes = ['large', 'default', 'small']
      
      sizes.forEach(size => {
        const wrapper = mount(Button, {
          props: { size }
        })
        if (size !== 'default') {
          expect(wrapper.classes()).toContain(`el-button--${size}`)
        }
      })
    })
    it('should show loading state', () => {
      const wrapper = mount(Button, {
        props: {
          loading: true
        }
      })
      expect(wrapper.classes()).toContain('is-loading')
      expect(wrapper.findComponent(Loading).exists()).toBe(true)
    })
    it('should render custom loading icon', () => {
      const wrapper = mount(Button, {
        props: {
          loading: true,
          loadingIcon: Search
        }
      })
      expect(wrapper.findComponent(Search).exists()).toBe(true)
    })
    it('should render icon correctly', () => {
      const wrapper = mount(Button, {
        props: {
          icon: Search
        }
      })
      expect(wrapper.findComponent(Search).exists()).toBe(true)
    })
  })
  // 4. 插槽测试
  describe('slots', () => {
    it('should render icon slot', () => {
      const wrapper = mount(Button, {
        slots: {
          icon: '<i class="custom-icon"></i>'
        }
      })
      expect(wrapper.find('.custom-icon').exists()).toBe(true)
    })
    it('should render loading slot', () => {
      const wrapper = mount(Button, {
        props: {
          loading: true
        },
        slots: {
          loading: '<span class="custom-loading">Loading...</span>'
        }
      })
      expect(wrapper.find('.custom-loading').exists()).toBe(true)
    })
  })
  // 5. 事件测试
  describe('events', () => {
    it('should not trigger click when disabled', async () => {
      const handleClick = vi.fn()
      const wrapper = mount(Button, {
        props: {
          disabled: true,
          onClick: handleClick
        }
      })
      await wrapper.trigger('click')
      expect(handleClick).not.toHaveBeenCalled()
    })
    it('should not trigger click when loading', async () => {
      const handleClick = vi.fn()
      const wrapper = mount(Button, {
        props: {
          loading: true,
          onClick: handleClick
        }
      })
      await wrapper.trigger('click')
      expect(handleClick).not.toHaveBeenCalled()
    })
  })
  // 6. 样式测试
  describe('styles', () => {
    it('should apply custom color', () => {
      const customColor = '#ff0000'
      const wrapper = mount(Button, {
        props: {
          color: customColor
        }
      })
      const style = wrapper.attributes('style')
      expect(style).toContain('--el-button-bg-color')
    })
    it('should add space between Chinese characters', async () => {
      const wrapper = mount(Button, {
        props: {
          autoInsertSpace: true
        },
        slots: {
          default: '按钮'
        }
      })
      await nextTick()
      expect(wrapper.find('.el-button__text--expand').exists()).toBe(true)
    })
  })
  // 7. 可访问性测试
  describe('accessibility', () => {
    it('should have correct aria attributes when disabled', () => {
      const wrapper = mount(Button, {
        props: {
          disabled: true
        }
      })
      expect(wrapper.attributes('aria-disabled')).toBe('true')
    })
    it('should have correct aria attributes when loading', () => {
      const wrapper = mount(Button, {
        props: {
          loading: true
        }
      })
      expect(wrapper.attributes('aria-disabled')).toBe('true')
    })
  })
})
// 8. 按钮组测试
describe('ButtonGroup', () => {
  it('should render button group correctly', () => {
    const wrapper = mount(ButtonGroup, {
      slots: {
        default: `
          <el-button>Button 1</el-button>
          <el-button>Button 2</el-button>
        `
      },
      global: {
        components: {
          'el-button': Button
        }
      }
    })
    expect(wrapper.classes()).toContain('el-button-group')
    expect(wrapper.findAllComponents(Button)).toHaveLength(2)
  })
  it('should apply group size to buttons', () => {
    const wrapper = mount(ButtonGroup, {
      props: {
        size: 'large'
      },
      slots: {
        default: '<el-button>Button</el-button>'
      },
      global: {
        components: {
          'el-button': Button
        }
      }
    })
    const button = wrapper.findComponent(Button)
    expect(button.vm.size).toBe('large')
  })
})3.2 E2E 测试规范 
typescript
// e2e/button.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Button E2E Tests', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/components/button')
  })
  test('should render basic button', async ({ page }) => {
    const button = page.locator('.el-button').first()
    await expect(button).toBeVisible()
    await expect(button).toHaveText('Default')
  })
  test('should handle click interaction', async ({ page }) => {
    const button = page.locator('[data-testid="click-button"]')
    const counter = page.locator('[data-testid="click-counter"]')
    
    await expect(counter).toHaveText('0')
    await button.click()
    await expect(counter).toHaveText('1')
  })
  test('should show loading state', async ({ page }) => {
    const loadingButton = page.locator('[data-testid="loading-button"]')
    await loadingButton.click()
    
    await expect(loadingButton).toHaveClass(/is-loading/)
    await expect(loadingButton.locator('.el-icon')).toBeVisible()
  })
  test('should be keyboard accessible', async ({ page }) => {
    const button = page.locator('.el-button').first()
    
    // 使用 Tab 键聚焦
    await page.keyboard.press('Tab')
    await expect(button).toBeFocused()
    
    // 使用 Enter 键激活
    await page.keyboard.press('Enter')
    // 验证点击效果
  })
  test('should support different themes', async ({ page }) => {
    const primaryButton = page.locator('.el-button--primary').first()
    const successButton = page.locator('.el-button--success').first()
    
    await expect(primaryButton).toHaveCSS('background-color', 'rgb(64, 158, 255)')
    await expect(successButton).toHaveCSS('background-color', 'rgb(103, 194, 58)')
  })
  test('should work in button group', async ({ page }) => {
    const buttonGroup = page.locator('.el-button-group').first()
    const buttons = buttonGroup.locator('.el-button')
    
    await expect(buttons).toHaveCount(3)
    
    // 检查第一个按钮的边框半径
    const firstButton = buttons.first()
    await expect(firstButton).toHaveCSS('border-top-right-radius', '0px')
    await expect(firstButton).toHaveCSS('border-bottom-right-radius', '0px')
  })
})4. 文档规范 
4.1 组件文档结构 
markdown
# Button 按钮
常用的操作按钮。
## 基础用法
基础的按钮用法。
:::demo 使用 `type`、`plain`、`round` 和 `circle` 属性来定义 Button 的样式。
button/basic
:::
## 禁用状态
按钮不可用状态。
:::demo 你可以使用 `disabled` 属性来定义按钮是否可用,它接受一个 `Boolean` 值。
button/disabled
:::
## 链接按钮
没有边框和背景色的按钮。
:::demo 使用 `link` 属性创建链接按钮,通常用于页面内的功能性链接。
button/link
:::
## 文字按钮
没有边框和背景色的按钮。
:::demo 使用 `text` 属性创建文字按钮。
button/text
:::
## 图标按钮
带图标的按钮可增强辨识度(有文字)或节省空间(无文字)。
:::demo 设置 `icon` 属性即可,icon 的列表可以参考 Element Plus 的 icon 组件,也可以设置在文字右边的 icon ,只要使用 `i` 标签即可,可以使用自定义图标。
button/icon
:::
## 按钮组
以按钮组的方式出现,常用于多项类似操作。
:::demo 使用 `<el-button-group>` 标签来嵌套你的按钮。
button/group
:::
## 加载中
点击按钮后进行数据加载操作,在按钮上显示加载状态。
:::demo 要设置为 loading 状态,只要设置 `loading` 属性为 `true` 即可。
button/loading
:::
## 不同尺寸
Button 组件提供除了默认值以外的三种尺寸,可以在不同场景下选择合适的按钮尺寸。
:::demo 额外的尺寸:`large`、`small`,通过设置 `size` 属性来配置它们。
button/size
:::
## 自定义颜色
您可以自定义按钮颜色。
:::demo 我们将自动计算 hover 和 active 颜色。
button/custom-color
:::
## Button API
### Button Attributes
| 属性名                | 说明                                                         | 类型                                                         | 默认值    |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | --------- |
| size                  | 尺寸                                                         | `enum` - `'large'\| 'default' \| 'small'`                    | —         |
| type                  | 类型                                                         | `enum` - `'primary'\| 'success' \| 'warning' \| 'danger' \| 'info'` | —         |
| plain                 | 是否朴素按钮                                                 | `boolean`                                                    | `false`   |
| text ^(2.2.0)         | 是否文字按钮                                                 | `boolean`                                                    | `false`   |
| link ^(2.2.1)         | 是否链接按钮                                                 | `boolean`                                                    | `false`   |
| round                 | 是否圆角按钮                                                 | `boolean`                                                    | `false`   |
| circle                | 是否圆形按钮                                                 | `boolean`                                                    | `false`   |
| loading               | 是否加载中状态                                               | `boolean`                                                    | `false`   |
| loading-icon          | 自定义加载中状态图标组件                                     | `string \| Component`                                        | `Loading` |
| disabled              | 按钮是否为禁用状态                                           | `boolean`                                                    | `false`   |
| icon                  | 图标组件                                                     | `string \| Component`                                        | —         |
| autofocus             | 原生 `autofocus` 属性                                        | `boolean`                                                    | `false`   |
| native-type           | 原生 `type` 属性                                             | `enum` - `'button' \| 'submit' \| 'reset'`                   | `button`  |
| auto-insert-space     | 自动在两个中文字符之间插入空格                               | `boolean`                                                    | —         |
| color                 | 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 | `string`                                                     | —         |
| dark                  | dark 模式, 意味着自动设置 `color` 为 dark 模式的颜色        | `boolean`                                                    | `false`   |
| tag ^(2.3.4)          | 自定义元素标签                                               | `string \| Component`                                        | `button`  |
### Button Events
| 事件名 | 说明             | 类型                      |
| ------ | ---------------- | ------------------------- |
| click  | 点击按钮时触发   | `(event: MouseEvent) => void` |
### Button Slots
| 插槽名  | 说明           |
| ------- | -------------- |
| default | 自定义默认内容 |
| loading | 自定义加载中组件 |
| icon    | 自定义图标组件 |
### Button Exposes
| 名称     | 说明           | 类型                 |
| -------- | -------------- | -------------------- |
| ref      | 按钮 html 元素 | `Ref<HTMLButtonElement>` |
| size     | 按钮尺寸       | `ComputedRef<string>`    |
| type     | 按钮类型       | `ComputedRef<string>`    |
| disabled | 按钮是否禁用   | `ComputedRef<boolean>`   |
## ButtonGroup API
### ButtonGroup Attributes
| 属性名 | 说明                                             | 类型                                              | 默认值 |
| ------ | ------------------------------------------------ | ------------------------------------------------- | ------ |
| size   | 用于控制该按钮组内按钮的大小                     | `enum` - `'large' \| 'default' \| 'small'`        | —      |
| type   | 用于控制该按钮组内按钮的类型                     | `enum` - `'primary' \| 'success' \| 'warning' \| 'danger' \| 'info'` | —      |
### ButtonGroup Slots
| 插槽名  | 说明                    | 子标签 |
| ------- | ----------------------- | ------ |
| default | 自定义按钮组内容        | Button |实践练习 
练习 1:创建自定义按钮组件 
基于 Element Plus 的代码规范,创建一个自定义的按钮组件:
- 支持所有标准按钮功能
- 添加自定义动画效果
- 完整的 TypeScript 类型定义
- 完善的测试用例
练习 2:贡献代码到 Element Plus 
尝试为 Element Plus 项目贡献代码:
- Fork 项目并设置开发环境
- 找到一个 issue 或提出改进建议
- 按照规范编写代码
- 提交 Pull Request
练习 3:编写组件文档 
为自定义组件编写完整的文档:
- API 文档
- 使用示例
- 最佳实践指南
- 常见问题解答
学习资源 
作业 
- 完成所有实践练习
- 阅读 Element Plus 源码,理解组件实现原理
- 参与 Element Plus 社区讨论
- 尝试修复一个简单的 bug 或添加小功能
下一步学习计划 
接下来我们将学习 Element Plus 代码贡献与 Pull Request 流程,深入了解如何参与开源项目的开发和维护。