Merge branch 'vibe/preview-panel-option-a'

# Conflicts:
#	src/content/components/virtual-preview-list.ts
#	tests/unit/virtual-preview-list.test.ts
这个提交包含在:
Zain 2026-03-20 20:58:15 +08:00
当前提交 ac517f16db
共有 5 个文件被更改,包括 156 次插入 和 18 次删除

9
findings.md 普通文件
查看文件

@ -0,0 +1,9 @@
# Findings
- 2026-03-20:Task 5 要求在 `vibe/preview-panel-option-a` 分支(HEAD `b7e4d944d3738ed33a4c5d1ffeb5e698b07bf3db`)上完成 `npx vitest run`、`npm run lint`、`npm run typecheck` 并更新 planning files。
- 2026-03-20:工作区 `/Users/lhly/chromeex/cloud-drive-renamer/.worktrees/preview-panel-option-a` 已切换到 Task 5 验证;前期 Task 4 结论仍保持可参考性但不再是当前目标。
- 2026-03-20:`tests/e2e/batch-rename.spec.ts` 直接访问 `https://pan.quark.cn`、等待扩展注入并操作 shadow DOM 中的 `rename-dialog`,依赖在线网站、未登录环境与真实 DOM,因此在常规 CI/本地环境中易失败、耗时并且不可控。
- 2026-03-20:主控复核 `npx vitest run` 的实际结果为 31 个测试文件、249 个测试,全部通过,运行耗时约 15.16s;早先子代理记录的 62/497 计数不准确,已修正。
- 2026-03-20:`npm run lint` 成功退出但报告 270 条 warning;其中以 `@typescript-eslint/no-explicit-any` 为主,也包含部分 `no-console` 与 `@typescript-eslint/no-unused-vars`,当前计划仅记录警告,不做改动。
- 2026-03-20:`npm run typecheck`(`tsc --noEmit`)已通过,未发现新的类型错误。
- 2026-03-20:Playwright 评估结论:由于测试需要访问 `https://pan.quark.cn`、等待扩展注入并登录,当前执行环境无法复现这些条件,因此决定跳过 `npx playwright test tests/e2e/batch-rename.spec.ts`。

11
progress.md 普通文件
查看文件

@ -0,0 +1,11 @@
# Progress
- 2026-03-20:接手 Task 5 验证,明确需运行 `npx vitest run`、`npm run lint`、`npm run typecheck` 并复核 Playwright 是否必要。
- 2026-03-20:根据规划指令阅读 `planning-with-files` 技能、执行 session catchup、确认 `/Users/lhly/chromeex/cloud-drive-renamer/.worktrees/preview-panel-option-a` 以及 `vibe/preview-panel-option-a`。
- 2026-03-20:查看 `tests/e2e/batch-rename.spec.ts`,确认它依赖 `https://pan.quark.cn` 页面、shadow DOM 交互,意味着 Playwright 测试在一般环境中需要真实扩展注入与登录凭据。
- 2026-03-20:更新 `task_plan.md`/`findings.md`/`progress.md` 以匹配 Task 5 的目标和执行流程,准备后续验证步骤。
- 2026-03-20:执行 `npx vitest run`,31 个测试文件、249 个测试通过,完整运行耗时约 15.16 秒(未出现失败)。
- 2026-03-20:运行 `npm run lint`,命令成功返回但报告 270 条 warning(以 `@typescript-eslint/no-explicit-any` 为主,也包含 `no-console` 与 `@typescript-eslint/no-unused-vars`);未列出错误。
- 2026-03-20:运行 `npm run typecheck`(`tsc --noEmit`)成功,未发现类型错误。
- 2026-03-20:评估 `npx playwright test tests/e2e/batch-rename.spec.ts`;由于测试需要访问 `https://pan.quark.cn`、等待扩展注入并登入,本地/CI 无法提供,因此决定跳过执行并在输出中说明原因。
- 2026-03-20:主控再次复核 `npx vitest run` / `npm run lint` / `npm run typecheck`,确认验证结果成立,并修正 planning files 中早先写大的测试计数。

查看文件

