mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-28 17:25: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)))
|
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> {
|
fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
||||||
let mut sys = Vec::new();
|
let mut sys = Vec::new();
|
||||||
let mut usr = Vec::new();
|
let mut usr = Vec::new();
|
||||||
@@ -81,17 +114,17 @@ fn import_csv(content: &str) -> Result<(Vec<String>, Vec<String>), String> {
|
|||||||
trimmed = stripped;
|
trimmed = stripped;
|
||||||
}
|
}
|
||||||
// 跳过 header 行 "type,path"
|
// 跳过 header 行 "type,path"
|
||||||
let fields: Vec<&str> = trimmed.split(',').collect();
|
let header_fields = parse_csv_line(trimmed);
|
||||||
if fields.len() >= 2 {
|
if header_fields.len() >= 2 {
|
||||||
let c0 = fields[0].trim().to_lowercase();
|
let c0 = header_fields[0].trim().to_lowercase();
|
||||||
let c1 = fields[1].trim().to_lowercase();
|
let c1 = header_fields[1].trim().to_lowercase();
|
||||||
if c0 == "type" && c1 == "path" {
|
if c0 == "type" && c1 == "path" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fields: Vec<&str> = trimmed.split(',').collect();
|
let fields = parse_csv_line(trimmed);
|
||||||
if fields.len() >= 2 {
|
if fields.len() >= 2 {
|
||||||
match fields[0].trim().to_lowercase().as_str() {
|
match fields[0].trim().to_lowercase().as_str() {
|
||||||
"system" | "sys" => sys.push(fields[1].trim().to_string()),
|
"system" | "sys" => sys.push(fields[1].trim().to_string()),
|
||||||
@@ -303,4 +336,32 @@ mod tests {
|
|||||||
let result = sanitize_paths(vec![" ".into(), "C:\\ok".into()]);
|
let result = sanitize_paths(vec![" ".into(), "C:\\ok".into()]);
|
||||||
assert_eq!(result, vec!["C:\\ok"]);
|
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 `
|
return `
|
||||||
window.__TAURI_INTERNALS__ = {
|
window.__TAURI_INTERNALS__ = {
|
||||||
invoke: async (cmd, args) => {
|
invoke: async (cmd, args) => {
|
||||||
|
const overrides = ${JSON.stringify(overrides)};
|
||||||
|
if (cmd in overrides) return overrides[cmd];
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 'check_admin': return true;
|
case 'check_admin': return true;
|
||||||
case 'load_system_paths': return ['C:\\\\Windows', 'C:\\\\Program Files'];
|
case 'load_system_paths': return ['C:\\\\Windows', 'C:\\\\Program Files'];
|
||||||
|
|||||||
@@ -1,28 +1,12 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { createIpcMock } from '../mocks/ipc';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.addInitScript(() => {
|
await page.addInitScript(createIpcMock({
|
||||||
window.__TAURI_INTERNALS__ = {
|
load_system_paths: ['C:\\Windows', 'invalid_path', 'C:\\Temp'],
|
||||||
invoke: async (cmd, _args) => {
|
load_user_paths: [],
|
||||||
switch (cmd) {
|
validate_path: false,
|
||||||
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.goto('/');
|
await page.goto('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user