main
Drew Owens 0.9.23 source 30c0ef7 9d ago
//! SOLIDWORKS VISUALIZE (0.9.19): started through its platform shortcut, its
//! three measured dialogs told apart by their WORDS, and none of them answered.

mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};

fn s(v: &Value) -> String {
    serde_json::to_string_pretty(v).unwrap_or_default()
}

const MAIN_CLASS: &str = "HwndWrapper[SLDWORKSVisualize.exe;Main Application;6f1c]";

struct Machine {
    dir: TempDir,
    lnk: String,
}

impl Machine {
    fn new(tag: &str, scenario: Value) -> Machine {
        let dir = TempDir::new(tag);
        std::fs::write(dir.join("visualize.json"), scenario.to_string()).unwrap();
        std::fs::write(dir.join("windows.json"), "[]").unwrap();
        let lnk = format!("{}/SOLIDWORKS Visualize.lnk", dir.path());
        std::fs::write(&lnk, "a platform shortcut").unwrap();
        Machine { dir, lnk }
    }

    fn boot(&self, ab: Option<&common::AbStub>) -> Bridge {
        let mut env: Vec<(&str, &str)> = vec![
            ("SWX_LAUNCH_FAKE_DIR", Box::leak(self.dir.path().into_boxed_str())),
            ("SWX_VISUALIZE_SHORTCUT", Box::leak(self.lnk.clone().into_boxed_str())),
        ];
        if let Some(a) = ab {
            env.push(("ADOM_DIRECT_API_URL", Box::leak(a.url.clone().into_boxed_str())));
        }
        Bridge::start_in(TempDir::new("visualize-scratch"), &env)
    }

    fn invoked_with(&self) -> Option<String> {
        std::fs::read_to_string(self.dir.join("visualize-invoked.json"))
            .ok()
            .and_then(|t| serde_json::from_str::<Value>(&t).ok())
            .and_then(|v| v["shortcut"].as_str().map(String::from))
    }
}

fn start(b: &Bridge) -> Value {
    b.call("swx_start", json!({"app": "visualize", "reason": "Drew wants a Visualize render", "waitSeconds": 3}))
}

#[test]
fn visualize_is_started_through_its_platform_shortcut_and_comes_up_ready() {
    let m = Machine::new("viz-ready", json!({"pid": 56488, "startsOnInvoke": true,
        "windows": [{"hwnd": 9100, "className": MAIN_CLASS, "title": "SOLIDWORKS Visualize"}]}));
    let b = m.boot(None);
    let st0 = b.call("swx_status", json!({}));
    check!(b, "before: Visualize is not running", st0["visualize"]["running"] == false, s(&st0["visualize"]));
    let r = start(&b);
    check!(
        b,
        "launched through the PLATFORM SHORTCUT (never the executable), and ready",
        r["success"] == true && r["launchedNow"] == true && r["stage"] == "ready"
            && m.invoked_with().map(|p| p.ends_with("SOLIDWORKS Visualize.lnk")).unwrap_or(false),
        format!("{} / invoked with {:?}", s(&r), m.invoked_with())
    );
    let st = b.call("swx_status", json!({}));
    check!(
        b,
        "swx_status.visualize: running, pid, the HwndWrapper main window, nothing waiting",
        st["visualize"]["running"] == true && st["visualize"]["pid"] == 56488 && st["visualize"]["hwnd"] == 9100
            && st["visualize"]["waitingOn"].is_null(),
        s(&st["visualize"])
    );
    let again = start(&b);
    check!(b, "a second start launches nothing", again["alreadyRunning"] == true && again["launchedNow"] == false, s(&again));
}

