SPB Git

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%
6.7 KB · 189 lines swift
Raw Blame History
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