// // ConsoleSmokeTests.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import XCTest /// XCUITest smoke (CLAUDE.md §9): launch the app, load a sample dataset, /// run a regression, and assert the output table appears in the console. /// /// The sandboxed app cannot read files staged by the test runner (and its /// container may be recreated between runs), so the dataset travels via /// `METRIKA_UITEST_CSV` in the launch environment; the app writes it into /// its own container and `use`s it on startup. /// /// SwiftUI `Text` exposes content through the accessibility **value** /// attribute, so all queries predicate on `value`, not `label`. final class ConsoleSmokeTests: XCTestCase { private func staticText( containing needle: String, in app: XCUIApplication ) -> XCUIElement { app.staticTexts.matching( NSPredicate(format: "value CONTAINS %@", needle) ).firstMatch } func testLoadDatasetAndRegress() throws { continueAfterFailure = false let app = XCUIApplication() app.launchEnvironment["METRIKA_UITEST_CSV"] = """ revenue,price,region,firm_id 120.5,10.2,1,1 98.3,12.1,1,1 143.2,9.8,2,2 110.0,11.5,2,2 155.9,8.9,3,3 101.2,12.8,3,3 133.4,10.1,1,4 88.7,13.5,2,4 149.1,9.2,3,5 115.6,11.0,1,5 """ app.launch() // Startup hook loads the dataset. let loaded = staticText(containing: "10 observations, 4 variables", in: app) XCTAssertTrue(loaded.waitForExistence(timeout: 10), "dataset did not load") let input = app.textFields.firstMatch XCTAssertTrue(input.waitForExistence(timeout: 5), "command field missing") input.click() input.typeText("gen log_rev = ln(revenue)\n") input.typeText("reg log_rev price, robust\n") // Regression output: robust banner and coefficient table. let banner = staticText(containing: "Linear regression (robust)", in: app) XCTAssertTrue(banner.waitForExistence(timeout: 10), "regression output missing") XCTAssertTrue( staticText(containing: "Coefficient", in: app).exists, "coefficient table missing" ) // Sidebar reflects the generated variable. let sidebar = staticText(containing: "log_rev", in: app) XCTAssertTrue(sidebar.waitForExistence(timeout: 5), "sidebar missing log_rev") } func testMetalScatterPath() throws { continueAfterFailure = false let app = XCUIApplication() app.launchEnvironment["METRIKA_METAL_THRESHOLD"] = "3" app.launchEnvironment["METRIKA_UITEST_CSV"] = """ revenue,price 120.5,10.2 98.3,12.1 143.2,9.8 110.0,11.5 155.9,8.9 101.2,12.8 """ app.launch() XCTAssertTrue( staticText(containing: "6 observations", in: app).waitForExistence(timeout: 10), "dataset did not load" ) let input = app.textFields.firstMatch input.click() input.typeText("scatter revenue price\n") // 6 points > threshold 3 → the Metal pane takes over. The pane is // exposed through its accessibility label. let metalPane = app.descendants(matching: .any).matching( NSPredicate(format: "label CONTAINS %@ OR value CONTAINS %@", "Metal scatter", "Metal scatter") ).firstMatch XCTAssertTrue(metalPane.waitForExistence(timeout: 10), "Metal scatter pane missing") } func testDataBrowserAndDoFile() throws { continueAfterFailure = false let app = XCUIApplication() app.launchEnvironment["METRIKA_UITEST_CSV"] = """ revenue,price,region 120.5,10.2,1 98.3,12.1,1 143.2,9.8,2 110.0,11.5,2 155.9,8.9,3 """ app.launch() XCTAssertTrue( staticText(containing: "5 observations", in: app).waitForExistence(timeout: 10), "dataset did not load" ) // Data pane: grid renders, filter compiles to an if-expression. switchPane(to: "Data", in: app) let grid = app.tables.firstMatch XCTAssertTrue(grid.waitForExistence(timeout: 5), "data grid missing") let filter = app.textFields.firstMatch XCTAssertTrue(filter.waitForExistence(timeout: 5), "filter bar missing") filter.click() filter.typeText("revenue > 115 & region != 3\n") XCTAssertTrue( staticText(containing: "2 of 5 observations", in: app) .waitForExistence(timeout: 5), "filter did not narrow rows" ) // Do-file pane: type a script and run it with ⌘R. switchPane(to: "Do-file", in: app) let editor = app.textViews.firstMatch XCTAssertTrue(editor.waitForExistence(timeout: 5), "editor missing") editor.click() editor.typeText("gen high = revenue > 120\ncount if high == 1") app.typeKey("r", modifierFlags: .command) // Output lands in the shared console. switchPane(to: "Console", in: app) XCTAssertTrue( staticText(containing: "do-file (2 lines)", in: app) .waitForExistence(timeout: 10), "do-file output missing from console" ) // Sidebar picked up the do-file's generated variable. XCTAssertTrue( staticText(containing: "high", in: app).waitForExistence(timeout: 5), "sidebar missing variable generated by do-file" ) // Manual pane: overview page and a command entry render. switchPane(to: "Manual", in: app) XCTAssertTrue( staticText(containing: "How Metrika works", in: app) .waitForExistence(timeout: 5), "manual overview missing" ) let regressEntry = app.staticTexts["regress"].firstMatch XCTAssertTrue(regressEntry.waitForExistence(timeout: 5), "regress entry missing") regressEntry.click() XCTAssertTrue( staticText(containing: "Ordinary least squares", in: app) .waitForExistence(timeout: 5), "regress manual page missing" ) } /// The pane switcher is a segmented picker in the toolbar; SwiftUI /// exposes segments as radio buttons (with a button fallback). private func switchPane(to name: String, in app: XCUIApplication) { let radio = app.radioButtons[name] if radio.waitForExistence(timeout: 3) { radio.click() return } app.buttons[name].firstMatch.click() } }