#[test]
fn the_first_run_content_folder_dialog_is_reported_and_never_answered() {
    let ab = common::AbStub::start(json!([]));
    let m = Machine::new("viz-content", json!({"pid": 56488, "startsOnInvoke": true,
        "windows": [{"hwnd": 9101, "className": MAIN_CLASS, "title": "SOLIDWORKS Visualize",
                     "text": ["Please choose a location for your 'SOLIDWORKS Visualize Content' folder. This folder will be created to provide a place for this software to store data.",
                              "C:\\Users\\drew\\Documents"],
                     "buttons": ["...", "Continue"]}]}));
    let b = m.boot(Some(&ab));
    let r = start(&b);
    check!(
        b,
        "waitingOn userVisualizeContentFolder, with the dialog's text and the prefilled path",
        r["stage"] == "waitingOn" && r["waitingOn"] == "userVisualizeContentFolder"
            && r["visualize"]["dialog"]["prefilledPath"] == "C:\\Users\\drew\\Documents"
            && r["visualize"]["dialog"]["text"].as_str().unwrap_or("").contains("Visualize Content"),
        s(&r)
    );
    let st = b.call("swx_status", json!({}));
    check!(b, "swx_status says the same", st["visualize"]["waitingOn"] == "userVisualizeContentFolder", s(&st["visualize"]));
    // MEASURED 2026-10-05: on first run the dialog IS the main window — the
    // block said hwnd:null beside dialog.hwnd. The top-level hwnd is reported
    // whenever the process has its HwndWrapper window, dialog or not.
    check!(
        b,
        "the top-level hwnd is the main window even while it shows the dialog (same hwnd)",
        st["visualize"]["hwnd"] == 9101 && st["visualize"]["dialog"]["hwnd"] == 9101 && st["visualize"]["ready"] == false,
        s(&st["visualize"])
    );
    check!(
        b,
        "NOTHING was answered: no click, no text set, no keystroke",
        ab.calls_to("desktop_ui_click").is_empty() && ab.calls_to("desktop_ui_set").is_empty()
            && ab.calls_to("desktop_click").is_empty() && ab.calls_to("desktop_type").is_empty()
            && ab.calls_to("desktop_press_key").is_empty(),
        format!("{:?}", ab.calls())
    );
}

#[test]
fn a_direct_start_notice_is_refused_with_the_fix_and_never_answered() {
    let m = Machine::new("viz-direct", json!({"pid": 56488, "runningBefore": true,
        "windows": [{"hwnd": 9102, "className": "#32770", "title": "SOLIDWORKS Visualize",
                     "text": ["SOLIDWORKS Visualize must be opened from the 3DEXPERIENCE Platform or from the desktop shortcut created from the Platform. Do you want to go to the 3DEXPERIENCE Platform?"],
                     "buttons": ["Yes", "No"]}]}));
    let b = m.boot(None);
    let r = start(&b);
    check!(
        b,
        "visualize_direct_start_refused, naming the fix (the platform shortcut); nothing invoked",
        r["errorCode"] == "visualize_direct_start_refused"
            && r["_hint"].as_str().unwrap_or("").contains("platform shortcut")
            && m.invoked_with().is_none(),
        s(&r)
    );
}

#[test]
fn a_visualize_login_points_at_the_sign_in_path() {
    let m = Machine::new("viz-login", json!({"pid": 56488, "startsOnInvoke": true,
        "windows": [{"hwnd": 4262148, "className": "WindowsForms10.Window.8.app.0.141b42a_r6_ad1",
                     "title": "Login - 3DEXPERIENCE ID - Dassault Systemes"}]}));
    let b = m.boot(None);
    let r = start(&b);
    check!(
        b,
        "waitingOn userSignIn, pointing at swx_sign_in with window:\"visualize\"",
        r["waitingOn"] == "userSignIn" && r["visualize"]["dialog"]["how"].as_str().unwrap_or("").contains("window:\"visualize\""),
        s(&r)
    );
}

#[test]
fn no_shortcut_means_no_start_and_a_reason_is_required() {
    let dir = TempDir::new("viz-none");
    std::fs::write(dir.join("visualize.json"), json!({"pid": 56488, "startsOnInvoke": true}).to_string()).unwrap();
    let b = Bridge::start_in(TempDir::new("viz-none-scratch"), &[("SWX_LAUNCH_FAKE_DIR", &dir.path())]);
    let r = b.call("swx_start", json!({"app": "visualize", "reason": "x"}));
    check!(
        b,
        "no platform shortcut: visualize_shortcut_missing — the executable is never started instead",
        r["errorCode"] == "visualize_shortcut_missing" && !dir.join("visualize-invoked.json").exists(),
        s(&r)
    );
    let r = b.call("swx_start", json!({"app": "visualize"}));
    check!(b, "a reason is required", r["errorCode"] == "reason_required", s(&r));
    let r = b.call("swx_start", json!({"app": "photoshop", "reason": "x"}));
    check!(b, "an unknown app is refused", r["errorCode"] == "bad_app", s(&r));
}

