From bce2dc86416fa494cc49c71180e06641b010ff0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Fri, 29 May 2026 23:51:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20E2E=20mock=20=E7=BB=9F=E4=B8=80=20+=20CS?= =?UTF-8?q?V=20=E5=BC=95=E5=8F=B7=E5=AD=97=E6=AE=B5=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E: - createIpcMock 支持 overrides 参数,保持 default: throw 行为 - search-clean.spec.ts 删除 30 行内联 mock,改用 createIpcMock(overrides) Rust: - 新增 parse_csv_line: 支持引号包裹字段 + 双引号转义 (RFC 4180 子集) - import_csv 改用 parse_csv_line 替代 split(',') - 与 TS 端 parseCsvLine 逻辑对称 测试: Rust 48 → 52 (+4), Frontend 100 Co-Authored-By: Claude Opus 4.7 --- core/src/fs.rs | 71 +++++++++++++++++++++++++++++++--- e2e/mocks/ipc.ts | 6 ++- e2e/tests/search-clean.spec.ts | 28 +++----------- 3 files changed, 77 insertions(+), 28 deletions(-) diff --git a/core/src/fs.rs b/core/src/fs.rs index d14e24c..5bf5e0f 100644 --- a/core/src/fs.rs +++ b/core/src/fs.rs @@ -64,6 +64,39 @@ fn import_json(content: &str) -> Result<(Vec, Vec), String> { Ok((sanitize_paths(data.system), sanitize_paths(data.user))) } +/// 解析 CSV 行,支持引号包裹的字段(RFC 4180 子集) +/// 与 TS 端 src/core/import-export.ts parseCsvLine 逻辑一致 +fn parse_csv_line(line: &str) -> Vec { + let mut fields = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + let mut chars = line.chars().peekable(); + + while let Some(ch) = chars.next() { + if in_quotes { + if ch == '"' { + if chars.peek() == Some(&'"') { + current.push('"'); + chars.next(); // 跳过转义引号 + } else { + in_quotes = false; + } + } else { + current.push(ch); + } + } else if ch == '"' { + in_quotes = true; + } else if ch == ',' { + fields.push(current); + current = String::new(); + } else { + current.push(ch); + } + } + fields.push(current); + fields +} + fn import_csv(content: &str) -> Result<(Vec, Vec), String> { let mut sys = Vec::new(); let mut usr = Vec::new(); @@ -81,17 +114,17 @@ fn import_csv(content: &str) -> Result<(Vec, Vec), String> { trimmed = stripped; } // 跳过 header 行 "type,path" - let fields: Vec<&str> = trimmed.split(',').collect(); - if fields.len() >= 2 { - let c0 = fields[0].trim().to_lowercase(); - let c1 = fields[1].trim().to_lowercase(); + let header_fields = parse_csv_line(trimmed); + if header_fields.len() >= 2 { + let c0 = header_fields[0].trim().to_lowercase(); + let c1 = header_fields[1].trim().to_lowercase(); if c0 == "type" && c1 == "path" { continue; } } } - let fields: Vec<&str> = trimmed.split(',').collect(); + let fields = parse_csv_line(trimmed); if fields.len() >= 2 { match fields[0].trim().to_lowercase().as_str() { "system" | "sys" => sys.push(fields[1].trim().to_string()), @@ -303,4 +336,32 @@ mod tests { let result = sanitize_paths(vec![" ".into(), "C:\\ok".into()]); assert_eq!(result, vec!["C:\\ok"]); } + + #[test] + fn parse_csv_line_basic() { + assert_eq!(parse_csv_line("a,b,c"), vec!["a", "b", "c"]); + } + + #[test] + fn parse_csv_line_quoted_comma() { + assert_eq!( + parse_csv_line(r#"system,"C:\Program Files, Inc\bin""#), + vec!["system", r#"C:\Program Files, Inc\bin"#] + ); + } + + #[test] + fn parse_csv_line_escaped_quotes() { + assert_eq!( + parse_csv_line(r#"system,"He said ""hello""""#), + vec!["system", r#"He said "hello""#] + ); + } + + #[test] + fn import_csv_quoted_comma_path() { + let csv = "type,path\nsystem,\"C:\\Program Files, Inc\\bin\"\n"; + let (sys, _) = import_csv(csv).unwrap(); + assert_eq!(sys, vec!["C:\\Program Files, Inc\\bin"]); + } } diff --git a/e2e/mocks/ipc.ts b/e2e/mocks/ipc.ts index 00acdbd..a59fb27 100644 --- a/e2e/mocks/ipc.ts +++ b/e2e/mocks/ipc.ts @@ -1,7 +1,11 @@ -export function createIpcMock() { +export type IpcOverrides = Partial>; + +export function createIpcMock(overrides: IpcOverrides = {}) { return ` window.__TAURI_INTERNALS__ = { invoke: async (cmd, args) => { + const overrides = ${JSON.stringify(overrides)}; + if (cmd in overrides) return overrides[cmd]; switch (cmd) { case 'check_admin': return true; case 'load_system_paths': return ['C:\\\\Windows', 'C:\\\\Program Files']; diff --git a/e2e/tests/search-clean.spec.ts b/e2e/tests/search-clean.spec.ts index b703d11..b37b5f7 100644 --- a/e2e/tests/search-clean.spec.ts +++ b/e2e/tests/search-clean.spec.ts @@ -1,28 +1,12 @@ import { test, expect } from '@playwright/test'; +import { createIpcMock } from '../mocks/ipc'; test.beforeEach(async ({ page }) => { - await page.addInitScript(() => { - window.__TAURI_INTERNALS__ = { - invoke: async (cmd, _args) => { - switch (cmd) { - case 'check_admin': return true; - case 'load_system_paths': return ['C:\\\\Windows', 'invalid_path', 'C:\\\\Temp']; - case 'load_user_paths': return []; - case 'load_disabled_state': return { system: [], user: [] }; - case 'save_system_paths': return undefined; - case 'save_user_paths': return undefined; - case 'save_disabled_state': return undefined; - case 'backup_registry': return ''; - case 'broadcast_env_change': return undefined; - case 'validate_path': return false; - case 'expand_env_vars': return ''; - case 'read_text_file': return ''; - case 'get_appdata_dir': return ''; - default: return undefined; - } - } - }; - }); + await page.addInitScript(createIpcMock({ + load_system_paths: ['C:\\Windows', 'invalid_path', 'C:\\Temp'], + load_user_paths: [], + validate_path: false, + })); await page.goto('/'); });