@@ -91,6 +91,7 @@ final class QueryViewModelTests: XCTestCase {
91 ["name": .string("Bob"), "age": .int(25)]
91 ["name": .string("Bob"), "age": .int(25)]
93 vm.columns = ["name", "age"]
93 vm.columns = ["name", "age"]
94+ vm.columnOrder = ["name", "age"]
94 let csv = vm.exportAsCSV()
95 let csv = vm.exportAsCSV()
96 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
97 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
@@ -106,6 +107,7 @@ final class QueryViewModelTests: XCTestCase {
106 ["msg": .string("hello, world"), "note": .string("has \"quotes\"")]
107 ["msg": .string("hello, world"), "note": .string("has \"quotes\"")]
108 vm.columns = ["msg", "note"]
109 vm.columns = ["msg", "note"]
110+ vm.columnOrder = ["msg", "note"]
109 let csv = vm.exportAsCSV()
111 let csv = vm.exportAsCSV()
111 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
113 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
@@ -269,5 +271,162 @@ final class QueryViewModelTests: XCTestCase {
269 vm.addColumnFilter(column: "msg", value: .string("it's a test"), exclude: false)
271 vm.addColumnFilter(column: "msg", value: .string("it's a test"), exclude: false)
270 XCTAssertTrue(vm.sqlQuery.contains("'it''s a test'"))
272 XCTAssertTrue(vm.sqlQuery.contains("'it''s a test'"))
275+ // MARK: - Column visibility
277+ func testVisibleColumnsExcludesHidden() {
278+ let vm = QueryViewModel()
279+ vm.columns = ["a", "b", "c"]
280+ vm.columnOrder = ["a", "b", "c"]
281+ vm.hiddenColumns = ["b"]
282+ XCTAssertEqual(vm.visibleColumns, ["a", "c"])
285+ func testToggleColumnVisibilityHidesColumn() {
286+ let vm = QueryViewModel()
287+ vm.columns = ["a", "b", "c"]
288+ vm.columnOrder = ["a", "b", "c"]
289+ vm.toggleColumnVisibility("b")
290+ XCTAssertTrue(vm.hiddenColumns.contains("b"))
291+ XCTAssertEqual(vm.visibleColumns, ["a", "c"])
294+ func testToggleColumnVisibilityShowsColumn() {
295+ let vm = QueryViewModel()
296+ vm.columns = ["a", "b", "c"]
297+ vm.columnOrder = ["a", "b", "c"]
298+ vm.hiddenColumns = ["b"]
299+ vm.toggleColumnVisibility("b")
300+ XCTAssertFalse(vm.hiddenColumns.contains("b"))
301+ XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
304+ func testCannotHideAllColumns() {
305+ let vm = QueryViewModel()
306+ vm.columns = ["a", "b"]
307+ vm.columnOrder = ["a", "b"]
308+ vm.toggleColumnVisibility("a")
309+ XCTAssertTrue(vm.hiddenColumns.contains("a"))
310+ // Trying to hide the last visible column should be prevented
311+ vm.toggleColumnVisibility("b")
312+ XCTAssertFalse(vm.hiddenColumns.contains("b"))
313+ XCTAssertEqual(vm.visibleColumns, ["b"])
316+ func testShowAllColumns() {
317+ let vm = QueryViewModel()
318+ vm.columns = ["a", "b", "c"]
319+ vm.columnOrder = ["a", "b", "c"]
320+ vm.hiddenColumns = ["a", "b"]
322+ XCTAssertTrue(vm.hiddenColumns.isEmpty)
323+ XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
326+ // MARK: - Column reordering
328+ func testMoveColumnByName() {
329+ let vm = QueryViewModel()
330+ vm.columns = ["a", "b", "c"]
331+ vm.columnOrder = ["a", "b", "c"]
332+ vm.moveColumn("c", to: "a")
333+ XCTAssertEqual(vm.columnOrder, ["c", "a", "b"])
336+ func testMoveColumnByIndexSet() {
337+ let vm = QueryViewModel()
338+ vm.columns = ["a", "b", "c"]
339+ vm.columnOrder = ["a", "b", "c"]
340+ vm.moveColumn(from: IndexSet(integer: 2), to: 0)
341+ XCTAssertEqual(vm.columnOrder, ["c", "a", "b"])
344+ func testResetColumnConfig() {
345+ let vm = QueryViewModel()
346+ vm.columns = ["a", "b", "c"]
347+ vm.columnOrder = ["c", "b", "a"]
348+ vm.hiddenColumns = ["b"]
349+ vm.resetColumnConfig()
350+ XCTAssertEqual(vm.columnOrder, ["a", "b", "c"])
351+ XCTAssertTrue(vm.hiddenColumns.isEmpty)
354+ func testVisibleColumnsRespectsOrder() {
355+ let vm = QueryViewModel()
356+ vm.columns = ["a", "b", "c"]
357+ vm.columnOrder = ["c", "a", "b"]
358+ vm.hiddenColumns = ["a"]
359+ XCTAssertEqual(vm.visibleColumns, ["c", "b"])
362+ // MARK: - Column config persistence
364+ func testApplyColumnConfigWithNoSavedConfig() {
365+ let vm = QueryViewModel()
366+ vm.applyColumnConfig(extractedColumns: ["x", "y", "z"], stream: nil)
367+ XCTAssertEqual(vm.columns, ["x", "y", "z"])
368+ XCTAssertEqual(vm.columnOrder, ["x", "y", "z"])
369+ XCTAssertTrue(vm.hiddenColumns.isEmpty)
372+ func testApplyColumnConfigMergesNewColumns() {
373+ let vm = QueryViewModel()
374+ // Save a config for "test_stream"
375+ let config = QueryViewModel.ColumnConfiguration(order: ["b", "a"], hidden: ["a"])
376+ if let data = try? JSONEncoder().encode(config) {
377+ UserDefaults.standard.set(data, forKey: "parseable_column_config_test_stream")
380+ // Apply with new columns that include an extra column "c"
381+ vm.applyColumnConfig(extractedColumns: ["a", "b", "c"], stream: "test_stream")
382+ XCTAssertEqual(vm.columnOrder, ["b", "a", "c"]) // saved order preserved, new "c" appended
383+ XCTAssertEqual(vm.hiddenColumns, ["a"]) // saved hidden preserved
386+ UserDefaults.standard.removeObject(forKey: "parseable_column_config_test_stream")
389+ func testApplyColumnConfigRemovesStaleSavedColumns() {
390+ let vm = QueryViewModel()
391+ // Save a config with a column that no longer exists
392+ let config = QueryViewModel.ColumnConfiguration(order: ["old", "a", "b"], hidden: ["old"])
393+ if let data = try? JSONEncoder().encode(config) {
394+ UserDefaults.standard.set(data, forKey: "parseable_column_config_test_stream2")
397+ vm.applyColumnConfig(extractedColumns: ["a", "b"], stream: "test_stream2")
398+ XCTAssertEqual(vm.columnOrder, ["a", "b"]) // "old" removed
399+ XCTAssertTrue(vm.hiddenColumns.isEmpty) // "old" was hidden but no longer exists
402+ UserDefaults.standard.removeObject(forKey: "parseable_column_config_test_stream2")
405+ func testClearResultsResetsColumnState() {
406+ let vm = QueryViewModel()
407+ vm.columns = ["a", "b"]
408+ vm.columnOrder = ["b", "a"]
409+ vm.hiddenColumns = ["a"]
411+ XCTAssertTrue(vm.columns.isEmpty)
412+ XCTAssertTrue(vm.columnOrder.isEmpty)
413+ XCTAssertTrue(vm.hiddenColumns.isEmpty)
416+ func testCSVExportUsesVisibleColumns() {
417+ let vm = QueryViewModel()
419+ ["name": .string("Alice"), "age": .int(30), "secret": .string("hidden")],
421+ vm.columns = ["name", "age", "secret"]
422+ vm.columnOrder = ["name", "age", "secret"]
423+ vm.hiddenColumns = ["secret"]
424+ let csv = vm.exportAsCSV()
426+ let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
427+ XCTAssertEqual(lines[0], "name,age")
428+ XCTAssertFalse(csv.contains("secret"))
429+ XCTAssertFalse(csv.contains("hidden"))