/// MEASURED 2026-10-12: after a lapsed platform session, invoking the
/// Visualize shortcut started SWXDesktopUpdate.exe and its 3DEXPERIENCE login
/// (title "3DEXPERIENCE"); Visualize never started; swx_start said
/// stage:"launching" and swx_status said waitingOn:null.
#[test]
fn a_start_parked_behind_the_platform_sign_in_says_so_instead_of_launching() {
    let ab = common::AbStub::start(json!([]));
    ab.set_response("desktop_ui_tree", json!({"success": true, "nodeCount": 14, "root": {"role": "Window", "name": "3DEXPERIENCE", "automationId": "SWXDesktopUpdateForm"}}));
    ab.when("desktop_find_control", json!({"automationId": "username"}),
        json!({"success": true, "data": {"found": true, "control": {"role": "Edit", "name": "Email or username", "automationId": "username", "settable": true,
               "rect": {"x": 1286, "y": 824, "w": 400, "h": 40, "centerX": 1486, "centerY": 844}}}}));
    ab.set_response("desktop_find_control", json!({"success": false, "error": "no control matched"}));
    let m = Machine::new("viz-parked", json!({"pid": 56488, "startsOnInvoke": false, "windows": []}));
    std::fs::write(m.dir.join("preexisting.json"), json!([{"pid": 61000, "image": "SWXDesktopUpdate.exe", "ageSeconds": 1}]).to_string()).unwrap();
    std::fs::write(m.dir.join("sign-in.json"), json!({"hwnd": 857684, "ownerPid": 61000,
        "className": "WindowsForms10.Window.8.app.0.30495d1_r6_ad1", "title": "3DEXPERIENCE"}).to_string()).unwrap();
    let b = m.boot(Some(&ab));
    let started = std::time::Instant::now();
    let r = b.call("swx_start", json!({"app": "visualize", "reason": "Drew asked for Visualize", "waitSeconds": 20}));
    check!(
        b,
        "swx_start: stage waitingOn, waitingOn userSignIn, the updater's window named — not \"launching\", and no 20 s wait",
        r["stage"] == "waitingOn" && r["waitingOn"] == "userSignIn" && r["signInWindow"]["hwnd"] == 857684
            && r["visualize"]["parkedBehind"].is_string() && started.elapsed() < std::time::Duration::from_secs(15),
        s(&r)
    );
    let st = b.call("swx_status", json!({}));
    check!(
        b,
        "swx_status: visualize.waitingOn userSignIn and the top-level waitingOn too",
        st["visualize"]["waitingOn"] == "userSignIn" && st["visualize"]["signInWindow"]["host"] == "desktopUpdate"
            && st["waitingOn"] == "userSignIn" && st["signInWindow"]["host"] == "desktopUpdate",
        s(&st)
    );
}

/// Any 3DEXPERIENCE login on the machine (here the launcher's, found by its
/// class) is surfaced by swx_start {app:"visualize"} instead of "launching".
#[test]
fn a_start_with_any_login_up_is_not_reported_as_launching() {
    let m = Machine::new("viz-parked-launcher", json!({"pid": 56488, "startsOnInvoke": false, "windows": []}));
    std::fs::write(m.dir.join("preexisting.json"), json!([{"pid": 70002, "image": "SWXDesktopLauncher.exe", "ageSeconds": 5}]).to_string()).unwrap();
    std::fs::write(m.dir.join("sign-in.json"), json!({"hwnd": 1002, "ownerPid": 70002,
        "className": "DSYSysLoginPanelWebView2Class", "title": "Login | 3DEXPERIENCE ID | Dassault Systèmes"}).to_string()).unwrap();
    let b = m.boot(None);
    let r = b.call("swx_start", json!({"app": "visualize", "reason": "Drew asked for Visualize", "waitSeconds": 1}));
    check!(
        b,
        "stage waitingOn, waitingOn userSignIn, the login named",
        r["stage"] == "waitingOn" && r["waitingOn"] == "userSignIn" && r["signInWindow"]["hwnd"] == 1002,
        s(&r)
    );
}