mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
fix: E2E mock 统一 + CSV 引号字段解析
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 <noreply@anthropic.com>
This commit is contained in:
+66
-5
@@ -64,6 +64,39 @@ fn import_json(content: &str) -> Result<(Vec<String>, Vec<String>), 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<String> {
|
||||
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<String>, Vec<String>), String> {
|
||||
let mut sys = Vec::new();
|
||||
let mut usr = Vec::new();
|
||||
@@ -81,17 +114,17 @@ fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), 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"]);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
export function createIpcMock() {
|
||||
export type IpcOverrides = Partial<Record<string, unknown>>;
|
||||
|
||||
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'];
|
||||
|
||||
@@ -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('/');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user