diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 00000000..d20c0fe4 --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a9f2ffc..afb80bfe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,3 +32,103 @@ jobs: - name: Run tests run: pnpm --filter @vaebe/ccui test + + e2e-coverage: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.x + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check E2E coverage + run: pnpm check:e2e-coverage + + e2e: + needs: e2e-coverage + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4] + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.x + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Chromium + run: pnpm --filter @vaebe/ccui-e2e exec playwright install --with-deps chromium + + - name: Run E2E tests + run: pnpm --filter @vaebe/ccui-e2e exec playwright test --shard=${{ matrix.shard }}/4 --reporter=blob + + - name: Upload E2E shard report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-blob-${{ matrix.shard }} + path: packages/e2e/blob-report/ + if-no-files-found: ignore + retention-days: 7 + + e2e-report: + if: ${{ !cancelled() }} + needs: e2e + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.x + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Download shard reports + uses: actions/download-artifact@v5 + with: + pattern: playwright-blob-* + path: packages/e2e/all-blob-reports + merge-multiple: true + + - name: Merge Playwright report + run: pnpm --filter @vaebe/ccui-e2e exec playwright merge-reports --reporter=html ./all-blob-reports + + - name: Upload merged E2E report + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: packages/e2e/playwright-report/ + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore index ea0781f7..c7a6a580 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,12 @@ packages/*/CHANGELOG.md # test packages/ccui/coverage packages/ccui/ui/**/__snapshots__ +packages/e2e/blob-report/ +packages/e2e/all-blob-reports/ +packages/e2e/playwright-report/ +packages/e2e/test-results/ +test-results/ .pnpm-debug.log .claude .codeflicker -docs-notes/screenshots \ No newline at end of file +docs-notes/screenshots diff --git a/CCUI-OPTIMIZATION-RISK-REPORT.md b/CCUI-OPTIMIZATION-RISK-REPORT.md deleted file mode 100644 index 36c4e15b..00000000 --- a/CCUI-OPTIMIZATION-RISK-REPORT.md +++ /dev/null @@ -1,391 +0,0 @@ -# ccui 组件待人工介入清单(58 条) - -> 经多代理重确认(每条 2 个对立视角 + 分歧裁决):原 131 条待审项中,0 误报;73 条已确认为安全自动修并已提交(commit 759b980);下列为**真正需要人工决策**的项——涉及对外行为/API 变更、快照/测试契约、设计取舍或较大重构。13 条标注「未复核」是裁决代理偶发失败、保守列入。 - - -## 响应式(行为/重构/契约) — 8 条 - - -### avatar -- **[high]** img/name/error/nobody 元素在 setup 期一次性构建,prop 变更后不重渲 - `avatar/src/avatar.tsx` @ 54-124, 138-145 - 方向:Move all four JSX const blocks AND the hasImgElement/hasNameElement helper bodies inside the returned render function so createVNode re-runs per render. Keep ns, styleNs, the backgroundNs computed, showErrorAvatar, and the watch outside. Concretely: `return () - -### config-provider -- **[high]** provide 注入的是 computed 的快照 ctx.value,子树永久丢失响应性 - `config-provider/src/config-provider.tsx` @ 76 - 方向:Provide the computed ref and unwrap it inside useConfig so all existing consumers keep reading plain properties. (1) Change line 76 to `provide(CONFIG_INJECT_KEY, ctx)`. (2) Type the key as `InjectionKey>` (or cast) and rewrite useCo - -### tabs -- **[medium]** 外部修改 modelValue 不会同步 active(缺 watch) - `tabs/src/tabs.tsx` @ 16 - 方向:需在 import 中加入 watch(当前 line 2 只导入了 defineComponent, provide, reactive),再在 setup 内 state 声明后添加同步逻辑: - -import { defineComponent, provide, reactive, watch } from 'vue' - -watch(() => props.modelValue, (v) => { if (v !== state.active) state.active = v }) - -加 v !== sta - -### tree -- **[medium]** defaultExpandAll 只在 setup 同步执行一次,异步/后到的 data 不生效 - `tree/src/tree.tsx` @ 72 - 方向:The bug is real but requires changes in BOTH files because the proposed watch alone won't propagate. Option A (recommended, minimal-risk): keep computing initialExpandedAll reactively AND make useControllableSet react to defaultValue changes when uncontrolled. - -### descriptions -- **[medium]** computed 内调用 slot 渲染函数并缓存 VNode,存在 VNode 复用风险 - `descriptions/src/descriptions.tsx` @ 36-37 - 方向:把 slot 的 VNode 调用从 computed 内推迟到 render 内(thunk 形态),slot 路径不缓存 VNode 实例。最小修改如下: - -1) ResolvedItem 的 label/content 改为可承载 thunk: - label: VNode | string | (() => VNode) - content: VNode | string | number | (() => VNode) - -2) resolveFromSlots 第36-37行存 thunk 而非立刻调 - -### textarea -- **[medium]** defaultValue 会吞掉“受控且初值为空字符串”的合法场景 - `textarea/src/textarea.tsx` @ 53 - 方向:The proposed fix `props.defaultValue ?? props.modelValue` is a no-op: in that branch props.modelValue is already '' (its default), so it equals the current `props.defaultValue ?? ''`. A correct fix requires making "provided" detectable: in textarea-types.ts ch - -### masonry -- **[medium]** ⚠️未复核 用数组下标 ci/ii 作 key,列数变化时 DOM 复用错位 - `masonry/src/masonry.tsx` @ 109,112 - 方向:用全局序号 gi 做稳定身份,优先 VNode 自带 key。修改 columns 计算携带 gi,并在渲染用 key={vnode.key ?? gi}。 - -1) 把 columns 计算改为携带原始全局序号(合并 sequential/默认两分支,因二者逻辑当前完全相同): - -const columns = computed(() => { - const items = flatChildren(slots.default?.() ?? []) - const cols: { vnode: VNode, gi - -### affix -- **[low]** watch target 仅监听 prop 引用变化,函数型 target 返回值改变时不会重新绑定 - `affix/src/affix.tsx` @ 153-160 - 方向:优先采用文档化方案:明确"函数型 target 仅在挂载时解析一次,引用不变则不重绑"。若要代码修复,不应放进每帧 scroll 都会跑的 update()(会引入每次滚动调用 resolveTarget + DOM 查询的开销与潜在反复 add/removeEventListener),而应仅在 onMounted 的 requestAnimationFrame 回调里或 resize/ResizeObserver 等低频时机做一次比对:`const next = resolveTarget(props.targ - -## 逻辑(设计取舍/时序) — 8 条 - - -### date-picker -- **[high]** showTime 下点击 clear 未重置 pendingValue / pendingDirty,面板残留旧高亮且 ok 仍可点 - `date-picker/src/date-picker.tsx` @ 401-405 - 方向:The proposed fix is directionally right but unsafe as written: it calls initialPendingTime(), which reads selectedDayjs.value (a computed off props.modelValue). emitChange(null) only emits update:modelValue and does NOT synchronously update selectedDayjs, so i - -### tag -- **[high]** variant 的 filled/solid/outlined 修饰类无任何样式,filled 与 solid 渲染完全一致 - `tag/src/tag.scss` @ 119 - 方向:The defect is real, but a blanket `&--variant-solid` background rule is not minimal/safe: solid's promise is per-preset-color saturation, and the preset colors are generated in the `@each` loop (lines 119-126) as `background: var(--ccui-#{$name}-1)` plus statu - -### message -- **[medium]** enforceMaxCount 用 arr.shift() 直接移除最旧消息,跳过 leave 过渡动画 - `message/src/use-message.ts` @ 80 - 方向:The bug is real but the suggested "call the oldest item's close" fix is NOT directly applicable: MessageItem (message-item.tsx) keeps `visible` and `close` internal to setup and never `expose`s them, and the holder renders children via h(MessageItem,...) with - -### modal -- **[medium]** Escape 关闭对所有打开的 modal 全局生效,栈式多层 modal 会被同时关闭 - `modal/src/modal.tsx` @ 80-84 - 方向:引入模块级栈并把 push/pop 绑定到 open/close 生命周期(不能只在 keydown 内处理): -1) 文件顶部加 `const modalEscStack: number[] = []`。 -2) onKeydown 改为仅当本实例位于栈顶才响应,并阻止冒泡: -``` -const onKeydown = (e: KeyboardEvent) => { - if (e.key !== 'Escape' || !props.closeOnEsc || !isOpen.value) return - if - -### table-column -- **[medium]** 全局自增 columnSeq 决定列序,动态重挂载的列会跳到末尾 - `table-column/src/table-column.tsx` @ 7, 19 - 方向:Do not apply the accuser's "minimal" variant as-is: locating by a stable key within the original `columns` order is invalid here, because in template-collection mode there is no `columns` array to index into — columns exist only via register(). The correct fix - -### input-number -- **[medium]** handleInput 在每次击键时即 clamp 到 min/max 并按 precision 取整,妨碍正常输入 - `input-number/src/input-number.tsx` @ 111 - 方向:Two-part change. (1) In handleInput, parse the raw value without clamp/precision so typing isn't disrupted, e.g. replace lines 111-112 with: `const raw = Number.parseFloat(value); if (!Number.isNaN(raw)) updateValue(raw, false)` — leaving clamp+precision to ha - -### space-compact -- **[medium]** size 生成的修饰类无任何样式承接,且未透传到子项,是死输出 - `space-compact/src/space-compact.tsx` @ 16 - 方向:Do NOT delete line 16 — that breaks existing assertions in test/space-compact.test.ts:24-26 requiring ccui-space-compact--large / --small. Take option (a): add the missing scss rules so the modifier is consumed. In space-compact.scss, inside the root block, ad - -### time-picker -- **[low]** 「此刻」按钮不遵守 step 与 disabled 约束 - `time-picker/src/time-picker.tsx` @ 196-203 (clickNow) - 方向:Align `now` to an existing, non-disabled cell using the same column value lists that render the panel, instead of naive step rounding (columns floor to multiples of step starting at 0, and disabled cells must be skipped). Add a helper and use it in clickNow: - - - -## 可访问性 — 25 条 - - -### color-picker -- **[high]** 清除按钮是嵌套在