From 733978c786ca3eb91c8568397513a19ef3ec6305 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 17:19:49 +0800 Subject: [PATCH 01/12] test: cover preview list old-to-new mapping --- .../components/virtual-preview-list.ts | 12 +++++++ tests/unit/virtual-preview-list.test.ts | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/unit/virtual-preview-list.test.ts diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index 5d137ec..53f41e6 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -98,6 +98,9 @@ export class VirtualPreviewList extends LitElement { return html`
+
+ ${item.file.name} +
${item.newName} ${statusBadge} @@ -229,6 +232,15 @@ export class VirtualPreviewList extends LitElement { min-width: 0; } + .old-name { + font-size: 12px; + color: var(--cdr-text-secondary, #595959); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .new-name { font-size: 14px; color: var(--cdr-text, #262626); diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts new file mode 100644 index 0000000..0d08077 --- /dev/null +++ b/tests/unit/virtual-preview-list.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list'; + +describe('VirtualPreviewList pending preview mapping', () => { + it('renders original name and new name for a pending preview item', async () => { + const element = new VirtualPreviewList(); + element.items = [ + { + file: { + id: 'file-1', + name: 'old-episode.mkv', + ext: 'mkv', + size: 42, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-episode.mkv', + conflict: false, + done: false, + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const textContent = element.shadowRoot?.textContent ?? ''; + expect(textContent).toContain('old-episode.mkv'); + expect(textContent).toContain('new-episode.mkv'); + + element.remove(); + }); +}); From add84da7b4130691312f5c341b061cad7943e147 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 17:38:22 +0800 Subject: [PATCH 02/12] feat: surface conflict and error info --- .../components/virtual-preview-list.ts | 54 +++++++++-- tests/unit/virtual-preview-list.test.ts | 89 +++++++++++++++++++ 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index 53f41e6..ce987ec 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -74,6 +74,15 @@ export class VirtualPreviewList extends LitElement { * Render a single preview item * @private */ + private resolveErrorText(error?: string): string | undefined { + if (!error) return undefined; + + const translationKey = `error_${error}`; + const translated = I18nService.t(translationKey); + + return translated === translationKey ? error : translated; + } + private renderPreviewItem(item: PreviewItem): any { const statusClass = item.conflict ? 'conflict' @@ -95,19 +104,28 @@ export class VirtualPreviewList extends LitElement { ? html`${I18nService.t('status_pending')}` : ''; + const errorText = this.resolveErrorText(item.error); + const errorTitle = item.error ?? errorText ?? ''; + const conflictHint = item.conflict + ? html`${I18nService.t('error_api_conflict')}` + : ''; + return html`
-
- ${item.file.name} -
-
- ${item.newName} +
+
+ ${item.file.name} +
${statusBadge}
+
+ ${item.newName} + ${conflictHint} +
${ - item.error - ? html`
${item.error}
` + errorText + ? html`
${errorText}
` : '' }
@@ -232,10 +250,16 @@ export class VirtualPreviewList extends LitElement { min-width: 0; } + .old-name-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + .old-name { font-size: 12px; color: var(--cdr-text-secondary, #595959); - margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -253,6 +277,14 @@ export class VirtualPreviewList extends LitElement { gap: 8px; } + .new-name-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .new-name.conflict { color: var(--cdr-warning-text, #fa8c16); } @@ -269,6 +301,12 @@ export class VirtualPreviewList extends LitElement { color: var(--cdr-text-secondary, #595959); } + .conflict-hint { + font-size: 12px; + color: var(--cdr-warning-text, #fa8c16); + flex-shrink: 0; + } + .error-message { margin-top: 4px; font-size: 12px; diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index 0d08077..a89c337 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list'; +import { I18nService } from '../../src/utils/i18n'; describe('VirtualPreviewList pending preview mapping', () => { it('renders original name and new name for a pending preview item', async () => { @@ -29,4 +30,92 @@ describe('VirtualPreviewList pending preview mapping', () => { element.remove(); }); + + it('renders conflict items with status badge and helper text', async () => { + const element = new VirtualPreviewList(); + element.items = [ + { + file: { + id: 'file-conflict', + name: 'old-conflict.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-conflict.mkv', + conflict: true, + done: false, + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const textContent = element.shadowRoot?.textContent ?? ''; + expect(textContent).toContain('old-conflict.mkv'); + expect(textContent).toContain('new-conflict.mkv'); + expect(textContent).toContain(I18nService.t('preview_badge_conflict')); + expect(textContent).toContain(I18nService.t('error_api_conflict')); + + const badge = element.shadowRoot?.querySelector('.old-name-row .status-badge.conflict'); + expect(badge).toBeTruthy(); + + element.remove(); + }); + + it('shows localized extract_episode_not_found message instead of raw error key', async () => { + const element = new VirtualPreviewList(); + element.items = [ + { + file: { + id: 'file-error', + name: 'old-error.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-error.mkv', + conflict: false, + error: 'extract_episode_not_found', + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const textContent = element.shadowRoot?.textContent ?? ''; + expect(textContent).toContain(I18nService.t('error_extract_episode_not_found')); + + element.remove(); + }); + + it('keeps pending badge next to old name row when showStatus is true', async () => { + const element = new VirtualPreviewList(); + element.showStatus = true; + element.items = [ + { + file: { + id: 'file-pending', + name: 'old-pending.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-pending.mkv', + conflict: false, + done: false, + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const badge = element.shadowRoot?.querySelector('.old-name-row .status-badge.pending'); + expect(badge).toBeTruthy(); + + element.remove(); + }); }); From f4fd8a450377b78914152e3d9fa03cb3a5054885 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 17:52:37 +0800 Subject: [PATCH 03/12] fix: skip i18n for raw error texts --- .../components/virtual-preview-list.ts | 8 +++++ tests/unit/virtual-preview-list.test.ts | 33 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index ce987ec..e505cbc 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -74,9 +74,17 @@ export class VirtualPreviewList extends LitElement { * Render a single preview item * @private */ + private isTranslationCandidate(error: string): boolean { + return /^[a-z0-9_]+$/.test(error); + } + private resolveErrorText(error?: string): string | undefined { if (!error) return undefined; + if (!this.isTranslationCandidate(error)) { + return error; + } + const translationKey = `error_${error}`; const translated = I18nService.t(translationKey); diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index a89c337..b917d36 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { VirtualPreviewList } from '../../src/content/components/virtual-preview-list'; import { I18nService } from '../../src/utils/i18n'; @@ -118,4 +118,35 @@ describe('VirtualPreviewList pending preview mapping', () => { element.remove(); }); + + it('renders raw error text without warning when given a network error message', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + const element = new VirtualPreviewList(); + element.items = [ + { + file: { + id: 'file-network', + name: 'old-network.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-network.mkv', + conflict: false, + error: 'Network error', + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const textContent = element.shadowRoot?.textContent ?? ''; + expect(textContent).toContain('Network error'); + expect(warnSpy).not.toHaveBeenCalled(); + + warnSpy.mockRestore(); + element.remove(); + }); }); From 702bf0def381f16dec9a2843e7bf2e0f351b9f07 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 18:07:00 +0800 Subject: [PATCH 04/12] chore: lock preview name structure --- .../components/virtual-preview-list.ts | 10 +++-- tests/unit/virtual-preview-list.test.ts | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index e505cbc..d5d3e71 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -117,17 +117,19 @@ export class VirtualPreviewList extends LitElement { const conflictHint = item.conflict ? html`${I18nService.t('error_api_conflict')}` : ''; + const previewItemClasses = ['preview-item', statusClass].filter(Boolean).join(' '); + const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' '); return html` -
-
+
+
-
+
${item.file.name}
${statusBadge}
-
+
${item.newName} ${conflictHint}
diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index b917d36..3f73701 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -149,4 +149,47 @@ describe('VirtualPreviewList pending preview mapping', () => { warnSpy.mockRestore(); element.remove(); }); + + it('locks preview-item/preview-content structure with secondary/primary name classes and truncation styles', async () => { + const element = new VirtualPreviewList(); + element.items = [ + { + file: { + id: 'file-structure', + name: 'old-very-long-name-that-should-truncate.mkv', + ext: 'mkv', + size: 1, + mtime: Date.now(), + isDir: false, + }, + newName: 'new-very-long-name-that-should-also-truncate.mkv', + conflict: false, + }, + ]; + + document.body.appendChild(element); + await element.updateComplete; + + const previewItem = element.shadowRoot?.querySelector('.preview-item[data-preview-item="true"]'); + expect(previewItem).toBeTruthy(); + + const previewContent = previewItem?.querySelector('.preview-content[data-preview-content="true"]'); + expect(previewContent).toBeTruthy(); + + const oldName = previewContent?.querySelector('.old-name.old-name-secondary'); + expect(oldName).toBeTruthy(); + + const newName = previewContent?.querySelector('.new-name.new-name-primary'); + expect(newName).toBeTruthy(); + const newNameText = newName?.querySelector('.new-name-text'); + expect(newNameText).toBeTruthy(); + const styleContent = Array.from(element.shadowRoot?.querySelectorAll('style') ?? []) + .map((style) => style.textContent ?? '') + .join(' '); + expect(styleContent).toContain('overflow: hidden'); + expect(styleContent).toContain('text-overflow: ellipsis'); + expect(styleContent).toContain('white-space: nowrap'); + + element.remove(); + }); }); From dc7b0addf634bf72d18c0fe7bde1e017884ce44a Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 18:22:27 +0800 Subject: [PATCH 05/12] fix: precise truncation assertions --- .../components/virtual-preview-list.ts | 9 +++++- tests/unit/virtual-preview-list.test.ts | 32 +++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index d5d3e71..5ef7fcb 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -275,6 +275,12 @@ export class VirtualPreviewList extends LitElement { white-space: nowrap; } + .old-name.old-name-secondary { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .new-name { font-size: 14px; color: var(--cdr-text, #262626); @@ -287,7 +293,8 @@ export class VirtualPreviewList extends LitElement { gap: 8px; } - .new-name-text { + .new-name-text, + .new-name.new-name-primary .new-name-text { flex: 1; min-width: 0; overflow: hidden; diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index 3f73701..5847543 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -150,7 +150,7 @@ describe('VirtualPreviewList pending preview mapping', () => { element.remove(); }); - it('locks preview-item/preview-content structure with secondary/primary name classes and truncation styles', async () => { + it('locks preview-item structure with semantic classes and target-specific truncation rules', async () => { const element = new VirtualPreviewList(); element.items = [ { @@ -170,25 +170,39 @@ describe('VirtualPreviewList pending preview mapping', () => { document.body.appendChild(element); await element.updateComplete; - const previewItem = element.shadowRoot?.querySelector('.preview-item[data-preview-item="true"]'); + + const previewItem = element.shadowRoot?.querySelector('.preview-item'); expect(previewItem).toBeTruthy(); - const previewContent = previewItem?.querySelector('.preview-content[data-preview-content="true"]'); + const previewContent = previewItem?.querySelector('.preview-content'); expect(previewContent).toBeTruthy(); const oldName = previewContent?.querySelector('.old-name.old-name-secondary'); expect(oldName).toBeTruthy(); - const newName = previewContent?.querySelector('.new-name.new-name-primary'); - expect(newName).toBeTruthy(); - const newNameText = newName?.querySelector('.new-name-text'); + const newNameText = previewContent?.querySelector('.new-name-text'); expect(newNameText).toBeTruthy(); + const styleContent = Array.from(element.shadowRoot?.querySelectorAll('style') ?? []) .map((style) => style.textContent ?? '') .join(' '); - expect(styleContent).toContain('overflow: hidden'); - expect(styleContent).toContain('text-overflow: ellipsis'); - expect(styleContent).toContain('white-space: nowrap'); + const extractBlock = (selector: string) => { + const start = styleContent.indexOf(selector); + expect(start, `missing ${selector}`).toBeGreaterThan(-1); + const braceOpen = styleContent.indexOf('{', start); + const braceClose = styleContent.indexOf('}', braceOpen); + return styleContent.slice(braceOpen + 1, braceClose); + }; + + const oldNameDeclarations = extractBlock('.old-name.old-name-secondary'); + expect(oldNameDeclarations).toContain('overflow: hidden'); + expect(oldNameDeclarations).toContain('text-overflow: ellipsis'); + expect(oldNameDeclarations).toContain('white-space: nowrap'); + + const newNameDeclarations = extractBlock('.new-name.new-name-primary .new-name-text'); + expect(newNameDeclarations).toContain('overflow: hidden'); + expect(newNameDeclarations).toContain('text-overflow: ellipsis'); + expect(newNameDeclarations).toContain('white-space: nowrap'); element.remove(); }); From 8c62159e5ec570c4a6a9aed6ba2d70ac3d2cf5b4 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 18:28:20 +0800 Subject: [PATCH 06/12] fix: drop test-only attrs --- src/content/components/virtual-preview-list.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index 5ef7fcb..b4a0689 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -121,8 +121,8 @@ export class VirtualPreviewList extends LitElement { const newNameClasses = ['new-name', 'new-name-primary', statusClass].filter(Boolean).join(' '); return html` -
-
+
+
${item.file.name} From 8b75e3074472d97b95284d3a2130b66e03da6131 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 18:39:31 +0800 Subject: [PATCH 07/12] refactor: stabilize truncation semantics --- .../components/virtual-preview-list.ts | 17 ++++++++++--- tests/unit/virtual-preview-list.test.ts | 25 +++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index b4a0689..2edc636 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -273,12 +273,16 @@ export class VirtualPreviewList extends LitElement { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + flex: 1 1 0; + min-width: 0; } .old-name.old-name-secondary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + flex: 1 1 0; + min-width: 0; } .new-name { @@ -293,9 +297,16 @@ export class VirtualPreviewList extends LitElement { gap: 8px; } - .new-name-text, - .new-name.new-name-primary .new-name-text { - flex: 1; + .new-name-text { + flex: 1 1 0; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .new-name-primary .new-name-text { + flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index 5847543..c9885fc 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -183,26 +183,29 @@ describe('VirtualPreviewList pending preview mapping', () => { const newNameText = previewContent?.querySelector('.new-name-text'); expect(newNameText).toBeTruthy(); - const styleContent = Array.from(element.shadowRoot?.querySelectorAll('style') ?? []) - .map((style) => style.textContent ?? '') + const stylesArray = Array.isArray(VirtualPreviewList.styles) + ? VirtualPreviewList.styles + : [VirtualPreviewList.styles]; + const staticCss = stylesArray + .map((result) => ('cssText' in result ? (result as any).cssText : result.toString())) .join(' '); const extractBlock = (selector: string) => { - const start = styleContent.indexOf(selector); + const start = staticCss.indexOf(selector); expect(start, `missing ${selector}`).toBeGreaterThan(-1); - const braceOpen = styleContent.indexOf('{', start); - const braceClose = styleContent.indexOf('}', braceOpen); - return styleContent.slice(braceOpen + 1, braceClose); + const braceOpen = staticCss.indexOf('{', start); + const braceClose = staticCss.indexOf('}', braceOpen); + return staticCss.slice(braceOpen + 1, braceClose); }; const oldNameDeclarations = extractBlock('.old-name.old-name-secondary'); + expect(oldNameDeclarations).toContain('flex: 1 1 0'); + expect(oldNameDeclarations).toContain('min-width: 0'); expect(oldNameDeclarations).toContain('overflow: hidden'); - expect(oldNameDeclarations).toContain('text-overflow: ellipsis'); - expect(oldNameDeclarations).toContain('white-space: nowrap'); - const newNameDeclarations = extractBlock('.new-name.new-name-primary .new-name-text'); + const newNameDeclarations = extractBlock('.new-name-primary .new-name-text'); + expect(newNameDeclarations).toContain('flex: 1 1 0'); + expect(newNameDeclarations).toContain('min-width: 0'); expect(newNameDeclarations).toContain('overflow: hidden'); - expect(newNameDeclarations).toContain('text-overflow: ellipsis'); - expect(newNameDeclarations).toContain('white-space: nowrap'); element.remove(); }); From 1b5bd627fe32010b5f30b969e9c508d8e4e7a9ee Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 18:42:50 +0800 Subject: [PATCH 08/12] refactor: clarify truncation ownership --- .../components/virtual-preview-list.ts | 14 ++--------- tests/unit/virtual-preview-list.test.ts | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/content/components/virtual-preview-list.ts b/src/content/components/virtual-preview-list.ts index 2edc636..c09e5ee 100644 --- a/src/content/components/virtual-preview-list.ts +++ b/src/content/components/virtual-preview-list.ts @@ -270,19 +270,14 @@ export class VirtualPreviewList extends LitElement { .old-name { font-size: 12px; color: var(--cdr-text-secondary, #595959); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1 1 0; - min-width: 0; } .old-name.old-name-secondary { + flex: 1 1 0; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - flex: 1 1 0; - min-width: 0; } .new-name { @@ -300,14 +295,9 @@ export class VirtualPreviewList extends LitElement { .new-name-text { flex: 1 1 0; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } .new-name-primary .new-name-text { - flex: 1 1 0; - min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index c9885fc..73fc276 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -189,7 +189,7 @@ describe('VirtualPreviewList pending preview mapping', () => { const staticCss = stylesArray .map((result) => ('cssText' in result ? (result as any).cssText : result.toString())) .join(' '); - const extractBlock = (selector: string) => { + const selectorBlock = (selector: string) => { const start = staticCss.indexOf(selector); expect(start, `missing ${selector}`).toBeGreaterThan(-1); const braceOpen = staticCss.indexOf('{', start); @@ -197,15 +197,25 @@ describe('VirtualPreviewList pending preview mapping', () => { return staticCss.slice(braceOpen + 1, braceClose); }; - const oldNameDeclarations = extractBlock('.old-name.old-name-secondary'); - expect(oldNameDeclarations).toContain('flex: 1 1 0'); - expect(oldNameDeclarations).toContain('min-width: 0'); + const oldNameDeclarations = selectorBlock('.old-name.old-name-secondary'); expect(oldNameDeclarations).toContain('overflow: hidden'); + expect(oldNameDeclarations).toContain('text-overflow: ellipsis'); + expect(oldNameDeclarations).toContain('white-space: nowrap'); - const newNameDeclarations = extractBlock('.new-name-primary .new-name-text'); - expect(newNameDeclarations).toContain('flex: 1 1 0'); - expect(newNameDeclarations).toContain('min-width: 0'); + const baseOldNameDeclarations = selectorBlock('.old-name'); + 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('text-overflow: ellipsis'); + 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(); }); From dc3364e7a42d5b9bfaca5d70de20c066541050c0 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 18:50:40 +0800 Subject: [PATCH 09/12] test: tighten selector parsing --- tests/unit/virtual-preview-list.test.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index 73fc276..38bbd81 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -190,11 +190,11 @@ describe('VirtualPreviewList pending preview mapping', () => { .map((result) => ('cssText' in result ? (result as any).cssText : result.toString())) .join(' '); const selectorBlock = (selector: string) => { - const start = staticCss.indexOf(selector); - expect(start, `missing ${selector}`).toBeGreaterThan(-1); - const braceOpen = staticCss.indexOf('{', start); - const braceClose = staticCss.indexOf('}', braceOpen); - return staticCss.slice(braceOpen + 1, braceClose); + const escaped = selector.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&'); + const regex = new RegExp(`${escaped}\\s*\\{([^}]*)\\}`, 's'); + const match = regex.exec(staticCss); + expect(match, `missing ${selector}`).toBeTruthy(); + return match![1]; }; const oldNameDeclarations = selectorBlock('.old-name.old-name-secondary'); @@ -203,6 +203,8 @@ describe('VirtualPreviewList pending preview mapping', () => { expect(oldNameDeclarations).toContain('white-space: nowrap'); 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'); From b7e4d944d3738ed33a4c5d1ffeb5e698b07bf3db Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 18:56:01 +0800 Subject: [PATCH 10/12] test: verify preview panel pass-through --- tests/unit/virtual-preview-list.test.ts | 85 +++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/unit/virtual-preview-list.test.ts b/tests/unit/virtual-preview-list.test.ts index 38bbd81..2c55b6b 100644 --- a/tests/unit/virtual-preview-list.test.ts +++ b/tests/unit/virtual-preview-list.test.ts @@ -1,4 +1,5 @@ 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 { I18nService } from '../../src/utils/i18n'; @@ -222,3 +223,87 @@ describe('VirtualPreviewList pending preview mapping', () => { 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(); + }); +}); From d96049b3b12753a0aac02ec66578d754d502d18c Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 19:06:17 +0800 Subject: [PATCH 11/12] chore: update planning files for Task 5 --- findings.md | 9 +++++++++ progress.md | 10 ++++++++++ task_plan.md | 17 +++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 findings.md create mode 100644 progress.md create mode 100644 task_plan.md diff --git a/findings.md b/findings.md new file mode 100644 index 0000000..89abb1f --- /dev/null +++ b/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`(62 个文件、497 个测试)已完成,所有测试通过,运行耗时约 15.4s。 +- 2026-03-20:`npm run lint` 成功退出但报告 270 条 `@typescript-eslint/no-explicit-any` 警告(适配器、规则、组件等使用 `any` 的位置),当前计划仅记录警告,不做改动。 +- 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`。 diff --git a/progress.md b/progress.md new file mode 100644 index 0000000..a534d30 --- /dev/null +++ b/progress.md @@ -0,0 +1,10 @@ +# 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`,62 个测试文件、497 个测试通过,完整运行耗时约 15.4 秒(未出现失败)。 +- 2026-03-20:运行 `npm run lint`,命令成功返回但报告 270 条 `@typescript-eslint/no-explicit-any` 警告(未列出错误);这些警告源自现有适配器/规则中使用的 `any`。 +- 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 无法提供,因此决定跳过执行并在输出中说明原因。 diff --git a/task_plan.md b/task_plan.md new file mode 100644 index 0000000..d91164f --- /dev/null +++ b/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 | 全量单测(62 个测试文件、497 个测试)通过,命令耗时 15.38s。 | +| 3. 运行 `npm run lint` 与 `npm run typecheck` | complete | `npm run lint` 成功但报告 270 个 `@typescript-eslint/no-explicit-any` 警告;`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 | +| --- | --- | --- | From 6b9b04ffb4310cd4fc06b63109c65a9f102770c0 Mon Sep 17 00:00:00 2001 From: Zain Date: Fri, 20 Mar 2026 19:08:24 +0800 Subject: [PATCH 12/12] docs: correct task 5 verification records --- findings.md | 4 ++-- progress.md | 5 +++-- task_plan.md | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/findings.md b/findings.md index 89abb1f..293d65f 100644 --- a/findings.md +++ b/findings.md @@ -3,7 +3,7 @@ - 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`(62 个文件、497 个测试)已完成,所有测试通过,运行耗时约 15.4s。 -- 2026-03-20:`npm run lint` 成功退出但报告 270 条 `@typescript-eslint/no-explicit-any` 警告(适配器、规则、组件等使用 `any` 的位置),当前计划仅记录警告,不做改动。 +- 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`。 diff --git a/progress.md b/progress.md index a534d30..d884443 100644 --- a/progress.md +++ b/progress.md @@ -4,7 +4,8 @@ - 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`,62 个测试文件、497 个测试通过,完整运行耗时约 15.4 秒(未出现失败)。 -- 2026-03-20:运行 `npm run lint`,命令成功返回但报告 270 条 `@typescript-eslint/no-explicit-any` 警告(未列出错误);这些警告源自现有适配器/规则中使用的 `any`。 +- 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 中早先写大的测试计数。 diff --git a/task_plan.md b/task_plan.md index d91164f..e17dd10 100644 --- a/task_plan.md +++ b/task_plan.md @@ -7,8 +7,8 @@ | 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 | 全量单测(62 个测试文件、497 个测试)通过,命令耗时 15.38s。 | -| 3. 运行 `npm run lint` 与 `npm run typecheck` | complete | `npm run lint` 成功但报告 270 个 `@typescript-eslint/no-explicit-any` 警告;`npm run typecheck`(`tsc --noEmit`)通过。 | +| 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 已记录所有命令/评估结论并准备提交。 |