@ -121,8 +121,8 @@ export class VirtualPreviewList extends LitElement {
const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' '); const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' ');
return html` return html`
<div class="${previewItemClasses}" data-preview-item="true"> <div class="${previewItemClasses}">
<div class="preview-content" data-preview-content="true"> <div class="preview-content">
<div class="old-name-row"> <div class="old-name-row">
<div class="old-name old-name-secondary" title=${item.file.name}> <div class="old-name old-name-secondary" title=${item.file.name}>
${item.file.name} ${item.file.name}
@ -270,12 +270,11 @@ export class VirtualPreviewList extends LitElement {
.old-name { .old-name {
font-size: 12px; font-size: 12px;
color: var(--cdr-text-secondary, #595959); color: var(--cdr-text-secondary, #595959);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.old-name.old-name-secondary { .old-name.old-name-secondary {
flex: 1 1 0;
min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -293,10 +292,12 @@ export class VirtualPreviewList extends LitElement {
gap: 8px; gap: 8px;
} }
.new-name-text, .new-name-text {
.new-name.new-name-primary .new-name-text { flex: 1 1 0;
flex: 1;
min-width: 0; min-width: 0;
}
.new-name-primary .new-name-text {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;

17
task_plan.md 普通文件
查看文件

@ -0,0 +1,17 @@
# Task Plan
## Goal
完成 Task 5 的全量验证:运行 `npx vitest run`、`npm run lint`、`npm run typecheck`,评估是否有必要执行 `npx playwright test tests/e2e/batch-rename.spec.ts`,并以新的规划文件/验证状态提交变更。
## Phases
| Phase | Status | Notes |
| --- | --- | --- |
| 1. 校准 Task 5 上下文与规划 | complete | 已更新 planning files、确认工作区 `/Users/lhly/chromeex/cloud-drive-renamer/.worktrees/preview-panel-option-a` 以及分支 `vibe/preview-panel-option-a`。 |
| 2. 运行 `npx vitest run` | complete | 全量单测(31 个测试文件、249 个测试)通过,命令耗时 15.16s。 |
| 3. 运行 `npm run lint` 与 `npm run typecheck` | complete | `npm run lint` 成功但报告 270 条 warning(以 `@typescript-eslint/no-explicit-any` 为主,也包含 `no-console` 与 `@typescript-eslint/no-unused-vars`);`npm run typecheck`(`tsc --noEmit`)通过。 |
| 4. 评估 `npx playwright test tests/e2e/batch-rename.spec.ts` | complete | 该测试依赖 `https://pan.quark.cn` 站点、扩展注入与登录凭据,当前环境无凭据且不可控,因此评估后决定跳过执行。 |
| 5. 汇总结果、提交 planning files | complete | planning files 已记录所有命令/评估结论并准备提交。 |
## Errors Encountered
| Error | Attempt | Resolution |
| --- | --- | --- |

查看文件

@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import { PreviewPanel } from '../../src/content/components/preview-panel';
import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list'; import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list';
import { I18nService } from '../../src/utils/i18n'; import { I18nService } from '../../src/utils/i18n';
@ -183,27 +184,126 @@ describe('VirtualPreviewList pending preview mapping', () => {
const newNameText = previewContent?.querySelector('.new-name-text'); const newNameText = previewContent?.querySelector('.new-name-text');
expect(newNameText).toBeTruthy(); expect(newNameText).toBeTruthy();
const styleContent = Array.from(element.shadowRoot?.querySelectorAll('style') ?? []) const stylesArray = Array.isArray(VirtualPreviewList.styles)
.map((style) => style.textContent ?? '') ? VirtualPreviewList.styles
: [VirtualPreviewList.styles];
const staticCss = stylesArray
.map((result) => ('cssText' in result ? (result as any).cssText : result.toString()))
.join(' '); .join(' ');
const extractBlock = (selector: string) => { const selectorBlock = (selector: string) => {
const start = styleContent.indexOf(selector); const escaped = selector.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&');
expect(start, `missing ${selector}`).toBeGreaterThan(-1); const regex = new RegExp(`${escaped}\\s*\\{([^}]*)\\}`, 's');
const braceOpen = styleContent.indexOf('{', start); const match = regex.exec(staticCss);
const braceClose = styleContent.indexOf('}', braceOpen); expect(match, `missing ${selector}`).toBeTruthy();
return styleContent.slice(braceOpen + 1, braceClose); return match![1];
}; };
const oldNameDeclarations = extractBlock('.old-name.old-name-secondary'); const oldNameDeclarations = selectorBlock('.old-name.old-name-secondary');
expect(oldNameDeclarations).toContain('overflow: hidden'); expect(oldNameDeclarations).toContain('overflow: hidden');
expect(oldNameDeclarations).toContain('text-overflow: ellipsis'); expect(oldNameDeclarations).toContain('text-overflow: ellipsis');
expect(oldNameDeclarations).toContain('white-space: nowrap'); expect(oldNameDeclarations).toContain('white-space: nowrap');
const newNameDeclarations = extractBlock('.new-name.new-name-primary .new-name-text'); const baseOldNameDeclarations = selectorBlock('.old-name');
expect(baseOldNameDeclarations).toContain('font-size: 12px');
expect(baseOldNameDeclarations).toContain('color: var(--cdr-text-secondary, #595959)');
expect(baseOldNameDeclarations).not.toContain('overflow: hidden');
expect(baseOldNameDeclarations).not.toContain('text-overflow: ellipsis');
expect(baseOldNameDeclarations).not.toContain('white-space: nowrap');
const newNameDeclarations = selectorBlock('.new-name-primary .new-name-text');
expect(newNameDeclarations).toContain('overflow: hidden'); expect(newNameDeclarations).toContain('overflow: hidden');
expect(newNameDeclarations).toContain('text-overflow: ellipsis'); expect(newNameDeclarations).toContain('text-overflow: ellipsis');
expect(newNameDeclarations).toContain('white-space: nowrap'); expect(newNameDeclarations).toContain('white-space: nowrap');
const baseNewNameDeclarations = selectorBlock('.new-name-text');
expect(baseNewNameDeclarations).not.toContain('overflow: hidden');
expect(baseNewNameDeclarations).not.toContain('text-overflow: ellipsis');
expect(baseNewNameDeclarations).not.toContain('white-space: nowrap');
element.remove(); element.remove();
}); });
}); });
describe('PreviewPanel integration with VirtualPreviewList', () => {
it('passes mixed preview items and status mode through while keeping stats accurate', async () => {
const items = [
{
file: {
id: 'file-done',
name: 'done-old.txt',
ext: 'txt',
size: 1,
mtime: Date.now(),
isDir: false,
},
newName: 'done-new.txt',
conflict: false,
done: true,
},
{
file: {
id: 'file-error',
name: 'error-old.txt',
ext: 'txt',
size: 1,
mtime: Date.now(),
isDir: false,
},
newName: 'error-new.txt',
conflict: false,
error: 'extract_episode_not_found',
},
{
file: {
id: 'file-pending',
name: 'pending-old.txt',
ext: 'txt',
size: 1,
mtime: Date.now(),
isDir: false,
},
newName: 'pending-new.txt',
conflict: false,
done: false,
},
];
const panel = new PreviewPanel();
panel.items = items;
panel.conflictCount = 1;
panel.showStatus = true;
document.body.appendChild(panel);
await panel.updateComplete;
const stats = new Map(
Array.from(panel.shadowRoot?.querySelectorAll('.panel-stats .stat-item') ?? []).map((item) => [
item.querySelector('.stat-label')?.textContent?.trim() ?? '',
item.querySelector('.stat-value')?.textContent?.trim() ?? '',
])
);
expect(stats.get(I18nService.t('preview_items'))).toBe('3');
expect(stats.get(I18nService.t('progress_remaining'))).toBe('1');
expect(stats.get(I18nService.t('progress_success'))).toBe('1');
expect(stats.get(I18nService.t('progress_failed'))).toBe('1');
expect(stats.get(I18nService.t('preview_summary_conflict'))).toBe('1');
const previewList = panel.shadowRoot?.querySelector('virtual-preview-list') as VirtualPreviewList | null;
expect(previewList).toBeTruthy();
expect(previewList?.items).toBe(items);
expect(previewList?.showStatus).toBe(true);
await previewList?.updateComplete;
const previewText = previewList?.shadowRoot?.textContent ?? '';
expect(previewText).toContain('done-old.txt');
expect(previewText).toContain('done-new.txt');
expect(previewText).toContain('error-old.txt');
expect(previewText).toContain('error-new.txt');
expect(previewText).toContain('pending-old.txt');
expect(previewText).toContain('pending-new.txt');
panel.remove();
});
});