2@testable import ParseableViewer
4final class QueryViewModelTests: XCTestCase {
5 // MARK: - Column extraction
7 func testExtractColumnsOrdersPriorityFieldsFirst() {
8 let vm = QueryViewModel()
9 let records: [LogRecord] = [
11 "custom_field": .string("val"),
12 "message": .string("hello"),
13 "p_timestamp": .string("2024-01-01"),
14 "level": .string("info"),
19 // Simulate the column extraction by running a query with mock data
20 // Since we can't easily mock the client, test the public interface
21 // by verifying filteredResults and columns after setting them directly
23 vm.columns = vm.extractColumns(from: records)
25 // Priority fields should come first
26 XCTAssertEqual(vm.columns[0], "p_timestamp")
27 XCTAssertEqual(vm.columns[1], "level")
28 XCTAssertEqual(vm.columns[2], "message")
30 // Remaining fields sorted alphabetically
31 let remaining = Array(vm.columns.dropFirst(3))
32 XCTAssertEqual(remaining, remaining.sorted())
35 func testExtractColumnsEmpty() {
36 let vm = QueryViewModel()
37 let cols = vm.extractColumns(from: [])
38 XCTAssertTrue(cols.isEmpty)
41 func testExtractColumnsMergesAcrossRecords() {
42 let vm = QueryViewModel()
43 let records: [LogRecord] = [
44 ["a": .int(1), "b": .int(2)],
45 ["b": .int(3), "c": .int(4)]
47 let cols = vm.extractColumns(from: records)
48 XCTAssertEqual(Set(cols), Set(["a", "b", "c"]))
49 XCTAssertEqual(cols.count, 3) // no duplicates
52 // MARK: - Filtered results
54 func testFilteredResultsEmptyFilter() {
55 let vm = QueryViewModel()
57 ["message": .string("hello")],
58 ["message": .string("world")]
61 XCTAssertEqual(vm.filteredResults.count, 2)
64 func testFilteredResultsMatchesContent() async {
65 let vm = QueryViewModel()
67 ["message": .string("hello world")],
68 ["message": .string("goodbye moon")],
69 ["message": .string("Hello there")]
71 vm.filterText = "hello"
72 try? await Task.sleep(for: .milliseconds(200))
73 XCTAssertEqual(vm.filteredResults.count, 2) // case-insensitive
76 func testFilteredResultsNoMatch() async {
77 let vm = QueryViewModel()
79 ["message": .string("hello")],
80 ["message": .string("world")]
83 try? await Task.sleep(for: .milliseconds(200))
84 XCTAssertEqual(vm.filteredResults.count, 0)
89 func testExportCSVBasic() {
90 let vm = QueryViewModel()
92 ["name": .string("Alice"), "age": .int(30)],
93 ["name": .string("Bob"), "age": .int(25)]
95 vm.columns = ["name", "age"]
96 vm.columnOrder = ["name", "age"]
97 let csv = vm.exportAsCSV()
99 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
100 XCTAssertEqual(lines.count, 3) // header + 2 rows
101 XCTAssertEqual(lines[0], "name,age")
102 XCTAssertEqual(lines[1], "Alice,30")
103 XCTAssertEqual(lines[2], "Bob,25")
106 func testExportCSVEscaping() {
107 let vm = QueryViewModel()
109 ["msg": .string("hello, world"), "note": .string("has \"quotes\"")]
111 vm.columns = ["msg", "note"]
112 vm.columnOrder = ["msg", "note"]
113 let csv = vm.exportAsCSV()
115 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
116 XCTAssertEqual(lines.count, 2)
117 XCTAssertTrue(lines[1].contains("\"hello, world\""))
118 XCTAssertTrue(lines[1].contains("\"has \"\"quotes\"\"\""))
121 func testExportCSVEmpty() {
122 let vm = QueryViewModel()
123 XCTAssertEqual(vm.exportAsCSV(), "")
126 // MARK: - JSON export
128 func testExportJSONEmpty() {
129 let vm = QueryViewModel()
130 XCTAssertEqual(vm.exportAsJSON(), "[]")
133 func testExportJSONNotEmpty() {
134 let vm = QueryViewModel()
135 vm.results = [["key": .string("value")]]
136 let json = vm.exportAsJSON()
137 XCTAssertTrue(json.contains("\"key\""))
138 XCTAssertTrue(json.contains("\"value\""))
141 func testProjectRecordsExcludesHiddenColumns() {
142 let records: [LogRecord] = [["visible": .string("v"), "hidden": .string("secret")]]
143 let projected = QueryViewModel.projectRecords(records, to: ["visible"])
144 XCTAssertEqual(projected.first?.keys.sorted(), ["visible"])
145 XCTAssertNil(projected.first?["hidden"], "Hidden column must not leak into export")
148 func testProjectRecordsEmptyColumnsKeepsAllFields() {
149 let records: [LogRecord] = [["a": .int(1), "b": .int(2)]]
150 let projected = QueryViewModel.projectRecords(records, to: [])
151 XCTAssertEqual(projected.first?.keys.sorted(), ["a", "b"])
154 // MARK: - Time range
156 func testTimeRangeOptionsAllHaveValues() {
157 for option in QueryViewModel.TimeRangeOption.allCases {
158 let range = option.dateRange()
159 XCTAssertLessThanOrEqual(range.start, range.end, "Option \(option.rawValue) has invalid range")
163 func testTimeRangePresets() {
165 let fiveMin = QueryViewModel.TimeRangeOption.last5Min.dateRange()
166 let diff = now.timeIntervalSince(fiveMin.start)
167 // Should be roughly 5 minutes (300 seconds) +/- 2 seconds for test execution time
168 XCTAssertTrue(abs(diff - 300) < 2, "Last 5 min diff was \(diff)")
171 // MARK: - Query history
173 func testSetDefaultQuery() {
174 let vm = QueryViewModel()
175 vm.setDefaultQuery(stream: "test_stream")
176 XCTAssertTrue(vm.sqlQuery.contains("test_stream"))
177 XCTAssertTrue(vm.sqlQuery.contains("SELECT"))
180 func testSetDefaultQueryDoesNotOverwrite() {
181 let vm = QueryViewModel()
182 vm.sqlQuery = "SELECT 1"
183 vm.setDefaultQuery(stream: "test_stream")
184 XCTAssertEqual(vm.sqlQuery, "SELECT 1")
187 // MARK: - Query cancellation
190 func testCancelQuerySetsState() {
191 let vm = QueryViewModel()
192 // Simulate loading state
195 XCTAssertFalse(vm.isLoading)
196 XCTAssertEqual(vm.errorMessage, "Query cancelled")
200 func testCancelQueryWhenNotLoading() {
201 let vm = QueryViewModel()
203 // Should still set the message but not crash
204 XCTAssertFalse(vm.isLoading)
205 XCTAssertEqual(vm.errorMessage, "Query cancelled")
208 // MARK: - Default query uses escaped identifiers
210 func testSetDefaultQueryEscapesStreamName() {
211 let vm = QueryViewModel()
212 vm.setDefaultQuery(stream: "my-stream")
213 XCTAssertTrue(vm.sqlQuery.contains("\"my-stream\""))
216 func testSetDefaultQueryEscapesQuotesInStreamName() {
217 let vm = QueryViewModel()
218 vm.setDefaultQuery(stream: "stream\"name")
219 XCTAssertTrue(vm.sqlQuery.contains("\"stream\"\"name\""))
222 // MARK: - Column filter
224 func testAddColumnFilterIncludeStringValue() {
225 let vm = QueryViewModel()
226 vm.sqlQuery = "SELECT * FROM \"logs\" ORDER BY p_timestamp DESC LIMIT 1000"
227 vm.addColumnFilter(column: "level", value: .string("error"), exclude: false)
228 XCTAssertTrue(vm.sqlQuery.contains("WHERE \"level\" = 'error'"))
229 XCTAssertTrue(vm.sqlQuery.contains("ORDER BY"))
232 func testAddColumnFilterExcludeStringValue() {
233 let vm = QueryViewModel()
234 vm.sqlQuery = "SELECT * FROM \"logs\" ORDER BY p_timestamp DESC LIMIT 1000"
235 vm.addColumnFilter(column: "level", value: .string("debug"), exclude: true)
236 XCTAssertTrue(vm.sqlQuery.contains("WHERE \"level\" <> 'debug'"))
237 XCTAssertTrue(vm.sqlQuery.contains("ORDER BY"))
240 func testAddColumnFilterNullValue() {
241 let vm = QueryViewModel()
242 vm.sqlQuery = "SELECT * FROM \"logs\" ORDER BY p_timestamp DESC"
243 vm.addColumnFilter(column: "tag", value: nil, exclude: false)
244 XCTAssertTrue(vm.sqlQuery.contains("WHERE \"tag\" IS NULL"))
247 func testAddColumnFilterExcludeNullValue() {
248 let vm = QueryViewModel()
249 vm.sqlQuery = "SELECT * FROM \"logs\" ORDER BY p_timestamp DESC"
250 vm.addColumnFilter(column: "tag", value: .null, exclude: true)
251 XCTAssertTrue(vm.sqlQuery.contains("WHERE \"tag\" IS NOT NULL"))
254 func testAddColumnFilterAppendsToExistingWhere() {
255 let vm = QueryViewModel()
256 vm.sqlQuery = "SELECT * FROM \"logs\" WHERE \"level\" = 'error' ORDER BY p_timestamp DESC"
257 vm.addColumnFilter(column: "host", value: .string("web-1"), exclude: false)
258 XCTAssertTrue(vm.sqlQuery.contains("AND \"host\" = 'web-1'"))
259 XCTAssertTrue(vm.sqlQuery.contains("WHERE \"level\" = 'error'"))
262 func testAddColumnFilterParenthesizesExistingOr() {
263 // An existing top-level OR must be parenthesized before AND-ing the new
264 // condition so precedence is preserved.
265 let vm = QueryViewModel()
266 vm.sqlQuery = "SELECT * FROM \"logs\" WHERE \"a\" = 1 OR \"b\" = 2"
267 vm.addColumnFilter(column: "host", value: .string("web-1"), exclude: false)
268 XCTAssertTrue(vm.sqlQuery.contains("(\"a\" = 1 OR \"b\" = 2) AND \"host\" = 'web-1'"))
271 func testAddColumnFilterIntValue() {
272 let vm = QueryViewModel()
273 vm.sqlQuery = "SELECT * FROM \"logs\""
274 vm.addColumnFilter(column: "status", value: .int(200), exclude: false)
275 XCTAssertTrue(vm.sqlQuery.contains("WHERE \"status\" = 200"))
278 func testAddColumnFilterNoOpOnEmptyQuery() {
279 let vm = QueryViewModel()
281 vm.addColumnFilter(column: "level", value: .string("info"), exclude: false)
282 XCTAssertEqual(vm.sqlQuery, "")
285 func testAddColumnFilterQueryWithoutOrderBy() {
286 let vm = QueryViewModel()
287 vm.sqlQuery = "SELECT * FROM \"logs\""
288 vm.addColumnFilter(column: "level", value: .string("warn"), exclude: true)
289 XCTAssertTrue(vm.sqlQuery.contains("WHERE \"level\" <> 'warn'"))
292 func testAddColumnFilterEscapesSingleQuotesInValue() {
293 let vm = QueryViewModel()
294 vm.sqlQuery = "SELECT * FROM \"logs\""
295 vm.addColumnFilter(column: "msg", value: .string("it's a test"), exclude: false)
296 XCTAssertTrue(vm.sqlQuery.contains("'it''s a test'"))
299 // MARK: - Column visibility
301 func testVisibleColumnsExcludesHidden() {
302 let vm = QueryViewModel()
303 vm.columns = ["a", "b", "c"]
304 vm.columnOrder = ["a", "b", "c"]
305 vm.hiddenColumns = ["b"]
306 XCTAssertEqual(vm.visibleColumns, ["a", "c"])
309 func testToggleColumnVisibilityHidesColumn() {
310 let vm = QueryViewModel()
311 vm.columns = ["a", "b", "c"]
312 vm.columnOrder = ["a", "b", "c"]
313 vm.toggleColumnVisibility("b")
314 XCTAssertTrue(vm.hiddenColumns.contains("b"))
315 XCTAssertEqual(vm.visibleColumns, ["a", "c"])
318 func testToggleColumnVisibilityShowsColumn() {
319 let vm = QueryViewModel()
320 vm.columns = ["a", "b", "c"]
321 vm.columnOrder = ["a", "b", "c"]
322 vm.hiddenColumns = ["b"]
323 vm.toggleColumnVisibility("b")
324 XCTAssertFalse(vm.hiddenColumns.contains("b"))
325 XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
328 func testCannotHideAllColumns() {
329 let vm = QueryViewModel()
330 vm.columns = ["a", "b"]
331 vm.columnOrder = ["a", "b"]
332 vm.toggleColumnVisibility("a")
333 XCTAssertTrue(vm.hiddenColumns.contains("a"))
334 // Trying to hide the last visible column should be prevented
335 vm.toggleColumnVisibility("b")
336 XCTAssertFalse(vm.hiddenColumns.contains("b"))
337 XCTAssertEqual(vm.visibleColumns, ["b"])
340 func testShowAllColumns() {
341 let vm = QueryViewModel()
342 vm.columns = ["a", "b", "c"]
343 vm.columnOrder = ["a", "b", "c"]
344 vm.hiddenColumns = ["a", "b"]
346 XCTAssertTrue(vm.hiddenColumns.isEmpty)
347 XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
350 // MARK: - Column reordering
352 func testMoveColumnByName() {
353 let vm = QueryViewModel()
354 vm.columns = ["a", "b", "c"]
355 vm.columnOrder = ["a", "b", "c"]
356 vm.moveColumn("c", to: "a")
357 XCTAssertEqual(vm.columnOrder, ["c", "a", "b"])
360 func testMoveColumnRightwardLandsAfterTarget() {
361 // Rightward move (e.g. the "Move Right" context action, which passes the
362 // adjacent column as the target) must land the column *after* the target,
363 // not produce a no-op.
364 let vm = QueryViewModel()
365 vm.columns = ["a", "b", "c"]
366 vm.columnOrder = ["a", "b", "c"]
367 vm.moveColumn("b", to: "c")
368 XCTAssertEqual(vm.columnOrder, ["a", "c", "b"])
371 func testMoveColumnLeftwardLandsBeforeTarget() {
372 let vm = QueryViewModel()
373 vm.columns = ["a", "b", "c"]
374 vm.columnOrder = ["a", "b", "c"]
375 vm.moveColumn("b", to: "a")
376 XCTAssertEqual(vm.columnOrder, ["b", "a", "c"])
379 func testMoveColumnByIndexSet() {
380 let vm = QueryViewModel()
381 vm.columns = ["a", "b", "c"]
382 vm.columnOrder = ["a", "b", "c"]
383 vm.moveColumn(from: IndexSet(integer: 2), to: 0)
384 XCTAssertEqual(vm.columnOrder, ["c", "a", "b"])
387 func testResetColumnConfig() {
388 let vm = QueryViewModel()
389 vm.columns = ["a", "b", "c"]
390 vm.columnOrder = ["c", "b", "a"]
391 vm.hiddenColumns = ["b"]
392 vm.resetColumnConfig()
393 XCTAssertEqual(vm.columnOrder, ["a", "b", "c"])
394 XCTAssertTrue(vm.hiddenColumns.isEmpty)
397 func testVisibleColumnsRespectsOrder() {
398 let vm = QueryViewModel()
399 vm.columns = ["a", "b", "c"]
400 vm.columnOrder = ["c", "a", "b"]
401 vm.hiddenColumns = ["a"]
402 XCTAssertEqual(vm.visibleColumns, ["c", "b"])
405 // MARK: - Column config persistence
407 func testApplyColumnConfigWithNoSavedConfig() {
408 let vm = QueryViewModel()
409 vm.applyColumnConfig(extractedColumns: ["x", "y", "z"], stream: nil)
410 XCTAssertEqual(vm.columns, ["x", "y", "z"])
411 XCTAssertEqual(vm.columnOrder, ["x", "y", "z"])
412 XCTAssertTrue(vm.hiddenColumns.isEmpty)
415 func testApplyColumnConfigMergesNewColumns() {
416 let vm = QueryViewModel()
417 // Save a config for "test_stream"
418 let config = QueryViewModel.ColumnConfiguration(order: ["b", "a"], hidden: ["a"])
419 if let data = try? JSONEncoder().encode(config) {
420 UserDefaults.standard.set(data, forKey: "parseable_column_config_test_stream")
423 // Apply with new columns that include an extra column "c"
424 vm.applyColumnConfig(extractedColumns: ["a", "b", "c"], stream: "test_stream")
425 XCTAssertEqual(vm.columnOrder, ["b", "a", "c"]) // saved order preserved, new "c" appended
426 XCTAssertEqual(vm.hiddenColumns, ["a"]) // saved hidden preserved
429 UserDefaults.standard.removeObject(forKey: "parseable_column_config_test_stream")
432 func testApplyColumnConfigRemovesStaleSavedColumns() {
433 let vm = QueryViewModel()
434 // Save a config with a column that no longer exists
435 let config = QueryViewModel.ColumnConfiguration(order: ["old", "a", "b"], hidden: ["old"])
436 if let data = try? JSONEncoder().encode(config) {
437 UserDefaults.standard.set(data, forKey: "parseable_column_config_test_stream2")
440 vm.applyColumnConfig(extractedColumns: ["a", "b"], stream: "test_stream2")
441 XCTAssertEqual(vm.columnOrder, ["a", "b"]) // "old" removed
442 XCTAssertTrue(vm.hiddenColumns.isEmpty) // "old" was hidden but no longer exists
445 UserDefaults.standard.removeObject(forKey: "parseable_column_config_test_stream2")
448 func testClearResultsResetsColumnState() {
449 let vm = QueryViewModel()
450 vm.columns = ["a", "b"]
451 vm.columnOrder = ["b", "a"]
452 vm.hiddenColumns = ["a"]
454 XCTAssertTrue(vm.columns.isEmpty)
455 XCTAssertTrue(vm.columnOrder.isEmpty)
456 XCTAssertTrue(vm.hiddenColumns.isEmpty)
459 func testCSVExportUsesVisibleColumns() {
460 let vm = QueryViewModel()
462 ["name": .string("Alice"), "age": .int(30), "secret": .string("hidden")],
464 vm.columns = ["name", "age", "secret"]
465 vm.columnOrder = ["name", "age", "secret"]
466 vm.hiddenColumns = ["secret"]
467 let csv = vm.exportAsCSV()
469 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
470 XCTAssertEqual(lines[0], "name,age")
471 XCTAssertFalse(csv.contains("secret"))
472 XCTAssertFalse(csv.contains("hidden"))
475 // MARK: - Empty column detection
477 func testEmptyColumnsDetectsAllNulls() {
478 let vm = QueryViewModel()
479 let records: [LogRecord] = [
480 ["a": .string("val"), "b": .null],
481 ["a": .string("val2"), "b": .null],
483 let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
484 XCTAssertEqual(empty, ["b"])
487 func testEmptyColumnsDetectsMissingKeys() {
488 let vm = QueryViewModel()
489 let records: [LogRecord] = [
490 ["a": .string("val")],
491 ["a": .string("val2")],
493 let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
494 XCTAssertEqual(empty, ["b"])
497 func testEmptyColumnsDetectsEmptyStrings() {
498 let vm = QueryViewModel()
499 let records: [LogRecord] = [
500 ["a": .string("val"), "b": .string("")],
501 ["a": .string("val2"), "b": .string("")],
503 let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
504 XCTAssertEqual(empty, ["b"])
507 func testEmptyColumnsMixedNullAndMissing() {
508 let vm = QueryViewModel()
509 let records: [LogRecord] = [
510 ["a": .string("val"), "b": .null],
511 ["a": .string("val2")],
512 ["a": .string("val3"), "b": .string("")],
514 let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
515 XCTAssertEqual(empty, ["b"])
518 func testEmptyColumnsNotEmptyIfAnyRecordHasValue() {
519 let vm = QueryViewModel()
520 let records: [LogRecord] = [
521 ["a": .string("val"), "b": .null],
522 ["a": .string("val2"), "b": .string("data")],
524 let empty = vm.emptyColumns(in: records, columns: ["a", "b"])
525 XCTAssertTrue(empty.isEmpty)
528 func testEmptyColumnsNoRecords() {
529 let vm = QueryViewModel()
530 let empty = vm.emptyColumns(in: [], columns: ["a", "b"])
531 XCTAssertEqual(empty, ["a", "b"])
534 func testEmptyColumnsNoColumns() {
535 let vm = QueryViewModel()
536 let records: [LogRecord] = [["a": .string("val")]]
537 let empty = vm.emptyColumns(in: records, columns: [])
538 XCTAssertTrue(empty.isEmpty)
541 func testAutoHiddenColumnsExcludedFromVisibleColumns() {
542 let vm = QueryViewModel()
543 vm.columns = ["a", "b", "c"]
544 vm.columnOrder = ["a", "b", "c"]
545 vm.autoHiddenColumns = ["b"]
546 XCTAssertEqual(vm.visibleColumns, ["a", "c"])
549 func testToggleAutoHiddenColumnShowsIt() {
550 let vm = QueryViewModel()
551 vm.columns = ["a", "b", "c"]
552 vm.columnOrder = ["a", "b", "c"]
553 vm.autoHiddenColumns = ["b"]
554 vm.toggleColumnVisibility("b")
555 XCTAssertFalse(vm.autoHiddenColumns.contains("b"))
556 XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
559 func testShowAllColumnsClearsAutoHidden() {
560 let vm = QueryViewModel()
561 vm.columns = ["a", "b", "c"]
562 vm.columnOrder = ["a", "b", "c"]
563 vm.hiddenColumns = ["a"]
564 vm.autoHiddenColumns = ["b"]
566 XCTAssertTrue(vm.hiddenColumns.isEmpty)
567 XCTAssertTrue(vm.autoHiddenColumns.isEmpty)
568 XCTAssertEqual(vm.visibleColumns, ["a", "b", "c"])
571 func testResetColumnConfigClearsAutoHidden() {
572 let vm = QueryViewModel()
573 vm.columns = ["a", "b", "c"]
574 vm.columnOrder = ["c", "b", "a"]
575 vm.autoHiddenColumns = ["b"]
576 vm.resetColumnConfig()
577 XCTAssertEqual(vm.columnOrder, ["a", "b", "c"])
578 XCTAssertTrue(vm.autoHiddenColumns.isEmpty)
581 func testClearResultsClearsAutoHidden() {
582 let vm = QueryViewModel()
583 vm.columns = ["a", "b"]
584 vm.columnOrder = ["a", "b"]
585 vm.autoHiddenColumns = ["b"]
587 XCTAssertTrue(vm.autoHiddenColumns.isEmpty)
590 // MARK: - Pending column config from saved queries
592 func testApplyColumnConfigUsesPendingConfig() {
593 let vm = QueryViewModel()
594 vm.pendingColumnConfig = QueryViewModel.ColumnConfiguration(
595 order: ["c", "a", "b"],
598 vm.applyColumnConfig(extractedColumns: ["a", "b", "c"], stream: "test_stream")
599 XCTAssertEqual(vm.columnOrder, ["c", "a", "b"])
600 XCTAssertEqual(vm.hiddenColumns, ["b"])
601 XCTAssertNil(vm.pendingColumnConfig, "Pending config should be consumed")
604 func testApplyColumnConfigPendingOverridesStreamDefault() {
605 let vm = QueryViewModel()
606 // Save a stream-level config
607 let streamConfig = QueryViewModel.ColumnConfiguration(order: ["a", "b", "c"], hidden: [])
608 if let data = try? JSONEncoder().encode(streamConfig) {
609 UserDefaults.standard.set(data, forKey: "parseable_column_config_pending_test_stream")
612 // Set a different pending config (from saved query)
613 vm.pendingColumnConfig = QueryViewModel.ColumnConfiguration(
614 order: ["c", "b", "a"],
617 vm.applyColumnConfig(extractedColumns: ["a", "b", "c"], stream: "pending_test_stream")
618 XCTAssertEqual(vm.columnOrder, ["c", "b", "a"])
619 XCTAssertEqual(vm.hiddenColumns, ["a"])
622 UserDefaults.standard.removeObject(forKey: "parseable_column_config_pending_test_stream")
625 func testApplyColumnConfigPendingMergesNewColumns() {
626 let vm = QueryViewModel()
627 // Pending config references columns a and b, but results also have c
628 vm.pendingColumnConfig = QueryViewModel.ColumnConfiguration(
632 vm.applyColumnConfig(extractedColumns: ["a", "b", "c"], stream: "test_stream")
633 XCTAssertEqual(vm.columnOrder, ["b", "a", "c"])
636 func testApplyColumnConfigPendingRemovesStaleColumns() {
637 let vm = QueryViewModel()
638 // Pending config references "old" which no longer exists in results
639 vm.pendingColumnConfig = QueryViewModel.ColumnConfiguration(
640 order: ["old", "a", "b"],
643 vm.applyColumnConfig(extractedColumns: ["a", "b"], stream: "test_stream")
644 XCTAssertEqual(vm.columnOrder, ["a", "b"])
645 XCTAssertTrue(vm.hiddenColumns.isEmpty)
648 func testCSVExportExcludesAutoHiddenColumns() {
649 let vm = QueryViewModel()
651 ["name": .string("Alice"), "age": .int(30), "empty_col": .null],
653 vm.columns = ["name", "age", "empty_col"]
654 vm.columnOrder = ["name", "age", "empty_col"]
655 vm.autoHiddenColumns = ["empty_col"]
656 let csv = vm.exportAsCSV()
658 let lines = csv.components(separatedBy: "\n").filter { !$0.isEmpty }
659 XCTAssertEqual(lines[0], "name,age")
660 XCTAssertFalse(csv.contains("empty_col"))
663 func testBoolZeroValuesNotTreatedAsEmpty() {
664 let vm = QueryViewModel()
665 let records: [LogRecord] = [
669 let empty = vm.emptyColumns(in: records, columns: ["a"])
670 XCTAssertTrue(empty.isEmpty)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list