spb/metrika Public
Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.
Swift 92.4%
HTML 3.3%
R 3%
Shell 1.3%
1//2// ConsoleSmokeTests.swift3// Metrika4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import XCTest1112/// XCUITest smoke (CLAUDE.md §9): launch the app, load a sample dataset,13/// run a regression, and assert the output table appears in the console.14///15/// The sandboxed app cannot read files staged by the test runner (and its16/// container may be recreated between runs), so the dataset travels via17/// `METRIKA_UITEST_CSV` in the launch environment; the app writes it into18/// its own container and `use`s it on startup.19///20/// SwiftUI `Text` exposes content through the accessibility **value**21/// attribute, so all queries predicate on `value`, not `label`.22final class ConsoleSmokeTests: XCTestCase {2324 private func staticText(25 containing needle: String, in app: XCUIApplication26 ) -> XCUIElement {27 app.staticTexts.matching(28 NSPredicate(format: "value CONTAINS %@", needle)29 ).firstMatch30 }3132 func testLoadDatasetAndRegress() throws {33 continueAfterFailure = false3435 let app = XCUIApplication()36 app.launchEnvironment["METRIKA_UITEST_CSV"] = """37 revenue,price,region,firm_id38 120.5,10.2,1,139 98.3,12.1,1,140 143.2,9.8,2,241 110.0,11.5,2,242 155.9,8.9,3,343 101.2,12.8,3,344 133.4,10.1,1,445 88.7,13.5,2,446 149.1,9.2,3,547 115.6,11.0,1,548 """49 app.launch()5051 // Startup hook loads the dataset.52 let loaded = staticText(containing: "10 observations, 4 variables", in: app)53 XCTAssertTrue(loaded.waitForExistence(timeout: 10), "dataset did not load")5455 let input = app.textFields.firstMatch56 XCTAssertTrue(input.waitForExistence(timeout: 5), "command field missing")57 input.click()58 input.typeText("gen log_rev = ln(revenue)\n")59 input.typeText("reg log_rev price, robust\n")6061 // Regression output: robust banner and coefficient table.62 let banner = staticText(containing: "Linear regression (robust)", in: app)63 XCTAssertTrue(banner.waitForExistence(timeout: 10), "regression output missing")64 XCTAssertTrue(65 staticText(containing: "Coefficient", in: app).exists,66 "coefficient table missing"67 )6869 // Sidebar reflects the generated variable.70 let sidebar = staticText(containing: "log_rev", in: app)71 XCTAssertTrue(sidebar.waitForExistence(timeout: 5), "sidebar missing log_rev")72 }7374 func testMetalScatterPath() throws {75 continueAfterFailure = false7677 let app = XCUIApplication()78 app.launchEnvironment["METRIKA_METAL_THRESHOLD"] = "3"79 app.launchEnvironment["METRIKA_UITEST_CSV"] = """80 revenue,price81 120.5,10.282 98.3,12.183 143.2,9.884 110.0,11.585 155.9,8.986 101.2,12.887 """88 app.launch()89 XCTAssertTrue(90 staticText(containing: "6 observations", in: app).waitForExistence(timeout: 10),91 "dataset did not load"92 )9394 let input = app.textFields.firstMatch95 input.click()96 input.typeText("scatter revenue price\n")9798 // 6 points > threshold 3 → the Metal pane takes over. The pane is99 // exposed through its accessibility label.100 let metalPane = app.descendants(matching: .any).matching(101 NSPredicate(format: "label CONTAINS %@ OR value CONTAINS %@",102 "Metal scatter", "Metal scatter")103 ).firstMatch104 XCTAssertTrue(metalPane.waitForExistence(timeout: 10), "Metal scatter pane missing")105 }106107 func testDataBrowserAndDoFile() throws {108 continueAfterFailure = false109110 let app = XCUIApplication()111 app.launchEnvironment["METRIKA_UITEST_CSV"] = """112 revenue,price,region113 120.5,10.2,1114 98.3,12.1,1115 143.2,9.8,2116 110.0,11.5,2117 155.9,8.9,3118 """119 app.launch()120 XCTAssertTrue(121 staticText(containing: "5 observations", in: app).waitForExistence(timeout: 10),122 "dataset did not load"123 )124125 // Data pane: grid renders, filter compiles to an if-expression.126 switchPane(to: "Data", in: app)127 let grid = app.tables.firstMatch128 XCTAssertTrue(grid.waitForExistence(timeout: 5), "data grid missing")129130 let filter = app.textFields.firstMatch131 XCTAssertTrue(filter.waitForExistence(timeout: 5), "filter bar missing")132 filter.click()133 filter.typeText("revenue > 115 & region != 3\n")134 XCTAssertTrue(135 staticText(containing: "2 of 5 observations", in: app)136 .waitForExistence(timeout: 5),137 "filter did not narrow rows"138 )139140 // Do-file pane: type a script and run it with ⌘R.141 switchPane(to: "Do-file", in: app)142 let editor = app.textViews.firstMatch143 XCTAssertTrue(editor.waitForExistence(timeout: 5), "editor missing")144 editor.click()145 editor.typeText("gen high = revenue > 120\ncount if high == 1")146 app.typeKey("r", modifierFlags: .command)147148 // Output lands in the shared console.149 switchPane(to: "Console", in: app)150 XCTAssertTrue(151 staticText(containing: "do-file (2 lines)", in: app)152 .waitForExistence(timeout: 10),153 "do-file output missing from console"154 )155 // Sidebar picked up the do-file's generated variable.156 XCTAssertTrue(157 staticText(containing: "high", in: app).waitForExistence(timeout: 5),158 "sidebar missing variable generated by do-file"159 )160161 // Manual pane: overview page and a command entry render.162 switchPane(to: "Manual", in: app)163 XCTAssertTrue(164 staticText(containing: "How Metrika works", in: app)165 .waitForExistence(timeout: 5),166 "manual overview missing"167 )168 let regressEntry = app.staticTexts["regress"].firstMatch169 XCTAssertTrue(regressEntry.waitForExistence(timeout: 5), "regress entry missing")170 regressEntry.click()171 XCTAssertTrue(172 staticText(containing: "Ordinary least squares", in: app)173 .waitForExistence(timeout: 5),174 "regress manual page missing"175 )176 }177178 /// The pane switcher is a segmented picker in the toolbar; SwiftUI179 /// exposes segments as radio buttons (with a button fallback).180 private func switchPane(to name: String, in app: XCUIApplication) {181 let radio = app.radioButtons[name]182 if radio.waitForExistence(timeout: 3) {183 radio.click()184 return185 }186 app.buttons[name].firstMatch.click()187 }188}189