From bc6863e57c3fda732ccbd1aaf4b19dcf771b9edc Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Thu, 17 Apr 2025 22:20:24 +1000 Subject: [PATCH 01/17] first test framework implementation --- .gitignore | 5 + test_framework/CMTestingFramework.ahk | 342 ++++++++++++++++++++++++++ test_framework/HowToUse.md | 189 ++++++++++++++ test_framework/SampleTestCase.ahk | 74 ++++++ test_framework/tests/StartGame.ahk | 51 ++++ 5 files changed, 661 insertions(+) create mode 100644 test_framework/CMTestingFramework.ahk create mode 100644 test_framework/HowToUse.md create mode 100644 test_framework/SampleTestCase.ahk create mode 100644 test_framework/tests/StartGame.ahk diff --git a/.gitignore b/.gitignore index 385ac1a4b2..e7c47fe2d6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ ChapterMaster.resource_order tmp/ stitch.config.json + + +test_framework/tests/logs +test_framework/tests/results +test_framework/tests/screenshots \ No newline at end of file diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk new file mode 100644 index 0000000000..d0da5c6110 --- /dev/null +++ b/test_framework/CMTestingFramework.ahk @@ -0,0 +1,342 @@ +; CM Testing Framework (AutoHotkey v2) +; This framework provides the foundation for automated testing of the CM application + +#Requires AutoHotkey v2.0 +#SingleInstance Force + +class CMTestingFramework { + ; Configuration + + __New() { + this.appPath := "D:\Documents\ChapterMaster\ChapterMaster.exe" + this.screenshotDir := A_ScriptDir "\screenshots\" + this.logDir := A_ScriptDir "\logs\" + this.resultDir := A_ScriptDir "\results\" + this.mouseSpeed := 2 ; 1-10, with 10 being slowest (more human-like) + this.errorLogPath := A_AppData "..\Local\ChapterMaster\Logs\last_messages.log" + + ; State tracking + this.currentTest := "" + this.testSteps := [] + this.testResult := "" + this.testStartTime := 0 + this.testEndTime := 0 + ; Ensure directories exist + DirCreate(this.screenshotDir) + DirCreate(this.logDir) + DirCreate(this.resultDir) + + ; Initialize error handling + OnError(ObjBindMethod(this, "ErrorHandler")) + } + + ; Start CM application + LaunchApp() { + try { + Run(this.appPath) + WinWait("ahk_exe " . this.appPath) + this.LogStep("Application launched successfully") + return true + } catch as e { + this.LogError("Failed to launch application: " . e.Message) + return false + } + } + + ; Start a new test + StartTest(testName) { + this.currentTest := testName + this.testSteps := [] + this.testResult := "" + this.testStartTime := A_Now + + this.LogStep("Starting test: " . testName) + return this + } + + ; End current test and save results + EndTest() { + this.testEndTime := A_Now + duration := DateDiff(this.testEndTime, this.testStartTime, "Seconds") + + this.LogStep("Test completed in " . duration . " seconds") + + ; Create test result report + result := "# Test Report: " . this.currentTest . "`n" + result .= "* Date: " . FormatTime(A_Now, "yyyy-MM-dd") . "`n" + result .= "* Time: " . FormatTime(A_Now, "HH:mm:ss") . "`n" + result .= "* Duration: " . duration . " seconds`n`n" + result .= "## Test Steps:`n" + + for step in this.testSteps { + result .= "* " . step . "`n" + } + + ; Save test result to file + fileName := this.resultDir . FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_" . this.currentTest . ".md" + try { + f := FileOpen(fileName, "w").Write(result) + f.Close() + this.testResult := result + return true + } catch as e { + this.LogError("Failed to save test results: " . e.Message) + return false + } + + } + + ; Log a test step + LogStep(description) { + timestamp := FormatTime(A_Now, "HH:mm:ss") + step := timestamp . " - " . description + this.testSteps.Push(step) + FileAppend(step . "`n", this.logDir . "test_log.txt") + return this + } + + ; Log an error + LogError(errorMsg) { + timestamp := FormatTime(A_Now, "HH:mm:ss") + errorStep := timestamp . " - ERROR: " . errorMsg + this.testSteps.Push(errorStep) + FileAppend(errorStep . "`n", this.logDir . "error_log.txt") + return this + } + + ; Mouse Simulation Functions + + ; Move mouse in a human-like manner + MoveMouse(x, y) { + MouseMove(x, y, this.mouseSpeed) + this.LogStep("Mouse moved to coordinates: " . x . ", " . y) + return this + } + + ; Click at current position or specified coordinates + Click(x := "", y := "", button := "left") { + if (x != "" && y != "") { + this.MoveMouse(x, y) + } + + ; GameMaker behaves a lil funky with the instant clicks of AHK and sometimes they dont register + Click(button, , , 1 ,'D') + this.Wait(10) + Click(button, , , 1, 'U') + this.LogStep("Mouse " . button . "-clicked at current position") + + if (!this.CheckForCrash()) + return this + + } + + ; Double-click at current position or specified coordinates + DoubleClick(x := "", y := "", button := "left") { + if (x != "" && y != "") { + this.MoveMouse(x, y) + } + + Click(button " Double") + this.LogStep("Mouse double-" . button . "-clicked at current position") + + if (!this.CheckForCrash()) + return this + } + + ; Drag mouse from one position to another + DragMouse(x1, y1, x2, y2, button := "left") { + this.MoveMouse(x1, y1) + MouseClickDrag(button, x1, y1, x2, y2, this.mouseSpeed) + this.LogStep("Mouse dragged from " . x1 . ", " . y1 . " to " . x2 . ", " . y2) + + if (!this.CheckForCrash()) + return this + } + + ; Keyboard Simulation Functions + + ; Send text (typing) + SendText(text) { + Send(text) + this.LogStep("Text sent: " . text) + + if (!this.CheckForCrash()) + return this + } + + ; Send keyboard combo (like Ctrl+S) + SendCombo(keys) { + Send(keys) + this.LogStep("Key combination sent: " . keys) + + if (!this.CheckForCrash()) + return this + } + + ; Press and hold a key + KeyDown(key) { + Send("{" . key . " down}") + this.LogStep("Key down: " . key) + if (!this.CheckForCrash()) + return this + } + + ; Release a key + KeyUp(key) { + Send("{" . key . " up}") + this.LogStep("Key up: " . key) + if (!this.CheckForCrash()) + return this + } + + ; Wait for specified milliseconds + Wait(ms) { + Sleep(ms) + this.LogStep("Waited for " . ms . " ms") + if (!this.CheckForCrash()) + return this + } + + ; Screenshot Functions + + ; Take a screenshot of the current window + TakeScreenshot(description := "") { + if (description == "") + description := "screenshot" + + ; Create filename with timestamp + filename := FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_" . this.currentTest . "_" . description . ".png" + path := this.screenshotDir . filename + + try { + ; Capture active window + WinGetPos(&x, &y, &w, &h, "A") + screenshot := Gui() + screenshot.Add("Picture", "w" . w . " h" . h, path) + WinCapture(x, y, w, h, path) + + this.LogStep("Screenshot saved: " . filename) + return path + } catch as e { + this.LogError("Failed to take screenshot: " . e.Message) + return "" + } + } + + ; Error Handling Functions + + ; Check if application is still running + IsAppRunning() { + return WinExist("ahk_exe " . this.appPath) + } + + ; Check for application crash + CheckForCrash() { + if (!this.IsAppRunning()) { + this.LogError("Application crashed during " . this.currentTest) + this.TakeScreenshot("crash") + this.ReadErrorLogs() + this.EndTest() + return true + } + return false + } + + ; Read error logs after crash + ReadErrorLogs() { + try { + ; Find the most recent error log + errorLogFiles := [] + loop files, this.errorLogPath . "\*.log" { + errorLogFiles.Push({ path: A_LoopFileFullPath, time: A_LoopFileTimeModified }) + } + + ; Sort by modification time (newest first) + errorLogFiles := Sort(errorLogFiles, (a, b) => b.time - a.time) + + if (errorLogFiles.Length > 0) { + latestLog := errorLogFiles[1].path + logContent := FileRead(latestLog) + + ; Extract relevant information + this.LogStep("Error log found: " . latestLog) + this.LogError("Error log content: " . SubStr(logContent, 1, 500) . (StrLen(logContent) > 500 ? "..." : + "")) + + ; Save error log with test results + FileCopy(latestLog, this.resultDir . FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_error.log") + } else { + this.LogError("No error logs found in " . this.errorLogPath) + } + } catch as e { + this.LogError("Failed to read error logs: " . e.Message) + } + } + + ; General error handler + ErrorHandler(exception, mode) { + this.LogError("Framework error: " . exception.Message) + this.TakeScreenshot("framework_error") + return true ; Continue running + } + + ; Restart the application + RestartApp() { + if (this.IsAppRunning()) { + WinClose("ahk_exe " . this.appPath) + WinWaitClose("ahk_exe " . this.appPath, , 5) + } + + return this.LaunchApp() + } + + ; Helper function to find image on screen + FindImage(imagePath, variation := 50) { + try { + ImageSearch(&foundX, &foundY, 0, 0, A_ScreenWidth, A_ScreenHeight, "*" . variation . " " . imagePath) + if (foundX && foundY) { + this.LogStep("Image found at: " . foundX . ", " . foundY) + return { x: foundX, y: foundY, found: true } + } else { + this.LogStep("Image not found: " . imagePath) + return { found: false } + } + } catch as e { + this.LogError("Error searching for image: " . e.Message) + return { found: false } + } + } + + ; Click on an image if found + ClickOnImage(imagePath, variation := 50) { + result := this.FindImage(imagePath, variation) + if (result.found) { + this.Click(result.x, result.y) + return true + } + return false + } +} + +; Helper function for capturing screenshots +WinCapture(x, y, w, h, filename) { + ; Create bitmap + hdc := DllCall("GetDC", "Ptr", 0) + hdcMem := DllCall("CreateCompatibleDC", "Ptr", hdc) + hBitmap := DllCall("CreateCompatibleBitmap", "Ptr", hdc, "Int", w, "Int", h) + DllCall("SelectObject", "Ptr", hdcMem, "Ptr", hBitmap) + + ; Copy screen to bitmap + DllCall("BitBlt", "Ptr", hdcMem, "Int", 0, "Int", 0, "Int", w, "Int", h, "Ptr", hdc, "Int", x, "Int", y, "UInt", + 0x00CC0020) ; SRCCOPY + + ; Save bitmap to file + DllCall("gdiplus\GdipCreateBitmapFromHBITMAP", "Ptr", hBitmap, "Ptr", 0, "Ptr*", &pBitmap := 0) + DllCall("gdiplus\GdipSaveImageToFile", "Ptr", pBitmap, "Str", filename, "Ptr", 0, "Ptr", 0) + + ; Cleanup + DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmap) + DllCall("DeleteObject", "Ptr", hBitmap) + DllCall("DeleteDC", "Ptr", hdcMem) + DllCall("ReleaseDC", "Ptr", 0, "Ptr", hdc) +} diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md new file mode 100644 index 0000000000..7c37b0a0ad --- /dev/null +++ b/test_framework/HowToUse.md @@ -0,0 +1,189 @@ +# Creating Test Cases for CM Using AutoHotkey Testing Framework + +This document provides guidance for CM developers on how to create automated test cases using our AutoHotkey v2 testing framework. + +## Getting Started + +1. **Install Prerequisites** + - Install AutoHotkey v2 from [https://www.autohotkey.com/](https://www.autohotkey.com/) + - Ensure you have access to the main CMTestingFramework.ahk file + +2. **File Structure** + - Place your test case scripts in the `tests` folder of the `test_framework` directory in the repo + - Name your test scripts descriptively (e.g., `ObliteratedCustomStart.ahk`, `MenuChecks.ahk`) + +3. **Game Settings** + - The test runner assumes you are running in **Windowed 720p** in order for pixels to line up. + - Go to your %LocalAppData%\ChapterMaster folder and open `saves.ini` and look for the [Settings] section + - If it says "fullscreen", run ChapterMaster and turn off Fullscreen in the options + - Close ChapterMaster and open `saves.ini` again. You should see 4 numbers separated by `|` pipes. Edit the last 2 to be `1280|720`. The first 2 are just the x,y coordinates of the top left corner of the window on your monitor. + +4. **Running Tests individually** + - Open the repo, go to `test_framework/tests` and open the desired test + - Easy Method: + - Ensure you have installed the VSCode extension [AHK++ (AutoHotkey Plus Plus)](https://marketplace.visualstudio.com/items/?itemName=mark-wiemer.vscode-autohotkey-plus-plus) + - Open the test case file in VSCode (not the framework) + - Right-click anywhere in the file, select `AHK++ > Run AHK Script` + - Using this method keeps you in the editor while you're working on your test case + - AHK native method + - In the file explorer, right-click your test case .ahk file and select "Run Script" (should be first option) + - Better for running tests if you're not editing them + +## Creating a Basic Test Case + +Each test script follows this structure: + +```autohotkey +; Test Name: [Test Name] +; Description: [Brief description of what this test does] +; Author: [Your Name] +; Date: [Creation/Last Modified Date] + +#Requires AutoHotkey v2.0 +#Include ..\CMTestingFramework.ahk + +TestMyFeature() { + ; Initialize framework + framework := CMTestingFramework() + + ; Start test + framework.StartTest("MyFeatureName") + + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + ; [Your test steps here] + + ; End test and save results + framework.EndTest() +} + +; Run the test +TestMyFeature() +``` + +## Key Framework Methods + +Here are the core methods to use in your test cases: + +### Setup and Cleanup +- `framework.StartTest("TestName")` - Initialize a test with a name +- `framework.LaunchApp()` - Launch the CM application +- `framework.EndTest()` - Complete the test and save results + +### Mouse Interaction +- `framework.Click(x, y)` - Click at specific coordinates +- `framework.DoubleClick(x, y)` - Double-click at coordinates +- `framework.MoveMouse(x, y)` - Move mouse to coordinates +- `framework.DragMouse(x1, y1, x2, y2)` - Click and drag from one point to another + +### Keyboard Interaction +- `framework.SendText("text")` - Type text +- `framework.SendCombo("{Ctrl down}{s}{Ctrl up}")` - Send key combinations +- `framework.KeyDown("key")` and `framework.KeyUp("key")` - Press and release keys + +### Wait and Timing +- `framework.Wait(milliseconds)` - Pause execution for specified time + +### Documentation +- `framework.TakeScreenshot("description")` - Capture the current screen state +- `framework.LogStep("description")` - Record a test step with description + +### Error Handling +- `framework.CheckForCrash()` - Detect if application crashed +- `framework.RestartApp()` - Restart the application after a crash + +## Finding UI Element Coordinates + +To interact with CM's UI elements, you'll need their screen coordinates: + +1. Use AutoHotkey's built-in Window Spy tool (right-click the AutoHotkey icon in the system tray and select "Window Spy") +2. Hover your mouse over the UI element you want to interact with +3. Note the "Mouse Position" coordinates shown in Window Spy. Use the `client` value which should say `default` next to it. + + +## Example Test Case + +Here's a complete example test case for Opening CM, starting a new game as Dark Angels and landing on the main game screen before finishing: + +```autohotkey +#Requires AutoHotkey v2.0 +#Include ..\CMTestingFramework.ahk + +TestStartGame() { + ; Initialize testing framework + framework := CMTestingFramework() + + ; Start test + framework.StartTest("StartGame") + + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + ; Wait for application to fully load + framework.Wait(7000) + .TakeScreenshot("Main_Menu") + + framework.Click(693, 520) ; Click on "New Game" in the main menu + .Wait(8000) + .TakeScreenshot("Creation") + + framework.Click(404, 223) ; Click Dark Angels + .Wait(3000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .TakeScreenshot("Game_Started") + + ; End test and save results + framework.EndTest() + + MsgBox("Test completed. Results saved to: " . framework.resultDir) +} + +; Run the test +TestStartGame() +``` + +## Best Practices + +1. **Start Simple**: Begin with basic workflows before attempting complex scenarios + +2. **Add Delays**: Include sufficient wait times between actions (use `framework.Wait()`) + +3. **Document with Screenshots**: Take screenshots at key points in your test + +4. **Handle Errors**: Always check for and handle possible application crashes + +5. **Organize by Workflow**: Create separate test files for different functional areas + +6. **Comment Your Code**: Include clear comments explaining the purpose of each step + +7. **Use Consistent Naming**: Name your tests and files with clear, descriptive names + +8. **Test One Thing**: Each test should focus on a single feature or workflow + +## Troubleshooting + +- **Coordinate Issues**: If clicks aren't hitting the right spots, verify coordinates in Window Spy +- **Timing Problems**: Increase wait times if actions seem to execute before the app is ready +- **Application Not Found**: Check the application path in CMTestingFramework.ahk + +## Getting Help + +If you encounter issues creating test cases, contact the testing team for assistance. diff --git a/test_framework/SampleTestCase.ahk b/test_framework/SampleTestCase.ahk new file mode 100644 index 0000000000..0c88c78ed3 --- /dev/null +++ b/test_framework/SampleTestCase.ahk @@ -0,0 +1,74 @@ +; CM Application Test Case: Create New Customer Record +; This test case demonstrates how to use the CMTestingFramework + +#Requires AutoHotkey v2.0 +#Include CMTestingFramework.ahk + +TestCreateNewCustomer() { + ; Initialize testing framework + framework := CMTestingFramework() + framework.appPath := "notepad.exe" + + ; Start test + framework.StartTest("CreateNewCustomer") + + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + ; Wait for application to fully load + framework.Wait(3000) + + ; Navigate to customer module + framework.Click(45, 120) ; Click on "Customers" in the main menu + .Wait(1000) + + ; Click on "New Customer" button + framework.Click(150, 80) + .Wait(1500) + .TakeScreenshot("new_customer_form") + + ; Fill out customer form + framework.Click(200, 150) ; Click on "First Name" field + .Wait(300) + .SendText("John") + .Wait(200) + .SendCombo("{Tab}") ; Move to "Last Name" field + .Wait(200) + .SendText("Smith") + .Wait(200) + .SendCombo("{Tab}") ; Move to "Email" field + .Wait(200) + .SendText("john.smith@example.com") + .Wait(200) + .SendCombo("{Tab}") ; Move to "Phone" field + .Wait(200) + .SendText("555-123-4567") + .Wait(200) + .TakeScreenshot("customer_form_filled") + + ; Save the new customer record + framework.Click(300, 400) ; Click "Save" button + .Wait(2000) + + ; Check for confirmation message or crash + if (framework.CheckForCrash()) { + framework.LogError("Application crashed during customer creation") + framework.RestartApp() + framework.EndTest() + return + } + + ; Verify the new customer appears in the list + framework.TakeScreenshot("customer_list_after_save") + + ; End test and save results + framework.EndTest() + + MsgBox("Test completed. Results saved to: " . framework.config.resultDir) +} + +; Run the test +TestCreateNewCustomer() diff --git a/test_framework/tests/StartGame.ahk b/test_framework/tests/StartGame.ahk new file mode 100644 index 0000000000..45ef98ccce --- /dev/null +++ b/test_framework/tests/StartGame.ahk @@ -0,0 +1,51 @@ +; CM Application Test Case: Create New Customer Record +; This test case demonstrates how to use the CMTestingFramework + +#Requires AutoHotkey v2.0 +#Include ..\CMTestingFramework.ahk + +TestStartGame() { + ; Initialize testing framework + framework := CMTestingFramework() + + ; Start test + framework.StartTest("StartGame") + + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + ; Wait for application to fully load + framework.Wait(7000) + .TakeScreenshot("Main_Menu") + + framework.Click(693, 520) ; Click on "New Game" in the main menu + .Wait(8000) + .TakeScreenshot("Creation") + + framework.Click(404, 223) ; Click Dark Angels + .Wait(3000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .Click(835, 751) ; Next button + .Wait(2000) + .TakeScreenshot("Game_Started") + + ; End test and save results + framework.EndTest() + + MsgBox("Test completed. Results saved to: " . framework.resultDir) +} + +; Run the test +TestStartGame() From 3c4c3c478f9537bda1bf775673824b9592ca97fa Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sat, 19 Apr 2025 14:32:53 +1000 Subject: [PATCH 02/17] more framework improvements --- test_framework/CMTestingFramework.ahk | 182 +++++++++++++++++++- test_framework/HowToUse.md | 132 +++++++++++--- test_framework/tests/Boilerplate.ahk | 29 ++++ test_framework/tests/DiscoverUIElements.ahk | 30 ++++ test_framework/tests/StartGame.ahk | 39 +++-- test_framework/tests/ui_elements.txt | 60 +++++++ 6 files changed, 427 insertions(+), 45 deletions(-) create mode 100644 test_framework/tests/Boilerplate.ahk create mode 100644 test_framework/tests/DiscoverUIElements.ahk create mode 100644 test_framework/tests/ui_elements.txt diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index d0da5c6110..6f6e3e4645 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -26,6 +26,9 @@ class CMTestingFramework { DirCreate(this.logDir) DirCreate(this.resultDir) + ; UI Coordinate helpers + this.UIMap := CMUIMap() + ; Initialize error handling OnError(ObjBindMethod(this, "ErrorHandler")) } @@ -117,11 +120,12 @@ class CMTestingFramework { Click(x := "", y := "", button := "left") { if (x != "" && y != "") { this.MoveMouse(x, y) + Sleep(30) } ; GameMaker behaves a lil funky with the instant clicks of AHK and sometimes they dont register - Click(button, , , 1 ,'D') - this.Wait(10) + Click(button, , , 1, 'D') + Sleep(10) Click(button, , , 1, 'U') this.LogStep("Mouse " . button . "-clicked at current position") @@ -161,7 +165,7 @@ class CMTestingFramework { this.LogStep("Text sent: " . text) if (!this.CheckForCrash()) - return this + return this } ; Send keyboard combo (like Ctrl+S) @@ -169,7 +173,7 @@ class CMTestingFramework { Send(keys) this.LogStep("Key combination sent: " . keys) - if (!this.CheckForCrash()) + if (!this.CheckForCrash()) return this } @@ -197,9 +201,16 @@ class CMTestingFramework { return this } + ; Wait for specified seconds + WaitSeconds(secs) { + ms := secs * 1000 + return this.Wait(ms) + } + ; Screenshot Functions ; Take a screenshot of the current window + ; !!! NOT IMPLEMENTED TakeScreenshot(description := "") { if (description == "") description := "screenshot" @@ -316,6 +327,70 @@ class CMTestingFramework { } return false } + + ; Click on an element based on its name in CMUIMap + ; framework.ClickElement("MainMenu.NewGame") + ClickElement(elementPath) { + this.LogStep("Getting element " . elementPath) + element := this.UIMap.GetElement(elementPath) + if (element) { + this.Click(element.x, element.y) + this.LogStep("Clicked element: " . elementPath) + return this + } else { + this.LogError("Element not found: " . elementPath) + return this + } + } + + MoveToElement(elementPath) { + element := this.UIMap.GetElement(elementPath) + if (element) { + this.MoveMouse(element.x, element.y) + this.LogStep("Moved to element: " . elementPath) + return this + } else { + this.LogError("Element not found: " . elementPath) + return this + } + } + + ; Element Discovery Tool + DiscoverUIElements() { + ; Create a GUI + discoveryGui := Gui(, "UI Element Discovery Tool") + discoveryGui.Add("Text", , "Press F7 to capture mouse position") + discoveryGui.Add("Text", "vCoordinates w200 h20", "X: 0, Y: 0") + discoveryGui.Add("Text", , "Section Name:") + discoveryGui.Add("Edit", "vSectionName w150") + discoveryGui.Add("Text", , "Element Name:") + discoveryGui.Add("Edit", "vElementName w150") + discoveryGui.Add("Button", "Default", "Save").OnEvent("Click", SavePosition) + discoveryGui.Show() + + ; Set up hotkey + Hotkey("F7", CapturePosition) + + CapturePosition(*) { + MouseGetPos(&x, &y) + discoveryGui["Coordinates"].Value := "{x: " . x . ", y: " . y . "}" + } + + SavePosition(*) { + ; MouseGetPos(&x, &y) + sectionName := discoveryGui["SectionName"].Value + elementName := discoveryGui["ElementName"].Value + + if (sectionName && elementName) { + ; Append to UI map file + FileAppend("this." . sectionName . "." . elementName . " := " . discoveryGui["Coordinates"].Value . "`n", + A_ScriptDir . "\ui_elements.txt") + MsgBox("Saved " . sectionName . "." . elementName . " at " . discoveryGui["Coordinates"].Value) + } else { + MsgBox("Please enter both section and element names") + } + } + } } ; Helper function for capturing screenshots @@ -340,3 +415,102 @@ WinCapture(x, y, w, h, filename) { DllCall("DeleteDC", "Ptr", hdcMem) DllCall("ReleaseDC", "Ptr", 0, "Ptr", hdc) } + +; This class stores xy coordinates for common game elements and buttons +; All coordinates assume a game size of 1280x720 +class CMUIMap { + __Init(){ + this.MainMenu := {} + this.Creation := {} + this.GameScreen := {} + this.InGameMenu := {} + this.ChapterManagement := {} + this.Apothecarium := {} + this.Armamentarium := {} + this.Fleet := {} + this.Diplomacy := {} + + + } + + __New() { + this.MainMenu.NewGame := { x: 629, y: 422 } + this.MainMenu.LoadGame := { x: -820, y: 187 } + this.MainMenu.LoadGame := { x: 625, y: 452 } + this.MainMenu.Options := { x: 634, y: 487 } + this.MainMenu.Exit := { x: 634, y: 522 } + this.Creation.DarkAngels := { x: 371, y: 155 } + this.Creation.BlackTemplars := { x: 388, y: 248 } + this.Creation.CustomSlot1 := { x: 373, y: 422 } + this.Creation.CreateCustom := { x: 625, y: 519 } + this.Creation.CreateRandom := { x: 679, y: 521 } + this.Creation.Deathwatch := { x: 539, y: 517 } + this.Creation.AngryMarines := { x: 373, y: 524 } + this.Creation.NextArrow := { x: 760, y: 631 } + this.Creation.BackArrow := { x: 523, y: 639 } + this.Creation.SkipArrow := { x: 822, y: 635 } + this.Creation.AdvSlot1 := { x: 369, y: 479 } + this.Creation.DisadvSlot1 := { x: 680, y: 481 } + this.Creation.Slide1Homeworld := { x: 430, y: 203 } + this.Creation.Slide1FleetBased := { x: 630, y: 202 } + this.Creation.Slide1Penitent := { x: 778, y: 201 } + this.Creation.Slide6Leader := { x: 388, y: 581 } + this.Creation.Slide6Champion := { x: 584, y: 583 } + this.Creation.Slide6Psyker := { x: 775, y: 582 } + this.GameScreen.ChapterManagement := { x: 95, y: 686 } + this.GameScreen.ChapterSettings := { x: 197, y: 687 } + this.GameScreen.Apothecarium := { x: 342, y: 688 } + this.GameScreen.Reclusium := { x: 414, y: 690 } + this.GameScreen.Librarium := { x: 518, y: 690 } + this.GameScreen.Armamentarium := { x: 610, y: 693 } + this.GameScreen.Recruitment := { x: 705, y: 689 } + this.GameScreen.Fleet := { x: 792, y: 692 } + this.GameScreen.Diplomacy := { x: 952, y: 691 } + this.GameScreen.EventLog := { x: 1075, y: 683 } + this.GameScreen.EndTurn := { x: 1190, y: 690 } + this.GameScreen.Help := { x: 1139, y: 27 } + this.GameScreen.Menu := { x: 1226, y: 25 } + this.InGameMenu.Save := { x: 745, y: 231 } + this.InGameMenu.Load := { x: 753, y: 298 } + this.InGameMenu.Options := { x: 750, y: 363 } + this.InGameMenu.Exit := { x: 749, y: 422 } + this.InGameMenu.Return := { x: 746, y: 551 } + this.ChapterManagement.Headquarters := { x: 632, y: 173 } + this.ChapterManagement.Company1 := { x: 79, y: 419 } + this.ChapterManagement.Company10 := { x: 1189, y: 362 } + this.ChapterManagement.Company5 := { x: 575, y: 378 } + this.ChapterManagement.SquadView := { x: 862, y: 137 } + this.ChapterManagement.ShowProfile := { x: 979, y: 134 } + this.ChapterManagement.SelectAll := { x: 870, y: 502 } + this.Apothecarium.AddTestSlave := { x: 370, y: 642 } + this.Armamentarium.EnterForge := { x: 495, y: 322 } + this.Armamentarium.Equipment := { x: 816, y: 67 } + this.Armamentarium.Armour := { x: 898, y: 64 } + this.Armamentarium.Vehicles := { x: 1002, y: 72 } + this.Armamentarium.Ships := { x: 1218, y: 70 } + this.Fleet.ShipSlot1 := { x: 882, y: 93 } + this.Diplomacy.ImperiumAudience := { x: 244, y: 275 } + this.Diplomacy.MechanicusAudience := { x: 235, y: 385 } + this.Diplomacy.MeetChaosEmmisary := { x: 691, y: 189 } + + + ; Add more sections and elements as needed + } + + ; Get element coordinates by path (e.g., "MainMenu.NewGame") + GetElement(path) { + parts := StrSplit(path, ".") + if (parts.Length != 2) + return false + + section := parts[1] + element := parts[2] + + if (this.HasOwnProp(section)) { + sectionObj := this.%section% + if (sectionObj.HasOwnProp(element)) + return sectionObj.%element% + } + return false + } +} diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index 7c37b0a0ad..db053ba1ee 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -79,6 +79,8 @@ Here are the core methods to use in your test cases: - `framework.DoubleClick(x, y)` - Double-click at coordinates - `framework.MoveMouse(x, y)` - Move mouse to coordinates - `framework.DragMouse(x1, y1, x2, y2)` - Click and drag from one point to another +- `framework.ClickElement("Section.ElementName")` - Click on a named UI element +- `framework.MoveToElement("Section.ElementName")` - Move mouse to a named UI element ### Keyboard Interaction - `framework.SendText("text")` - Type text @@ -96,18 +98,99 @@ Here are the core methods to use in your test cases: - `framework.CheckForCrash()` - Detect if application crashed - `framework.RestartApp()` - Restart the application after a crash -## Finding UI Element Coordinates +## Using Named UI Elements -To interact with CM's UI elements, you'll need their screen coordinates: +Instead of using raw coordinates, you can reference UI elements by name: + +### Using ClickElement and MoveToElement + +These methods allow you to interact with UI elements by name rather than remembering coordinates: + +```autohotkey +; Click on a button using its name +framework.ClickElement("MainMenu.NewGame") + +; Move to an element without clicking +framework.MoveToElement("GameScreen.SaveGame") +``` + +The format is always `"Section.ElementName"`, where: +- `Section` is a logical grouping of UI elements (e.g., MainMenu, GameScreen, Dialog) +- `ElementName` is the specific element within that section + +### Benefits of Using Named Elements + +- **Readability**: Tests are more descriptive and easier to understand +- **Maintainability**: If UI coordinates change, you only need to update the UI Map, not each test +- **Consistency**: Ensures the same element is always referenced the same way + +### Example with Named Elements + +```autohotkey +framework.StartTest("StartGameWithNamedElements") + .LaunchApp() + .Wait(7000) + .ClickElement("MainMenu.NewGame") + .Wait(8000) + .ClickElement("Creation.DarkAngels") + .Wait(3000) + .ClickElement("Creation.SkipArrow") + .Wait(2000) + .EndTest() +``` + +## Discovering UI Element Coordinates + +### Using the DiscoverUIElements Tool + +The framework includes a tool to help identify and record UI element coordinates: + +1. **Launch the Discovery Tool** + ```autohotkey + DiscoverUIElements() + ``` + +2. **Using the Tool** + - Position your mouse over a UI element in the CM application + - Press F7 to capture the current mouse coordinates + - Enter a Section Name (e.g., "MainMenu") + - Enter an Element Name (e.g., "NewGameButton") + - Click "Save" + +3. **Finding the Saved Coordinates** + - The tool saves coordinates to `ui_elements.txt` in your script directory + - Each entry is formatted for easy pasting into the `CMUIMap` class + +4. **Typical Workflow** + ```autohotkey + ; Example script to discover UI elements + #Requires AutoHotkey v2.0 + #Include ..\CMTestingFramework.ahk + + ; Launch CM + framework := CMTestingFramework() + framework.LaunchApp() + + ; Start the discovery tool + DiscoverUIElements() + ``` + +5. **After Capturing Elements** + - Copy the contents of `ui_elements.txt` + - Add them to the `CMUIMap` class in the appropriate sections + - Use your newly mapped elements in tests with `ClickElement()` and `MoveToElement()` + +### Manual Coordinate Finding + +You can still use AutoHotkey's Window Spy as described below: 1. Use AutoHotkey's built-in Window Spy tool (right-click the AutoHotkey icon in the system tray and select "Window Spy") 2. Hover your mouse over the UI element you want to interact with 3. Note the "Mouse Position" coordinates shown in Window Spy. Use the `client` value which should say `default` next to it. +## Example Test Case Using Named Elements -## Example Test Case - -Here's a complete example test case for Opening CM, starting a new game as Dark Angels and landing on the main game screen before finishing: +Here's a complete example test case using named UI elements: ```autohotkey #Requires AutoHotkey v2.0 @@ -130,23 +213,23 @@ TestStartGame() { framework.Wait(7000) .TakeScreenshot("Main_Menu") - framework.Click(693, 520) ; Click on "New Game" in the main menu + framework.ClickElement("MainMenu.NewGame") ; Click on "New Game" using named element .Wait(8000) .TakeScreenshot("Creation") - framework.Click(404, 223) ; Click Dark Angels + framework.ClickElement("Creation.DarkAngels") ; Click Dark Angels by name .Wait(3000) - .Click(835, 751) ; Next button + .ClickElement("Creation.NextArrow") ; Click Next button .Wait(2000) - .Click(835, 751) ; Next button + .ClickElement("Creation.NextArrow") ; Click Next button again .Wait(2000) - .Click(835, 751) ; Next button + .ClickElement("Creation.NextArrow") ; Click Next button again .Wait(2000) - .Click(835, 751) ; Next button + .ClickElement("Creation.NextArrow") ; Click Next button again .Wait(2000) - .Click(835, 751) ; Next button + .ClickElement("Creation.NextArrow") ; Click Next button again .Wait(2000) - .Click(835, 751) ; Next button + .ClickElement("Creation.NextArrow") ; Click Next button again .Wait(2000) .TakeScreenshot("Game_Started") @@ -162,27 +245,32 @@ TestStartGame() ## Best Practices -1. **Start Simple**: Begin with basic workflows before attempting complex scenarios +1. **Use Named Elements**: Prefer `ClickElement()` over raw coordinates when possible + +2. **Map New Elements**: When encountering a new UI element, add it to the UI Map + +3. **Start Simple**: Begin with basic workflows before attempting complex scenarios -2. **Add Delays**: Include sufficient wait times between actions (use `framework.Wait()`) +4. **Add Delays**: Include sufficient wait times between actions (use `framework.Wait()`) -3. **Document with Screenshots**: Take screenshots at key points in your test +5. **Document with Screenshots**: Take screenshots at key points in your test -4. **Handle Errors**: Always check for and handle possible application crashes +6. **Handle Errors**: Always check for and handle possible application crashes -5. **Organize by Workflow**: Create separate test files for different functional areas +7. **Organize by Workflow**: Create separate test files for different functional areas -6. **Comment Your Code**: Include clear comments explaining the purpose of each step +8. **Comment Your Code**: Include clear comments explaining the purpose of each step -7. **Use Consistent Naming**: Name your tests and files with clear, descriptive names +9. **Use Consistent Naming**: Name your tests and files with clear, descriptive names -8. **Test One Thing**: Each test should focus on a single feature or workflow +10. **Test One Thing**: Each test should focus on a single feature or workflow ## Troubleshooting -- **Coordinate Issues**: If clicks aren't hitting the right spots, verify coordinates in Window Spy +- **Coordinate Issues**: If clicks aren't hitting the right spots, verify coordinates in Window Spy or use the DiscoverUIElements tool - **Timing Problems**: Increase wait times if actions seem to execute before the app is ready - **Application Not Found**: Check the application path in CMTestingFramework.ahk +- **Element Not Found Error**: Verify the element path is correct (e.g., "MainMenu.NewGame") ## Getting Help diff --git a/test_framework/tests/Boilerplate.ahk b/test_framework/tests/Boilerplate.ahk new file mode 100644 index 0000000000..33ee5649e2 --- /dev/null +++ b/test_framework/tests/Boilerplate.ahk @@ -0,0 +1,29 @@ +; CM Application Test Case: Create New Customer Record +; Copy/paste this layout for easy test creation from scratch + +#Requires AutoHotkey v2.0 +#Include ..\CMTestingFramework.ahk + +TestBoilerplate() { + ; Initialize testing framework + framework := CMTestingFramework() + + ; Start test + framework.StartTest("StartGame") + + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + ; Write steps here + + ; End test and save results + framework.EndTest() + + MsgBox("Test completed. Results saved to: " . framework.resultDir) +} + +; Run the test +TestBoilerplate() diff --git a/test_framework/tests/DiscoverUIElements.ahk b/test_framework/tests/DiscoverUIElements.ahk new file mode 100644 index 0000000000..52adc74536 --- /dev/null +++ b/test_framework/tests/DiscoverUIElements.ahk @@ -0,0 +1,30 @@ +; CM Application Test Case: Create New Customer Record +; This test case demonstrates how to use the CMTestingFramework + +#Requires AutoHotkey v2.0 +#Include ..\CMTestingFramework.ahk + +DiscoverUIElementsRunner() { + ; Initialize testing framework + framework := CMTestingFramework() + + ; Start test + ; framework.StartTest("StartGame") + + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + ; Write steps here + framework.DiscoverUIElements() + + ; End test and save results + ; framework.EndTest() + + ; MsgBox("Test completed. Results saved to: " . framework.resultDir) +} + +; Run the test +DiscoverUIElementsRunner() diff --git a/test_framework/tests/StartGame.ahk b/test_framework/tests/StartGame.ahk index 45ef98ccce..0f17e82106 100644 --- a/test_framework/tests/StartGame.ahk +++ b/test_framework/tests/StartGame.ahk @@ -19,27 +19,28 @@ TestStartGame() { ; Wait for application to fully load framework.Wait(7000) - .TakeScreenshot("Main_Menu") - - framework.Click(693, 520) ; Click on "New Game" in the main menu - .Wait(8000) - .TakeScreenshot("Creation") - - framework.Click(404, 223) ; Click Dark Angels + .ClickElement("MainMenu.NewGame") ; Click on "New Game" in the main menu + .Wait(8500) + .ClickElement("Creation.DarkAngels") ; Click Dark Angels .Wait(3000) - .Click(835, 751) ; Next button - .Wait(2000) - .Click(835, 751) ; Next button - .Wait(2000) - .Click(835, 751) ; Next button - .Wait(2000) - .Click(835, 751) ; Next button - .Wait(2000) - .Click(835, 751) ; Next button - .Wait(2000) - .Click(835, 751) ; Next button + .ClickElement("Creation.SkipArrow") + .Wait(5000) + .Click(500, 400) + .Click(500, 400) + .Click(500, 400) + .Click(500, 400) + .Click(500, 400) ;skip intro sprawl + .Wait(1000) + .ClickElement("GameScreen.ChapterManagement") + .Wait(1000) + .ClickElement("GameScreen.ChapterSettings") + .Wait(1000) + .ClickElement("GameScreen.Fleet") .Wait(2000) - .TakeScreenshot("Game_Started") + .ClickElement("GameScreen.Fleet") + .Wait(1000) + .ClickElement("GameScreen.EndTurn") + .Wait(2000) ; waiting at the end checks for crashing ; End test and save results framework.EndTest() diff --git a/test_framework/tests/ui_elements.txt b/test_framework/tests/ui_elements.txt new file mode 100644 index 0000000000..5a9ab8a740 --- /dev/null +++ b/test_framework/tests/ui_elements.txt @@ -0,0 +1,60 @@ +this.MainMenu.NewGame := {x: 41, y: 168} +this.MainMenu.NewGame := X: 675, Y: 419 +this.MainMenu.NewGame := {x: 629, y: 422} +this.MainMenu.LoadGame := {x: -820, y: 187} +this.MainMenu.LoadGame := {x: 625, y: 452} +this.MainMenu.Options := {x: 634, y: 487} +this.MainMenu.Exit := {x: 634, y: 522} +this.Creation.DarkAngels := {x: 371, y: 155} +this.Creation.BlackTemplars := {x: 388, y: 248} +this.Creation.CustomSlot1 := {x: 373, y: 422} +this.Creation.CreateCustom := {x: 625, y: 519} +this.Creation.CreateRandom := {x: 679, y: 521} +this.Creation.Deathwatch := {x: 539, y: 517} +this.Creation.AngryMarines := {x: 373, y: 524} +this.Creation.NextArrow := {x: 760, y: 631} +this.Creation.BackArrow := {x: 523, y: 639} +this.Creation.SkipArrow := {x: 822, y: 635} +this.Creation.AdvSlot1 := {x: 369, y: 479} +this.Creation.DisadvSlot1 := {x: 680, y: 481} +this.Creation.Slide1Homeworld := {x: 430, y: 203} +this.Creation.Slide1FleetBased := {x: 630, y: 202} +this.Creation.Slide1Penitent := {x: 778, y: 201} +this.Creation.Slide6Leader := {x: 388, y: 581} +this.Creation.Slide6Champion := {x: 584, y: 583} +this.Creation.Slide6Psyker := {x: 775, y: 582} +this.GameScreen.ChapterManagement := {x: 95, y: 686} +this.GameScreen.ChapterSettings := {x: 197, y: 687} +this.GameScreen.Apothecarium := {x: 342, y: 688} +this.GameScreen.Reclusium := {x: 414, y: 690} +this.GameScreen.Librarium := {x: 518, y: 690} +this.GameScreen.Armamentarium := {x: 610, y: 693} +this.GameScreen.Recruitment := {x: 705, y: 689} +this.GameScreen.Fleet := {x: 792, y: 692} +this.GameScreen.Diplomacy := {x: 952, y: 691} +this.GameScreen.EventLog := {x: 1075, y: 683} +this.GameScreen.EndTurn := {x: 1190, y: 690} +this.GameScreen.Help := {x: 1139, y: 27} +this.GameScreen.Menu := {x: 1226, y: 25} +this.InGameMenu.Save := {x: 745, y: 231} +this.InGameMenu.Load := {x: 753, y: 298} +this.InGameMenu.Options := {x: 750, y: 363} +this.InGameMenu.Exit := {x: 749, y: 422} +this.InGameMenu.Return := {x: 746, y: 551} +this.ChapterManagement.Headquarters := {x: 632, y: 173} +this.ChapterManagement.Company1 := {x: 79, y: 419} +this.ChapterManagement.Company10 := {x: 1189, y: 362} +this.ChapterManagement.Company5 := {x: 575, y: 378} +this.ChapterManagement.SquadView := {x: 862, y: 137} +this.ChapterManagement.ShowProfile := {x: 979, y: 134} +this.ChapterManagement.SelectAll := {x: 870, y: 502} +this.Apothecarium.AddTestSlave := {x: 370, y: 642} +this.Armamentarium.EnterForge := {x: 495, y: 322} +this.Armamentarium.Equipment := {x: 816, y: 67} +this.Armamentarium.Armour := {x: 898, y: 64} +this.Armamentarium.Vehicles := {x: 1002, y: 72} +this.Armamentarium.Ships := {x: 1218, y: 70} +this.Fleet.ShipSlot1 := {x: 882, y: 93} +this.Diplomacy.ImperiumAudience := {x: 244, y: 275} +this.Diplomacy.MechanicusAudience := {x: 235, y: 385} +this.Diplomacy.MeetChaosEmmisary := {x: 691, y: 189} From 7a7bb335154b445b24e8bb7c3bf07fd620e36b2d Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sat, 19 Apr 2025 23:39:21 +1000 Subject: [PATCH 03/17] working test suite runner --- test_framework/CMTestingFramework.ahk | 79 +++++++++++++------ .../{tests => }/DiscoverUIElements.ahk | 10 +-- test_framework/RunAllTests.ahk | 70 ++++++++++++++++ test_framework/SampleTestCase.ahk | 74 ----------------- test_framework/tests/Boilerplate.ahk | 3 - test_framework/tests/TestObliteratedStart.ahk | 51 ++++++++++++ .../{StartGame.ahk => TestStartGame.ahk} | 3 - test_framework/{tests => }/ui_elements.txt | 9 +++ 8 files changed, 188 insertions(+), 111 deletions(-) rename test_framework/{tests => }/DiscoverUIElements.ahk (65%) create mode 100644 test_framework/RunAllTests.ahk delete mode 100644 test_framework/SampleTestCase.ahk create mode 100644 test_framework/tests/TestObliteratedStart.ahk rename test_framework/tests/{StartGame.ahk => TestStartGame.ahk} (90%) rename test_framework/{tests => }/ui_elements.txt (87%) diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index 6f6e3e4645..d628d1926f 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -12,8 +12,8 @@ class CMTestingFramework { this.screenshotDir := A_ScriptDir "\screenshots\" this.logDir := A_ScriptDir "\logs\" this.resultDir := A_ScriptDir "\results\" - this.mouseSpeed := 2 ; 1-10, with 10 being slowest (more human-like) - this.errorLogPath := A_AppData "..\Local\ChapterMaster\Logs\last_messages.log" + this.mouseSpeed := 8 ; 1-10, with 10 being slowest (more human-like) + this.errorLogPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\Logs" ; State tracking this.currentTest := "" @@ -46,6 +46,16 @@ class CMTestingFramework { } } + CloseApp() { + try { + WinClose("ahk_exe " . this.appPath) + return true + } catch as e { + this.LogError("Failed to properly close application: " . e.Message) + return false + } + } + ; Start a new test StartTest(testName) { this.currentTest := testName @@ -87,6 +97,7 @@ class CMTestingFramework { return false } + this.CloseApp() } ; Log a test step @@ -120,13 +131,13 @@ class CMTestingFramework { Click(x := "", y := "", button := "left") { if (x != "" && y != "") { this.MoveMouse(x, y) - Sleep(30) + Sleep(100) } ; GameMaker behaves a lil funky with the instant clicks of AHK and sometimes they dont register - Click(button, , , 1, 'D') - Sleep(10) - Click(button, , , 1, 'U') + Click(button, 'D') + Sleep(30) + Click(button, 'U') this.LogStep("Mouse " . button . "-clicked at current position") if (!this.CheckForCrash()) @@ -245,10 +256,22 @@ class CMTestingFramework { CheckForCrash() { if (!this.IsAppRunning()) { this.LogError("Application crashed during " . this.currentTest) - this.TakeScreenshot("crash") this.ReadErrorLogs() this.EndTest() return true + } else if(this.CheckForCrashDialog()) { + this.LogError("Crash dialog detected via window class") + this.ReadErrorLogs() + this.EndTest() + return true + } + return false + } + + CheckForCrashDialog() { + ; Check if any dialog from the app exists with specific window class + if (WinExist("ahk_exe " . this.appPath . " ahk_class #32770")) { ; #32770 is common for dialogs + return true } return false } @@ -257,21 +280,23 @@ class CMTestingFramework { ReadErrorLogs() { try { ; Find the most recent error log - errorLogFiles := [] - loop files, this.errorLogPath . "\*.log" { - errorLogFiles.Push({ path: A_LoopFileFullPath, time: A_LoopFileTimeModified }) + ; errorLogFiles := [] + + latestFile := {path: "", time: 0} + loop files, this.errorLogPath . "\*_error.log" { + if(latestFile.time < A_LoopFileTimeModified){ + latestFile.path := A_LoopFileFullPath + latestFile.time := A_LoopFileTimeModified + } } - ; Sort by modification time (newest first) - errorLogFiles := Sort(errorLogFiles, (a, b) => b.time - a.time) - - if (errorLogFiles.Length > 0) { - latestLog := errorLogFiles[1].path + if (latestFile.time > 0) { + latestLog := latestFile.path logContent := FileRead(latestLog) ; Extract relevant information this.LogStep("Error log found: " . latestLog) - this.LogError("Error log content: " . SubStr(logContent, 1, 500) . (StrLen(logContent) > 500 ? "..." : + this.LogError("Error log content: " . SubStr(logContent, 1, 1000) . (StrLen(logContent) > 1000 ? "..." : "")) ; Save error log with test results @@ -433,6 +458,7 @@ class CMUIMap { } + ; To populate new coords, use DiscoverUIElements.ahk, see HowToUse.md for instructions __New() { this.MainMenu.NewGame := { x: 629, y: 422 } this.MainMenu.LoadGame := { x: -820, y: 187 } @@ -451,12 +477,21 @@ class CMUIMap { this.Creation.SkipArrow := { x: 822, y: 635 } this.Creation.AdvSlot1 := { x: 369, y: 479 } this.Creation.DisadvSlot1 := { x: 680, y: 481 } - this.Creation.Slide1Homeworld := { x: 430, y: 203 } - this.Creation.Slide1FleetBased := { x: 630, y: 202 } - this.Creation.Slide1Penitent := { x: 778, y: 201 } - this.Creation.Slide6Leader := { x: 388, y: 581 } - this.Creation.Slide6Champion := { x: 584, y: 583 } - this.Creation.Slide6Psyker := { x: 775, y: 582 } + this.Creation.Homeworld := { x: 430, y: 203 } + this.Creation.FleetBased := { x: 630, y: 202 } + this.Creation.Penitent := { x: 778, y: 201 } + this.Creation.Leader := { x: 388, y: 581 } + this.Creation.Champion := { x: 584, y: 583 } + this.Creation.Psyker := { x: 775, y: 582 } + this.Creation.StrengthUp := { x: 388, y: 273 } + this.Creation.StrengthDown := { x: 363, y: 276 } + this.Creation.CooperationUp := { x: 391, y: 321 } + this.Creation.CooperationDown := { x: 359, y: 321 } + this.Creation.PurityUp := { x: 390, y: 367 } + this.Creation.PurityDown := { x: 356, y: 364 } + this.Creation.StabilityUp := { x: 388, y: 405 } + this.Creation.StabilityDown := { x: 367, y: 407 } + this.Creation.Obliterated := { x: 384, y: 416 } this.GameScreen.ChapterManagement := { x: 95, y: 686 } this.GameScreen.ChapterSettings := { x: 197, y: 687 } this.GameScreen.Apothecarium := { x: 342, y: 688 } diff --git a/test_framework/tests/DiscoverUIElements.ahk b/test_framework/DiscoverUIElements.ahk similarity index 65% rename from test_framework/tests/DiscoverUIElements.ahk rename to test_framework/DiscoverUIElements.ahk index 52adc74536..25667ffdbe 100644 --- a/test_framework/tests/DiscoverUIElements.ahk +++ b/test_framework/DiscoverUIElements.ahk @@ -2,28 +2,20 @@ ; This test case demonstrates how to use the CMTestingFramework #Requires AutoHotkey v2.0 -#Include ..\CMTestingFramework.ahk +#Include CMTestingFramework.ahk DiscoverUIElementsRunner() { ; Initialize testing framework framework := CMTestingFramework() - ; Start test - ; framework.StartTest("StartGame") - ; Launch CM application if (!framework.LaunchApp()) { MsgBox("Failed to launch application") return } - ; Write steps here framework.DiscoverUIElements() - ; End test and save results - ; framework.EndTest() - - ; MsgBox("Test completed. Results saved to: " . framework.resultDir) } ; Run the test diff --git a/test_framework/RunAllTests.ahk b/test_framework/RunAllTests.ahk new file mode 100644 index 0000000000..57fdfa710a --- /dev/null +++ b/test_framework/RunAllTests.ahk @@ -0,0 +1,70 @@ +#Requires AutoHotkey v2.0 +#SingleInstance Force + +RunAllTests() { + ; Get the script's directory + scriptDir := A_ScriptDir + + ; Define the tests directory + testsDir := scriptDir . "\tests" + + ; Check if the tests directory exists + if (!DirExist(testsDir)) { + MsgBox("Tests directory not found: " . testsDir) + return + } + + ; Array to hold test results + testResults := [] + + ; Find all .ahk files that begin with "Test" + testFiles := [] + loop files, testsDir . "\Test*.ahk" { + testFiles.Push(A_LoopFileFullPath) + } + + ; Check if any test files were found + if (testFiles.Length = 0) { + MsgBox("No test files found starting with 'Test' in: " . testsDir) + return + } + + ; Display progress + MsgBox("Found " . testFiles.Length . " test files. Running tests in sequence...") + + ; Create a log file for results + logFile := scriptDir . "\tests\results\TestSuiteResults_" . FormatTime(A_Now, "yyyyMMdd_HHmmss") . ".log" + FileAppend("CM Test Suite Run - " . FormatTime(A_Now, "yyyy-MM-dd HH:mm:ss") . "`n`n", logFile) + + ; Run each test file + for testFile in testFiles { + testName := GetFileNameWithoutExtension(testFile) + + FileAppend("Running test: " . testName . "...`n", logFile) + + ; Run the test and wait for it to complete + RunWait('"' . A_AhkPath . '" "' . testFile . '"') + + ; Test completed + FileAppend("Completed test: " . testName . "`n`n", logFile) + testResults.Push({ name: testName, status: "Completed" }) + } + + ; Output summary + summary := "Test Run Summary:`n" + for testResult in testResults { + summary .= testResult.name . ": " . testResult.status . "`n" + } + + FileAppend("`n" . summary, logFile) + ; MsgBox("All tests completed. Results saved to: " . logFile) +} + +; Helper function to get file name without extension +GetFileNameWithoutExtension(filePath) { + SplitPath(filePath, &fileName, ,) + return StrReplace(fileName, ".ahk", "") +} + +; Run the function +RunAllTests() \ No newline at end of file diff --git a/test_framework/SampleTestCase.ahk b/test_framework/SampleTestCase.ahk deleted file mode 100644 index 0c88c78ed3..0000000000 --- a/test_framework/SampleTestCase.ahk +++ /dev/null @@ -1,74 +0,0 @@ -; CM Application Test Case: Create New Customer Record -; This test case demonstrates how to use the CMTestingFramework - -#Requires AutoHotkey v2.0 -#Include CMTestingFramework.ahk - -TestCreateNewCustomer() { - ; Initialize testing framework - framework := CMTestingFramework() - framework.appPath := "notepad.exe" - - ; Start test - framework.StartTest("CreateNewCustomer") - - ; Launch CM application - if (!framework.LaunchApp()) { - MsgBox("Failed to launch application") - return - } - - ; Wait for application to fully load - framework.Wait(3000) - - ; Navigate to customer module - framework.Click(45, 120) ; Click on "Customers" in the main menu - .Wait(1000) - - ; Click on "New Customer" button - framework.Click(150, 80) - .Wait(1500) - .TakeScreenshot("new_customer_form") - - ; Fill out customer form - framework.Click(200, 150) ; Click on "First Name" field - .Wait(300) - .SendText("John") - .Wait(200) - .SendCombo("{Tab}") ; Move to "Last Name" field - .Wait(200) - .SendText("Smith") - .Wait(200) - .SendCombo("{Tab}") ; Move to "Email" field - .Wait(200) - .SendText("john.smith@example.com") - .Wait(200) - .SendCombo("{Tab}") ; Move to "Phone" field - .Wait(200) - .SendText("555-123-4567") - .Wait(200) - .TakeScreenshot("customer_form_filled") - - ; Save the new customer record - framework.Click(300, 400) ; Click "Save" button - .Wait(2000) - - ; Check for confirmation message or crash - if (framework.CheckForCrash()) { - framework.LogError("Application crashed during customer creation") - framework.RestartApp() - framework.EndTest() - return - } - - ; Verify the new customer appears in the list - framework.TakeScreenshot("customer_list_after_save") - - ; End test and save results - framework.EndTest() - - MsgBox("Test completed. Results saved to: " . framework.config.resultDir) -} - -; Run the test -TestCreateNewCustomer() diff --git a/test_framework/tests/Boilerplate.ahk b/test_framework/tests/Boilerplate.ahk index 33ee5649e2..1643cbc7b7 100644 --- a/test_framework/tests/Boilerplate.ahk +++ b/test_framework/tests/Boilerplate.ahk @@ -1,4 +1,3 @@ -; CM Application Test Case: Create New Customer Record ; Copy/paste this layout for easy test creation from scratch #Requires AutoHotkey v2.0 @@ -21,8 +20,6 @@ TestBoilerplate() { ; End test and save results framework.EndTest() - - MsgBox("Test completed. Results saved to: " . framework.resultDir) } ; Run the test diff --git a/test_framework/tests/TestObliteratedStart.ahk b/test_framework/tests/TestObliteratedStart.ahk new file mode 100644 index 0000000000..1a5fa68634 --- /dev/null +++ b/test_framework/tests/TestObliteratedStart.ahk @@ -0,0 +1,51 @@ +; This test case launches a new custom chapter with the obliterated start + +#Requires AutoHotkey v2.0 +#Include ..\CMTestingFramework.ahk + +TestObliteratedStart() { + ; Initialize testing framework + framework := CMTestingFramework() + + ; Start test + framework.StartTest("ObliteratedStart") + + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + ; Wait for application to fully load + framework.Wait(7000) + .ClickElement("MainMenu.NewGame") ; Click on "New Game" in the main menu + .Wait(8500) + .ClickElement("Creation.CreateCustom") ; Click Dark Angels + .Wait(3000) + .ClickElement("Creation.Homeworld") + .ClickElement("Creation.PurityUp") + .ClickElement("Creation.PurityUp") + .ClickElement("Creation.PurityUp") + .ClickElement("Creation.PurityUp") + .ClickElement("Creation.PurityUp") + .ClickElement("Creation.DisadvSlot1") + .ClickElement("Creation.Obliterated") + .ClickElement("Creation.NextArrow").WaitSeconds(2) + .ClickElement("Creation.NextArrow").WaitSeconds(2) + .ClickElement("Creation.NextArrow").WaitSeconds(2) + .ClickElement("Creation.NextArrow").WaitSeconds(2) + .ClickElement("Creation.NextArrow").WaitSeconds(3) + .ClickElement("GameScreen.EndTurn") + .ClickElement("GameScreen.EndTurn") + .ClickElement("GameScreen.EndTurn") + .ClickElement("GameScreen.EndTurn") + .ClickElement("GameScreen.EndTurn") + .ClickElement("GameScreen.EndTurn") + .WaitSeconds(1) + + ; End test and save results + framework.EndTest() +} + +; Run the test +TestObliteratedStart() diff --git a/test_framework/tests/StartGame.ahk b/test_framework/tests/TestStartGame.ahk similarity index 90% rename from test_framework/tests/StartGame.ahk rename to test_framework/tests/TestStartGame.ahk index 0f17e82106..9aaa9e02e0 100644 --- a/test_framework/tests/StartGame.ahk +++ b/test_framework/tests/TestStartGame.ahk @@ -1,4 +1,3 @@ -; CM Application Test Case: Create New Customer Record ; This test case demonstrates how to use the CMTestingFramework #Requires AutoHotkey v2.0 @@ -44,8 +43,6 @@ TestStartGame() { ; End test and save results framework.EndTest() - - MsgBox("Test completed. Results saved to: " . framework.resultDir) } ; Run the test diff --git a/test_framework/tests/ui_elements.txt b/test_framework/ui_elements.txt similarity index 87% rename from test_framework/tests/ui_elements.txt rename to test_framework/ui_elements.txt index 5a9ab8a740..8e7f9c040f 100644 --- a/test_framework/tests/ui_elements.txt +++ b/test_framework/ui_elements.txt @@ -58,3 +58,12 @@ this.Fleet.ShipSlot1 := {x: 882, y: 93} this.Diplomacy.ImperiumAudience := {x: 244, y: 275} this.Diplomacy.MechanicusAudience := {x: 235, y: 385} this.Diplomacy.MeetChaosEmmisary := {x: 691, y: 189} +this.Creation.StrengthUp := {x: 388, y: 273} +this.Creation.StrengthDown := {x: 363, y: 276} +this.Creation.CooperationUp := {x: 391, y: 321} +this.Creation.CooperationDown := {x: 359, y: 321} +this.Creation.PurityUp := {x: 390, y: 367} +this.Creation.PurityDown := {x: 356, y: 364} +this.Creation.StabilityUp := {x: 388, y: 405} +this.Creation.StabilityDown := {x: 367, y: 407} +this.Creation.Obliterated := {x: 384, y: 416} From 4fbb18af493f11f63cd5fcc18abfeda68b04c4e4 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 00:49:51 +1000 Subject: [PATCH 04/17] messing with save file reading, mixed results --- test_framework/CMTestingFramework.ahk | 54 +++++- test_framework/JSON.ahk | 175 ++++++++++++++++++++ test_framework/RunAllTests.ahk | 2 + test_framework/logs/test_log.txt | 1 + test_framework/tests/SampleReadSaveGame.ahk | 32 ++++ test_framework/ui_elements.txt | 3 + 6 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 test_framework/JSON.ahk create mode 100644 test_framework/logs/test_log.txt create mode 100644 test_framework/tests/SampleReadSaveGame.ahk diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index d628d1926f..ac4288d468 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -3,6 +3,7 @@ #Requires AutoHotkey v2.0 #SingleInstance Force +#Include JSON.ahk class CMTestingFramework { ; Configuration @@ -14,6 +15,8 @@ class CMTestingFramework { this.resultDir := A_ScriptDir "\results\" this.mouseSpeed := 8 ; 1-10, with 10 being slowest (more human-like) this.errorLogPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\Logs" + this.savesPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\Save Files" + this.savesIniPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\saves.ini" ; State tracking this.currentTest := "" @@ -49,7 +52,12 @@ class CMTestingFramework { CloseApp() { try { WinClose("ahk_exe " . this.appPath) - return true + if(WinWaitClose("ahk_exe " . this.appPath, , 5) == 1){ + return true + } else { + this.LogError("Timeout while waiting to close app") + return false + } } catch as e { this.LogError("Failed to properly close application: " . e.Message) return false @@ -68,7 +76,7 @@ class CMTestingFramework { } ; End current test and save results - EndTest() { + EndTest(status := "Success") { this.testEndTime := A_Now duration := DateDiff(this.testEndTime, this.testStartTime, "Seconds") @@ -78,7 +86,8 @@ class CMTestingFramework { result := "# Test Report: " . this.currentTest . "`n" result .= "* Date: " . FormatTime(A_Now, "yyyy-MM-dd") . "`n" result .= "* Time: " . FormatTime(A_Now, "HH:mm:ss") . "`n" - result .= "* Duration: " . duration . " seconds`n`n" + result .= "* Duration: " . duration . " seconds`n" + result .= "* Status: " . status . "`n`n" result .= "## Test Steps:`n" for step in this.testSteps { @@ -91,13 +100,14 @@ class CMTestingFramework { f := FileOpen(fileName, "w").Write(result) f.Close() this.testResult := result + this.CloseApp() return true } catch as e { this.LogError("Failed to save test results: " . e.Message) + this.CloseApp() return false } - this.CloseApp() } ; Log a test step @@ -262,6 +272,7 @@ class CMTestingFramework { } else if(this.CheckForCrashDialog()) { this.LogError("Crash dialog detected via window class") this.ReadErrorLogs() + this.CloseDialog() this.EndTest() return true } @@ -276,6 +287,15 @@ class CMTestingFramework { return false } + CloseDialog() { + ; Check if any dialog from the app exists with specific window class + if (WinExist("ahk_exe " . this.appPath . " ahk_class #32770")) { ; #32770 is common for dialogs + WinClose("ahk_exe " . this.appPath . " ahk_class #32770") + WinWaitClose("ahk_exe " . this.appPath . " ahk_class #32770") + } + return + } + ; Read error logs after crash ReadErrorLogs() { try { @@ -309,10 +329,20 @@ class CMTestingFramework { } } + ; !NOT IMPLEMENTED + ReadSaveFile(slot := 1) { + saveFile := this.savesPath . "\save" . slot . ".json" + saveFileString := FileRead(saveFile) + + FileCopy(saveFile, this.resultDir . FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_savefile.json") + saveObject := JSON.parse(saveFileString, false, false) + return saveObject.Save + } + ; General error handler ErrorHandler(exception, mode) { this.LogError("Framework error: " . exception.Message) - this.TakeScreenshot("framework_error") + ; this.TakeScreenshot("framework_error") return true ; Continue running } @@ -356,7 +386,6 @@ class CMTestingFramework { ; Click on an element based on its name in CMUIMap ; framework.ClickElement("MainMenu.NewGame") ClickElement(elementPath) { - this.LogStep("Getting element " . elementPath) element := this.UIMap.GetElement(elementPath) if (element) { this.Click(element.x, element.y) @@ -416,8 +445,11 @@ class CMTestingFramework { } } } -} + + + +} ; Helper function for capturing screenshots WinCapture(x, y, w, h, filename) { ; Create bitmap @@ -444,18 +476,19 @@ WinCapture(x, y, w, h, filename) { ; This class stores xy coordinates for common game elements and buttons ; All coordinates assume a game size of 1280x720 class CMUIMap { + ; Add to this list when a new category of ui elements is wanted __Init(){ this.MainMenu := {} this.Creation := {} this.GameScreen := {} this.InGameMenu := {} + this.SaveMenu := {} + this.LoadMenu := {} this.ChapterManagement := {} this.Apothecarium := {} this.Armamentarium := {} this.Fleet := {} this.Diplomacy := {} - - } ; To populate new coords, use DiscoverUIElements.ahk, see HowToUse.md for instructions @@ -510,6 +543,9 @@ class CMUIMap { this.InGameMenu.Options := { x: 750, y: 363 } this.InGameMenu.Exit := { x: 749, y: 422 } this.InGameMenu.Return := { x: 746, y: 551 } + this.SaveMenu.SaveSlot1 := { x: 1109, y: 362 } + this.LoadMenu.LoadSlot1 := { x: 1109, y: 362 } + this.LoadMenu.LoadAutosave := { x: 1127, y: 241 } this.ChapterManagement.Headquarters := { x: 632, y: 173 } this.ChapterManagement.Company1 := { x: 79, y: 419 } this.ChapterManagement.Company10 := { x: 1189, y: 362 } diff --git a/test_framework/JSON.ahk b/test_framework/JSON.ahk new file mode 100644 index 0000000000..1266853bb8 --- /dev/null +++ b/test_framework/JSON.ahk @@ -0,0 +1,175 @@ +/************************************************************************ + * @description: JSON格式字符串序列化和反序列化, 修改自[HotKeyIt/Yaml](https://github.com/HotKeyIt/Yaml) + * 增加了对true/false/null类型的支持, 保留了数值的类型 + * @author thqby, HotKeyIt + * @date 2024/02/24 + * @version 1.0.7 + ***********************************************************************/ + +class JSON { + static null := ComValue(1, 0), true := ComValue(0xB, 1), false := ComValue(0xB, 0) + + /** + * Converts a AutoHotkey Object Notation JSON string into an object. + * @param text A valid JSON string. + * @param keepbooltype convert true/false/null to JSON.true / JSON.false / JSON.null where it's true, otherwise 1 / 0 / '' + * @param as_map object literals are converted to map, otherwise to object + */ + static parse(text, keepbooltype := false, as_map := true) { + keepbooltype ? (_true := this.true, _false := this.false, _null := this.null) : (_true := true, _false := false, + _null := "") + as_map ? (map_set := (maptype := Map).Prototype.Set) : (map_set := (obj, key, val) => obj.%key% := val, maptype := + Object) + NQ := "", LF := "", LP := 0, P := "", R := "" + D := [C := (A := InStr(text := LTrim(text, " `t`r`n"), "[") = 1) ? [] : maptype()], text := LTrim(SubStr(text, + 2), " `t`r`n"), L := 1, N := 0, V := K := "", J := C, !(Q := InStr(text, '"') != 1) ? text := LTrim(text, + '"') : "" + loop parse text, '"' { + Q := NQ ? 1 : !Q + NQ := Q && RegExMatch(A_LoopField, '(^|[^\\])(\\\\)*\\$') + if !Q { + if (t := Trim(A_LoopField, " `t`r`n")) = "," || (t = ":" && V := 1) + continue + else if t && (InStr("{[]},:", SubStr(t, 1, 1)) || A && RegExMatch(t, + "m)^(null|false|true|-?\d+(\.\d*(e[-+]\d+)?)?)\s*[,}\]\r\n]")) { + loop parse t { + if N && N-- + continue + if InStr("`n`r `t", A_LoopField) + continue + else if InStr("{[", A_LoopField) { + if !A && !V + throw Error("Malformed JSON - missing key.", 0, t) + C := A_LoopField = "[" ? [] : maptype(), A ? D[L].Push(C) : map_set(D[L], K, C), D.Has(++L) ? + D[L] := C : D.Push(C), V := "", A := Type(C) = "Array" + continue + } else if InStr("]}", A_LoopField) { + if !A && V + throw Error("Malformed JSON - missing value.", 0, t) + else if L = 0 + throw Error("Malformed JSON - to many closing brackets.", 0, t) + else C := --L = 0 ? "" : D[L], A := Type(C) = "Array" + } else if !(InStr(" `t`r,", A_LoopField) || (A_LoopField = ":" && V := 1)) { + if RegExMatch(SubStr(t, A_Index), + "m)^(null|false|true|-?\d+(\.\d*(e[-+]\d+)?)?)\s*[,}\]\r\n]", &R) && (N := R.Len(0) - 2, R := + R.1, 1) { + if A + C.Push(R = "null" ? _null : R = "true" ? _true : R = "false" ? _false : IsNumber(R) ? + R + 0 : R) + else if V + map_set(C, K, R = "null" ? _null : R = "true" ? _true : R = "false" ? _false : + IsNumber(R) ? R + 0 : R), K := V := "" + else throw Error("Malformed JSON - missing key.", 0, t) + } else { + ; Added support for comments without '"' + if A_LoopField == '/' { + nt := SubStr(t, A_Index + 1, 1), N := 0 + if nt == '/' { + if nt := InStr(t, '`n', , A_Index + 2) + N := nt - A_Index - 1 + } else if nt == '*' { + if nt := InStr(t, '*/', , A_Index + 2) + N := nt + 1 - A_Index + } else nt := 0 + if N + continue + } + throw Error("Malformed JSON - unrecognized character.", 0, A_LoopField " in " t) + } + } + } + } else if A || InStr(t, ':') > 1 + throw Error("Malformed JSON - unrecognized character.", 0, SubStr(t, 1, 1) " in " t) + } else if NQ && (P .= A_LoopField '"', 1) + continue + else if A + LF := P A_LoopField, C.Push(InStr(LF, "\") ? UC(LF) : LF), P := "" + else if V + LF := P A_LoopField, map_set(C, K, InStr(LF, "\") ? UC(LF) : LF), K := V := P := "" + else + LF := P A_LoopField, K := InStr(LF, "\") ? UC(LF) : LF, P := "" + } + return J + UC(S, e := 1) { + static m := Map('"', '"', "a", "`a", "b", "`b", "t", "`t", "n", "`n", "v", "`v", "f", "`f", "r", "`r") + local v := "" + loop parse S, "\" + if !((e := !e) && A_LoopField = "" ? v .= "\" : !e ? (v .= A_LoopField, 1) : 0) + v .= (t := m.Get(SubStr(A_LoopField, 1, 1), 0)) ? t SubStr(A_LoopField, 2) : + (t := RegExMatch(A_LoopField, "i)^(u[\da-f]{4}|x[\da-f]{2})\K")) ? + Chr("0x" SubStr(A_LoopField, 2, t - 2)) SubStr(A_LoopField, t) : "\" A_LoopField, + e := A_LoopField = "" ? e : !e + return v + } + } + + /** + * Converts a AutoHotkey Array/Map/Object to a Object Notation JSON string. + * @param obj A AutoHotkey value, usually an object or array or map, to be converted. + * @param expandlevel The level of JSON string need to expand, by default expand all. + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + */ + static stringify(obj, expandlevel := unset, space := " ") { + expandlevel := IsSet(expandlevel) ? Abs(expandlevel) : 10000000 + return Trim(CO(obj, expandlevel)) + CO(O, J := 0, R := 0, Q := 0) { + static M1 := "{", M2 := "}", S1 := "[", S2 := "]", N := "`n", C := ",", S := "- ", E := "", K := ":" + if (OT := Type(O)) = "Array" { + D := !R ? S1 : "" + for key, value in O { + F := (VT := Type(value)) = "Array" ? "S" : InStr("Map,Object", VT) ? "M" : E + Z := VT = "Array" && value.Length = 0 ? "[]" : ((VT = "Map" && value.count = 0) || (VT = "Object" && + ObjOwnPropCount(value) = 0)) ? "{}" : "" + D .= (J > R ? "`n" CL(R + 2) : "") (F ? (%F%1 (Z ? "" : CO(value, J, R + 1, F)) %F%2) : ES(value)) ( + OT = "Array" && O.Length = A_Index ? E : C) + } + } else { + D := !R ? M1 : "" + for key, value in (OT := Type(O)) = "Map" ? (Y := 1, O) : (Y := 0, O.OwnProps()) { + F := (VT := Type(value)) = "Array" ? "S" : InStr("Map,Object", VT) ? "M" : E + Z := VT = "Array" && value.Length = 0 ? "[]" : ((VT = "Map" && value.count = 0) || (VT = "Object" && + ObjOwnPropCount(value) = 0)) ? "{}" : "" + D .= (J > R ? "`n" CL(R + 2) : "") (Q = "S" && A_Index = 1 ? M1 : E) ES(key) K (F ? (%F%1 (Z ? "" : + CO(value, J, R + 1, F)) %F%2) : ES(value)) (Q = "S" && A_Index = (Y ? O.count : ObjOwnPropCount( + O)) ? M2 : E) (J != 0 || R ? (A_Index = (Y ? O.count : ObjOwnPropCount(O)) ? E : C) : E) + if J = 0 && !R + D .= (A_Index < (Y ? O.count : ObjOwnPropCount(O)) ? C : E) + } + } + if J > R + D .= "`n" CL(R + 1) + if R = 0 + D := RegExReplace(D, "^\R+") (OT = "Array" ? S2 : M2) + return D + } + ES(S) { + switch Type(S) { + case "Float": + if (v := '', d := InStr(S, 'e')) + v := SubStr(S, d), S := SubStr(S, 1, d - 1) + if ((StrLen(S) > 17) && (d := RegExMatch(S, "(99999+|00000+)\d{0,3}$"))) + S := Round(S, Max(1, d - InStr(S, ".") - 1)) + return S v + case "Integer": + return S + case "String": + S := StrReplace(S, "\", "\\") + S := StrReplace(S, "`t", "\t") + S := StrReplace(S, "`r", "\r") + S := StrReplace(S, "`n", "\n") + S := StrReplace(S, "`b", "\b") + S := StrReplace(S, "`f", "\f") + S := StrReplace(S, "`v", "\v") + S := StrReplace(S, '"', '\"') + return '"' S '"' + default: + return S == this.true ? "true" : S == this.false ? "false" : "null" + } + } + CL(i) { + loop (s := "", space ? i - 1 : 0) + s .= space + return s + } + } +} \ No newline at end of file diff --git a/test_framework/RunAllTests.ahk b/test_framework/RunAllTests.ahk index 57fdfa710a..8469ad7988 100644 --- a/test_framework/RunAllTests.ahk +++ b/test_framework/RunAllTests.ahk @@ -1,3 +1,5 @@ +; Executes all test files in the /tests folder which start with "Test" + #Requires AutoHotkey v2.0 #SingleInstance Force diff --git a/test_framework/logs/test_log.txt b/test_framework/logs/test_log.txt new file mode 100644 index 0000000000..d69d9b8f86 --- /dev/null +++ b/test_framework/logs/test_log.txt @@ -0,0 +1 @@ +00:01:03 - Application launched successfully diff --git a/test_framework/tests/SampleReadSaveGame.ahk b/test_framework/tests/SampleReadSaveGame.ahk new file mode 100644 index 0000000000..5303226c0d --- /dev/null +++ b/test_framework/tests/SampleReadSaveGame.ahk @@ -0,0 +1,32 @@ +; This test case demonstrates how to use the CMTestingFramework + +#Requires AutoHotkey v2.0 +#Include ..\CMTestingFramework.ahk + +TestSaveGame() { + ; Initialize testing framework + framework := CMTestingFramework() + + ; Start test + framework.StartTest("SaveGame") + + ; JSON reading doesn't work in AHK, but ini reading does. + slot := "1" + marinesCount := IniRead(framework.savesIniPath, slot, "marines", 0) + + if(Integer(marinesCount) < 1000){ + status := "Success" + } else { + status := "Failure" + framework.LogStep("Failed Step: Expected a marines value between 990 and 1100, instead got " . marinesCount) + } + + MsgBox("Marines in save slot 1: " . marinesCount) + + + ; End test and save results + framework.EndTest(status) +} + +; Run the test +TestSaveGame() diff --git a/test_framework/ui_elements.txt b/test_framework/ui_elements.txt index 8e7f9c040f..b39207af69 100644 --- a/test_framework/ui_elements.txt +++ b/test_framework/ui_elements.txt @@ -67,3 +67,6 @@ this.Creation.PurityDown := {x: 356, y: 364} this.Creation.StabilityUp := {x: 388, y: 405} this.Creation.StabilityDown := {x: 367, y: 407} this.Creation.Obliterated := {x: 384, y: 416} +this.SaveMenu.SaveSlot1 := {x: 1109, y: 362} +this.LoadMenu.LoadSlot1 := {x: 1109, y: 362} +this.LoadMenu.LoadAutosave := {x: 1127, y: 241} From 3fcea269b9e3bd0af4ac126032e8879c5dd4d651 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 01:14:48 +1000 Subject: [PATCH 05/17] quickstart tests --- test_framework/CMTestingFramework.ahk | 129 ++++++++---------- ...eReadSaveGame.ahk => TestReadSaveGame.ahk} | 0 test_framework/tests/TestStartGame.ahk | 14 +- 3 files changed, 57 insertions(+), 86 deletions(-) rename test_framework/tests/{SampleReadSaveGame.ahk => TestReadSaveGame.ahk} (100%) diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index ac4288d468..09261108dc 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -232,28 +232,28 @@ class CMTestingFramework { ; Take a screenshot of the current window ; !!! NOT IMPLEMENTED - TakeScreenshot(description := "") { - if (description == "") - description := "screenshot" - - ; Create filename with timestamp - filename := FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_" . this.currentTest . "_" . description . ".png" - path := this.screenshotDir . filename - - try { - ; Capture active window - WinGetPos(&x, &y, &w, &h, "A") - screenshot := Gui() - screenshot.Add("Picture", "w" . w . " h" . h, path) - WinCapture(x, y, w, h, path) - - this.LogStep("Screenshot saved: " . filename) - return path - } catch as e { - this.LogError("Failed to take screenshot: " . e.Message) - return "" - } - } + ; TakeScreenshot(description := "") { + ; if (description == "") + ; description := "screenshot" + + ; ; Create filename with timestamp + ; filename := FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_" . this.currentTest . "_" . description . ".png" + ; path := this.screenshotDir . filename + + ; try { + ; ; Capture active window + ; WinGetPos(&x, &y, &w, &h, "A") + ; screenshot := Gui() + ; screenshot.Add("Picture", "w" . w . " h" . h, path) + ; WinCapture(x, y, w, h, path) + + ; this.LogStep("Screenshot saved: " . filename) + ; return path + ; } catch as e { + ; this.LogError("Failed to take screenshot: " . e.Message) + ; return "" + ; } + ; } ; Error Handling Functions @@ -356,32 +356,6 @@ class CMTestingFramework { return this.LaunchApp() } - ; Helper function to find image on screen - FindImage(imagePath, variation := 50) { - try { - ImageSearch(&foundX, &foundY, 0, 0, A_ScreenWidth, A_ScreenHeight, "*" . variation . " " . imagePath) - if (foundX && foundY) { - this.LogStep("Image found at: " . foundX . ", " . foundY) - return { x: foundX, y: foundY, found: true } - } else { - this.LogStep("Image not found: " . imagePath) - return { found: false } - } - } catch as e { - this.LogError("Error searching for image: " . e.Message) - return { found: false } - } - } - - ; Click on an image if found - ClickOnImage(imagePath, variation := 50) { - result := this.FindImage(imagePath, variation) - if (result.found) { - this.Click(result.x, result.y) - return true - } - return false - } ; Click on an element based on its name in CMUIMap ; framework.ClickElement("MainMenu.NewGame") @@ -446,31 +420,40 @@ class CMTestingFramework { } } - + ;;; This section contains some snippets of common things to quickly get a test running + + ; Quick start new game as given chapter + ; chapterName must have coords registered in CMUIMap already + StartGameAs(chapterName := ""){ + if(chapterName == ""){ + this.LogError("No chapter name provided to StartGameAs") + return false + } + this.Wait(7000) + .ClickElement("MainMenu.NewGame") + .Wait(8500) + .ClickElement("Creation." . chapterName) + .Wait(3000) + .ClickElement("Creation.SkipArrow") + .Wait(4000) + .Click(500, 400) + .Click(500, 400) + .Click(500, 400) + .Click(500, 400) + .Click(500, 400) + .Wait(1000) + return this + } + + ; Need to make sure save slots 1 2 and 3 are registered in CMUIMap + SaveGameToSlot(slotNum := "1"){ + this.ClickElement("GameScreen.Menu") + .ClickElement("InGameMenu.Save") + .ClickElement("SaveMenu.SaveSlot" . slotNum) + return this + } -} -; Helper function for capturing screenshots -WinCapture(x, y, w, h, filename) { - ; Create bitmap - hdc := DllCall("GetDC", "Ptr", 0) - hdcMem := DllCall("CreateCompatibleDC", "Ptr", hdc) - hBitmap := DllCall("CreateCompatibleBitmap", "Ptr", hdc, "Int", w, "Int", h) - DllCall("SelectObject", "Ptr", hdcMem, "Ptr", hBitmap) - - ; Copy screen to bitmap - DllCall("BitBlt", "Ptr", hdcMem, "Int", 0, "Int", 0, "Int", w, "Int", h, "Ptr", hdc, "Int", x, "Int", y, "UInt", - 0x00CC0020) ; SRCCOPY - - ; Save bitmap to file - DllCall("gdiplus\GdipCreateBitmapFromHBITMAP", "Ptr", hBitmap, "Ptr", 0, "Ptr*", &pBitmap := 0) - DllCall("gdiplus\GdipSaveImageToFile", "Ptr", pBitmap, "Str", filename, "Ptr", 0, "Ptr", 0) - - ; Cleanup - DllCall("gdiplus\GdipDisposeImage", "Ptr", pBitmap) - DllCall("DeleteObject", "Ptr", hBitmap) - DllCall("DeleteDC", "Ptr", hdcMem) - DllCall("ReleaseDC", "Ptr", 0, "Ptr", hdc) } ; This class stores xy coordinates for common game elements and buttons @@ -501,10 +484,10 @@ class CMUIMap { this.Creation.DarkAngels := { x: 371, y: 155 } this.Creation.BlackTemplars := { x: 388, y: 248 } this.Creation.CustomSlot1 := { x: 373, y: 422 } - this.Creation.CreateCustom := { x: 625, y: 519 } - this.Creation.CreateRandom := { x: 679, y: 521 } this.Creation.Deathwatch := { x: 539, y: 517 } this.Creation.AngryMarines := { x: 373, y: 524 } + this.Creation.CreateCustom := { x: 625, y: 519 } + this.Creation.CreateRandom := { x: 679, y: 521 } this.Creation.NextArrow := { x: 760, y: 631 } this.Creation.BackArrow := { x: 523, y: 639 } this.Creation.SkipArrow := { x: 822, y: 635 } diff --git a/test_framework/tests/SampleReadSaveGame.ahk b/test_framework/tests/TestReadSaveGame.ahk similarity index 100% rename from test_framework/tests/SampleReadSaveGame.ahk rename to test_framework/tests/TestReadSaveGame.ahk diff --git a/test_framework/tests/TestStartGame.ahk b/test_framework/tests/TestStartGame.ahk index 9aaa9e02e0..2d4ea3b379 100644 --- a/test_framework/tests/TestStartGame.ahk +++ b/test_framework/tests/TestStartGame.ahk @@ -17,19 +17,7 @@ TestStartGame() { } ; Wait for application to fully load - framework.Wait(7000) - .ClickElement("MainMenu.NewGame") ; Click on "New Game" in the main menu - .Wait(8500) - .ClickElement("Creation.DarkAngels") ; Click Dark Angels - .Wait(3000) - .ClickElement("Creation.SkipArrow") - .Wait(5000) - .Click(500, 400) - .Click(500, 400) - .Click(500, 400) - .Click(500, 400) - .Click(500, 400) ;skip intro sprawl - .Wait(1000) + framework.StartGameAs("DarkAngels") .ClickElement("GameScreen.ChapterManagement") .Wait(1000) .ClickElement("GameScreen.ChapterSettings") From 276a27c12b64e23f02c460de84f7891c36f96a64 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 09:29:22 +1000 Subject: [PATCH 06/17] game stat reading --- scripts/scr_cheatcode/scr_cheatcode.gml | 3 + scripts/scr_save/scr_save.gml | 88 +++++++++++ test_framework/CMTestingFramework.ahk | 40 +++-- test_framework/HowToUse.md | 10 +- test_framework/JSON.ahk | 175 ---------------------- test_framework/logs/test_log.txt | 3 + test_framework/tests/TestReadSaveGame.ahk | 39 +++-- test_framework/ui_elements.txt | 1 + 8 files changed, 160 insertions(+), 199 deletions(-) delete mode 100644 test_framework/JSON.ahk diff --git a/scripts/scr_cheatcode/scr_cheatcode.gml b/scripts/scr_cheatcode/scr_cheatcode.gml index f802f062c5..db5edb9860 100644 --- a/scripts/scr_cheatcode/scr_cheatcode.gml +++ b/scripts/scr_cheatcode/scr_cheatcode.gml @@ -296,6 +296,9 @@ function scr_cheatcode(argument0) { var _fleet = get_nearest_player_fleet(0,0); add_ship_to_fleet(new_player_ship("Gloriana"),_fleet); break; + case "dumpstats": + scr_dump_stats(); + break; } } diff --git a/scripts/scr_save/scr_save.gml b/scripts/scr_save/scr_save.gml index e14ca9cc24..a09b1c6966 100644 --- a/scripts/scr_save/scr_save.gml +++ b/scripts/scr_save/scr_save.gml @@ -168,3 +168,91 @@ function scr_save(save_part,save_id, autosaving = false) { log_message($"Autosaving took {diff} seconds!"); } } + +/// Testing/debugging function that uses easily parsed ini format so that the test runner can +/// check things like number of marines per company per role, requistion, forge points etc +function scr_dump_stats(){ + var _marine_totals = { + company_0: { + total: 0, + chapter_master: 0, + honour_guard: 0, + veteran: 0, + terminator: 0, + captain: 0, + ancient: 0, + champion: 0, + apothecary: 0, + chaplain: 0, + librarian: 0, + tactical: 0, + assault: 0, + devastator: 0, + scout: 0, + dreadnought: 0, + } + } + for(var c = 1; c < 11; c++){ + _marine_totals[$$"company_{c}"] = variable_clone(_marine_totals.company_0); + } + + for(var coy = 0; coy < 11; coy++){ + for(var m = 0; m < 500; m++){ + if(obj_ini.name[coy][m] != ""){ + var _unit = fetch_unit([coy,m]); + if(_unit == ""){ + continue; + } + var _propname = $"company_{coy}"; + _marine_totals[$_propname][$"total"] += 1; + switch(_unit.role()){ + case obj_ini.role[100][eROLE.Ancient]: _marine_totals[$_propname][$"ancient"] += 1; break; + case obj_ini.role[100][eROLE.Apothecary]: _marine_totals[$_propname][$"apothecary"] += 1; break; + case obj_ini.role[100][eROLE.ChapterMaster]: _marine_totals[$_propname][$"chapter_master"] += 1; break; + case obj_ini.role[100][eROLE.Veteran]: _marine_totals[$_propname][$"veteran"] += 1; break; + case obj_ini.role[100][eROLE.Terminator]: _marine_totals[$_propname][$"terminator"] += 1; break; + case obj_ini.role[100][eROLE.Tactical]: _marine_totals[$_propname][$"tactical"] += 1; break; + case obj_ini.role[100][eROLE.Assault]: _marine_totals[$_propname][$"assault"] += 1; break; + case obj_ini.role[100][eROLE.Devastator]: _marine_totals[$_propname][$"devastator"] += 1; break; + case obj_ini.role[100][eROLE.Scout]: _marine_totals[$_propname][$"scout"] += 1; break; + case obj_ini.role[100][eROLE.Captain]: _marine_totals[$_propname][$"captain"] += 1; break; + case obj_ini.role[100][eROLE.Champion]: _marine_totals[$_propname][$"champion"] += 1; break; + case obj_ini.role[100][eROLE.Chaplain]: _marine_totals[$_propname][$"chaplain"] += 1; break; + case obj_ini.role[100][eROLE.Dreadnought]: _marine_totals[$_propname][$"dreadnought"] += 1; break; + case obj_ini.role[100][eROLE.Librarian]: _marine_totals[$_propname][$"librarian"] += 1; break; + case "Codiciery": _marine_totals[$_propname][$"librarian"] += 1; break; + case "Lexicanum": _marine_totals[$_propname][$"librarian"] += 1; break; + } + } + } + } + + // todo + var _ship_totals = { + battle_barge: 0, + strike_cruiser: 0, + hunter: 0, + gladius: 0 + } + + + + ini_open("gamestats.ini"); + ini_write_real("Resources", "requisition", obj_controller.requisition); + ini_write_real("Resources", "forge_points", obj_controller.forge_points); + ini_write_real("Resources", "apothecary_recruit_points", obj_controller.apothecary_recruit_points); + ini_write_real("Resources", "marines", obj_controller.marines); + + + for(var c = 0; c < 11; c ++){ + var stats = _marine_totals[$$"company_{c}"]; + var _role_names = struct_get_names(stats); + var _role_len = array_length(_role_names); + for (var k = 0; k < _role_len; k++){ + var _role_name = _role_names[k]; + var _val = stats[$_role_name]; + ini_write_real($"Marines.Coy{c}", _role_name, _val); + } + } + ini_close(); +} \ No newline at end of file diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index 09261108dc..0b9b0f9080 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -3,7 +3,6 @@ #Requires AutoHotkey v2.0 #SingleInstance Force -#Include JSON.ahk class CMTestingFramework { ; Configuration @@ -17,6 +16,7 @@ class CMTestingFramework { this.errorLogPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\Logs" this.savesPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\Save Files" this.savesIniPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\saves.ini" + this.gameStatsIniPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\gamestats.ini" ; State tracking this.currentTest := "" @@ -182,6 +182,7 @@ class CMTestingFramework { ; Send text (typing) SendText(text) { + SetKeyDelay(100) Send(text) this.LogStep("Text sent: " . text) @@ -267,13 +268,13 @@ class CMTestingFramework { if (!this.IsAppRunning()) { this.LogError("Application crashed during " . this.currentTest) this.ReadErrorLogs() - this.EndTest() + this.EndTest("Failure") return true } else if(this.CheckForCrashDialog()) { this.LogError("Crash dialog detected via window class") this.ReadErrorLogs() this.CloseDialog() - this.EndTest() + this.EndTest("Failure") return true } return false @@ -330,14 +331,14 @@ class CMTestingFramework { } ; !NOT IMPLEMENTED - ReadSaveFile(slot := 1) { - saveFile := this.savesPath . "\save" . slot . ".json" - saveFileString := FileRead(saveFile) + ; ReadSaveFile(slot := 1) { + ; saveFile := this.savesPath . "\save" . slot . ".json" + ; saveFileString := FileRead(saveFile) - FileCopy(saveFile, this.resultDir . FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_savefile.json") - saveObject := JSON.parse(saveFileString, false, false) - return saveObject.Save - } + ; FileCopy(saveFile, this.resultDir . FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_savefile.json") + ; saveObject := JSON.parse(saveFileString, false, false) + ; return saveObject.Save + ; } ; General error handler ErrorHandler(exception, mode) { @@ -390,7 +391,8 @@ class CMTestingFramework { discoveryGui.Add("Text", , "Press F7 to capture mouse position") discoveryGui.Add("Text", "vCoordinates w200 h20", "X: 0, Y: 0") discoveryGui.Add("Text", , "Section Name:") - discoveryGui.Add("Edit", "vSectionName w150") + ; Should match CMUIMap top level sections + discoveryGui.Add("DropDownList", "vSectionName w150", ["MainMenu", "Creation", "GameScreen", "InGameMenu", "SaveMenu", "LoadMenu", "Apothecarium", "Armamentarium", "Fleet", "Diplomacy"]) discoveryGui.Add("Text", , "Element Name:") discoveryGui.Add("Edit", "vElementName w150") discoveryGui.Add("Button", "Default", "Save").OnEvent("Click", SavePosition) @@ -454,6 +456,21 @@ class CMTestingFramework { return this } + ; must be called from the normal game screen + DumpGameStats(){ + this.SendText("p") + .WaitSeconds(1) + .SendText("dumpstats") + .WaitSeconds(1) + .SendCombo("{enter}") + + return this + } + + ; Copy gamestats.ini to the test results folder so that they can be inspected upon failure + CopyGameStats(){ + FileCopy(this.gameStatsIniPath, this.resultDir . FormatTime(A_Now, "yyyyMMdd_HHmmss") . "_gamestats.ini") + } } ; This class stores xy coordinates for common game elements and buttons @@ -485,6 +502,7 @@ class CMUIMap { this.Creation.BlackTemplars := { x: 388, y: 248 } this.Creation.CustomSlot1 := { x: 373, y: 422 } this.Creation.Deathwatch := { x: 539, y: 517 } + this.Creation.Lamenters := { x: 546, y: 249 } this.Creation.AngryMarines := { x: 373, y: 524 } this.Creation.CreateCustom := { x: 625, y: 519 } this.Creation.CreateRandom := { x: 679, y: 521 } diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index db053ba1ee..b26bfba5d7 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -73,6 +73,7 @@ Here are the core methods to use in your test cases: - `framework.StartTest("TestName")` - Initialize a test with a name - `framework.LaunchApp()` - Launch the CM application - `framework.EndTest()` - Complete the test and save results +- `framework.CloseApp()` - Exit CM but keep the test running ### Mouse Interaction - `framework.Click(x, y)` - Click at specific coordinates @@ -87,12 +88,17 @@ Here are the core methods to use in your test cases: - `framework.SendCombo("{Ctrl down}{s}{Ctrl up}")` - Send key combinations - `framework.KeyDown("key")` and `framework.KeyUp("key")` - Press and release keys +### Premade Step Sequences +- `framework.StartGameAs("ChapterName")` - Opens game, clicks New Game, selects ChapterName, Skips through Creation screen, dismisses intro sprawl +- `framework.SaveToSlot("1")` - Opens the ingame menu and saves the game to the slot specified. Only works for slots 1 - 3 + ### Wait and Timing -- `framework.Wait(milliseconds)` - Pause execution for specified time +- `framework.Wait(milliseconds)` - Pause execution for specified time in ms +- `framework.WaitSeconds(seconds)` - Pause execution for specified time in seconds ### Documentation -- `framework.TakeScreenshot("description")` - Capture the current screen state - `framework.LogStep("description")` - Record a test step with description +- `framework.LogError("description")` - Record a test step error with description. Builtin methods will do this if they fail to perform an action like clicking or if the game crashes. ### Error Handling - `framework.CheckForCrash()` - Detect if application crashed diff --git a/test_framework/JSON.ahk b/test_framework/JSON.ahk deleted file mode 100644 index 1266853bb8..0000000000 --- a/test_framework/JSON.ahk +++ /dev/null @@ -1,175 +0,0 @@ -/************************************************************************ - * @description: JSON格式字符串序列化和反序列化, 修改自[HotKeyIt/Yaml](https://github.com/HotKeyIt/Yaml) - * 增加了对true/false/null类型的支持, 保留了数值的类型 - * @author thqby, HotKeyIt - * @date 2024/02/24 - * @version 1.0.7 - ***********************************************************************/ - -class JSON { - static null := ComValue(1, 0), true := ComValue(0xB, 1), false := ComValue(0xB, 0) - - /** - * Converts a AutoHotkey Object Notation JSON string into an object. - * @param text A valid JSON string. - * @param keepbooltype convert true/false/null to JSON.true / JSON.false / JSON.null where it's true, otherwise 1 / 0 / '' - * @param as_map object literals are converted to map, otherwise to object - */ - static parse(text, keepbooltype := false, as_map := true) { - keepbooltype ? (_true := this.true, _false := this.false, _null := this.null) : (_true := true, _false := false, - _null := "") - as_map ? (map_set := (maptype := Map).Prototype.Set) : (map_set := (obj, key, val) => obj.%key% := val, maptype := - Object) - NQ := "", LF := "", LP := 0, P := "", R := "" - D := [C := (A := InStr(text := LTrim(text, " `t`r`n"), "[") = 1) ? [] : maptype()], text := LTrim(SubStr(text, - 2), " `t`r`n"), L := 1, N := 0, V := K := "", J := C, !(Q := InStr(text, '"') != 1) ? text := LTrim(text, - '"') : "" - loop parse text, '"' { - Q := NQ ? 1 : !Q - NQ := Q && RegExMatch(A_LoopField, '(^|[^\\])(\\\\)*\\$') - if !Q { - if (t := Trim(A_LoopField, " `t`r`n")) = "," || (t = ":" && V := 1) - continue - else if t && (InStr("{[]},:", SubStr(t, 1, 1)) || A && RegExMatch(t, - "m)^(null|false|true|-?\d+(\.\d*(e[-+]\d+)?)?)\s*[,}\]\r\n]")) { - loop parse t { - if N && N-- - continue - if InStr("`n`r `t", A_LoopField) - continue - else if InStr("{[", A_LoopField) { - if !A && !V - throw Error("Malformed JSON - missing key.", 0, t) - C := A_LoopField = "[" ? [] : maptype(), A ? D[L].Push(C) : map_set(D[L], K, C), D.Has(++L) ? - D[L] := C : D.Push(C), V := "", A := Type(C) = "Array" - continue - } else if InStr("]}", A_LoopField) { - if !A && V - throw Error("Malformed JSON - missing value.", 0, t) - else if L = 0 - throw Error("Malformed JSON - to many closing brackets.", 0, t) - else C := --L = 0 ? "" : D[L], A := Type(C) = "Array" - } else if !(InStr(" `t`r,", A_LoopField) || (A_LoopField = ":" && V := 1)) { - if RegExMatch(SubStr(t, A_Index), - "m)^(null|false|true|-?\d+(\.\d*(e[-+]\d+)?)?)\s*[,}\]\r\n]", &R) && (N := R.Len(0) - 2, R := - R.1, 1) { - if A - C.Push(R = "null" ? _null : R = "true" ? _true : R = "false" ? _false : IsNumber(R) ? - R + 0 : R) - else if V - map_set(C, K, R = "null" ? _null : R = "true" ? _true : R = "false" ? _false : - IsNumber(R) ? R + 0 : R), K := V := "" - else throw Error("Malformed JSON - missing key.", 0, t) - } else { - ; Added support for comments without '"' - if A_LoopField == '/' { - nt := SubStr(t, A_Index + 1, 1), N := 0 - if nt == '/' { - if nt := InStr(t, '`n', , A_Index + 2) - N := nt - A_Index - 1 - } else if nt == '*' { - if nt := InStr(t, '*/', , A_Index + 2) - N := nt + 1 - A_Index - } else nt := 0 - if N - continue - } - throw Error("Malformed JSON - unrecognized character.", 0, A_LoopField " in " t) - } - } - } - } else if A || InStr(t, ':') > 1 - throw Error("Malformed JSON - unrecognized character.", 0, SubStr(t, 1, 1) " in " t) - } else if NQ && (P .= A_LoopField '"', 1) - continue - else if A - LF := P A_LoopField, C.Push(InStr(LF, "\") ? UC(LF) : LF), P := "" - else if V - LF := P A_LoopField, map_set(C, K, InStr(LF, "\") ? UC(LF) : LF), K := V := P := "" - else - LF := P A_LoopField, K := InStr(LF, "\") ? UC(LF) : LF, P := "" - } - return J - UC(S, e := 1) { - static m := Map('"', '"', "a", "`a", "b", "`b", "t", "`t", "n", "`n", "v", "`v", "f", "`f", "r", "`r") - local v := "" - loop parse S, "\" - if !((e := !e) && A_LoopField = "" ? v .= "\" : !e ? (v .= A_LoopField, 1) : 0) - v .= (t := m.Get(SubStr(A_LoopField, 1, 1), 0)) ? t SubStr(A_LoopField, 2) : - (t := RegExMatch(A_LoopField, "i)^(u[\da-f]{4}|x[\da-f]{2})\K")) ? - Chr("0x" SubStr(A_LoopField, 2, t - 2)) SubStr(A_LoopField, t) : "\" A_LoopField, - e := A_LoopField = "" ? e : !e - return v - } - } - - /** - * Converts a AutoHotkey Array/Map/Object to a Object Notation JSON string. - * @param obj A AutoHotkey value, usually an object or array or map, to be converted. - * @param expandlevel The level of JSON string need to expand, by default expand all. - * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. - */ - static stringify(obj, expandlevel := unset, space := " ") { - expandlevel := IsSet(expandlevel) ? Abs(expandlevel) : 10000000 - return Trim(CO(obj, expandlevel)) - CO(O, J := 0, R := 0, Q := 0) { - static M1 := "{", M2 := "}", S1 := "[", S2 := "]", N := "`n", C := ",", S := "- ", E := "", K := ":" - if (OT := Type(O)) = "Array" { - D := !R ? S1 : "" - for key, value in O { - F := (VT := Type(value)) = "Array" ? "S" : InStr("Map,Object", VT) ? "M" : E - Z := VT = "Array" && value.Length = 0 ? "[]" : ((VT = "Map" && value.count = 0) || (VT = "Object" && - ObjOwnPropCount(value) = 0)) ? "{}" : "" - D .= (J > R ? "`n" CL(R + 2) : "") (F ? (%F%1 (Z ? "" : CO(value, J, R + 1, F)) %F%2) : ES(value)) ( - OT = "Array" && O.Length = A_Index ? E : C) - } - } else { - D := !R ? M1 : "" - for key, value in (OT := Type(O)) = "Map" ? (Y := 1, O) : (Y := 0, O.OwnProps()) { - F := (VT := Type(value)) = "Array" ? "S" : InStr("Map,Object", VT) ? "M" : E - Z := VT = "Array" && value.Length = 0 ? "[]" : ((VT = "Map" && value.count = 0) || (VT = "Object" && - ObjOwnPropCount(value) = 0)) ? "{}" : "" - D .= (J > R ? "`n" CL(R + 2) : "") (Q = "S" && A_Index = 1 ? M1 : E) ES(key) K (F ? (%F%1 (Z ? "" : - CO(value, J, R + 1, F)) %F%2) : ES(value)) (Q = "S" && A_Index = (Y ? O.count : ObjOwnPropCount( - O)) ? M2 : E) (J != 0 || R ? (A_Index = (Y ? O.count : ObjOwnPropCount(O)) ? E : C) : E) - if J = 0 && !R - D .= (A_Index < (Y ? O.count : ObjOwnPropCount(O)) ? C : E) - } - } - if J > R - D .= "`n" CL(R + 1) - if R = 0 - D := RegExReplace(D, "^\R+") (OT = "Array" ? S2 : M2) - return D - } - ES(S) { - switch Type(S) { - case "Float": - if (v := '', d := InStr(S, 'e')) - v := SubStr(S, d), S := SubStr(S, 1, d - 1) - if ((StrLen(S) > 17) && (d := RegExMatch(S, "(99999+|00000+)\d{0,3}$"))) - S := Round(S, Max(1, d - InStr(S, ".") - 1)) - return S v - case "Integer": - return S - case "String": - S := StrReplace(S, "\", "\\") - S := StrReplace(S, "`t", "\t") - S := StrReplace(S, "`r", "\r") - S := StrReplace(S, "`n", "\n") - S := StrReplace(S, "`b", "\b") - S := StrReplace(S, "`f", "\f") - S := StrReplace(S, "`v", "\v") - S := StrReplace(S, '"', '\"') - return '"' S '"' - default: - return S == this.true ? "true" : S == this.false ? "false" : "null" - } - } - CL(i) { - loop (s := "", space ? i - 1 : 0) - s .= space - return s - } - } -} \ No newline at end of file diff --git a/test_framework/logs/test_log.txt b/test_framework/logs/test_log.txt index d69d9b8f86..f05e10f8d4 100644 --- a/test_framework/logs/test_log.txt +++ b/test_framework/logs/test_log.txt @@ -1 +1,4 @@ 00:01:03 - Application launched successfully +08:50:19 - Application launched successfully +08:54:25 - Application launched successfully +08:56:10 - Application launched successfully diff --git a/test_framework/tests/TestReadSaveGame.ahk b/test_framework/tests/TestReadSaveGame.ahk index 5303226c0d..a577a8cc77 100644 --- a/test_framework/tests/TestReadSaveGame.ahk +++ b/test_framework/tests/TestReadSaveGame.ahk @@ -6,27 +6,44 @@ TestSaveGame() { ; Initialize testing framework framework := CMTestingFramework() + framework.StartTest("LamentersStartStats") + status := "Success" - ; Start test - framework.StartTest("SaveGame") + ; Launch CM application + if (!framework.LaunchApp()) { + MsgBox("Failed to launch application") + return + } + + framework.StartGameAs("Lamenters") + .DumpGameStats() + .Wait(1000) - ; JSON reading doesn't work in AHK, but ini reading does. - slot := "1" - marinesCount := IniRead(framework.savesIniPath, slot, "marines", 0) + requisition := IniRead(framework.gameStatsIniPath, "Resources", "requisition") - if(Integer(marinesCount) < 1000){ - status := "Success" + if (Integer(requisition) > 500) { + framework.LogStep("Step Failed: Requisition should be 500 for Lamenters") + status := "Failure" } else { + framework.LogStep("Requisition matched expected value") + } + + coy_5_marines := IniRead(framework.gameStatsIniPath, "Marines.Coy5", "total") + + if (Integer(coy_5_marines) > 0) { + framework.LogStep("Step Failed: Lamenters should not have any marines in company 5") status := "Failure" - framework.LogStep("Failed Step: Expected a marines value between 990 and 1100, instead got " . marinesCount) + } else { + framework.LogStep("Coy 5 marines matched expected value") } - MsgBox("Marines in save slot 1: " . marinesCount) - + if(status == "Failure"){ + framework.CopyGameStats() + } ; End test and save results framework.EndTest(status) } ; Run the test -TestSaveGame() +TestSaveGame() \ No newline at end of file diff --git a/test_framework/ui_elements.txt b/test_framework/ui_elements.txt index b39207af69..f12c634a35 100644 --- a/test_framework/ui_elements.txt +++ b/test_framework/ui_elements.txt @@ -70,3 +70,4 @@ this.Creation.Obliterated := {x: 384, y: 416} this.SaveMenu.SaveSlot1 := {x: 1109, y: 362} this.LoadMenu.LoadSlot1 := {x: 1109, y: 362} this.LoadMenu.LoadAutosave := {x: 1127, y: 241} +this.Creation.Lamenters := {x: 546, y: 249} From eccf643838e0820333c039ddaaec990940a60fe6 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 09:49:07 +1000 Subject: [PATCH 07/17] testing options config file --- test_framework/CMTestingFramework.ahk | 24 ++++++++++++++++-------- test_framework/HowToUse.md | 2 ++ test_framework/logs/test_log.txt | 4 ---- 3 files changed, 18 insertions(+), 12 deletions(-) delete mode 100644 test_framework/logs/test_log.txt diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index 0b9b0f9080..4fda675102 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -8,15 +8,17 @@ class CMTestingFramework { ; Configuration __New() { - this.appPath := "D:\Documents\ChapterMaster\ChapterMaster.exe" - this.screenshotDir := A_ScriptDir "\screenshots\" - this.logDir := A_ScriptDir "\logs\" - this.resultDir := A_ScriptDir "\results\" + this.appPath := "D:\Documents\ChapterMaster\ChapterMaster.exe" ; Path to compiled ChapterMaster.exe file to run tests against + this.screenshotDir := A_ScriptDir "\screenshots\" ; Not in use yet + this.logDir := A_ScriptDir "\logs\" ; Stores all the framework logs and errors produced by the test runner + this.resultDir := A_ScriptDir "\results\" ; Stores all test results this.mouseSpeed := 8 ; 1-10, with 10 being slowest (more human-like) - this.errorLogPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\Logs" - this.savesPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\Save Files" - this.savesIniPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\saves.ini" - this.gameStatsIniPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster\gamestats.ini" + this.appDataPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster" ; The base path to CM user folder for saves, logs etc + this.errorLogPath := this.appDataPath . "\Logs" ; ChapterMaster logs and error files + this.savesPath := this.appDataPath . "\Save Files" ; ChapterMaster saves + this.savesIniPath := this.appDataPath . "\saves.ini" ; ChapterMaster save stats + this.gameStatsIniPath := this.appDataPath . "\gamestats.ini" ; Can be created by running the cheat code 'dumpstats' ingame, helpful for checking ingame values with the test runner + this.testSettingsPath := this.appDataPath . "\testing_options.ini" ; Configuration for this CMTestingFramework app, use to override certain settings per-developer/tester ; State tracking this.currentTest := "" @@ -29,6 +31,12 @@ class CMTestingFramework { DirCreate(this.logDir) DirCreate(this.resultDir) + + if(FileExist(this.testSettingsPath)){ + prev_path := this.appPath + this.appPath := IniRead(this.testSettingsPath, "Config", "app_path", prev_path) + } + ; UI Coordinate helpers this.UIMap := CMUIMap() diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index b26bfba5d7..07743637df 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -11,6 +11,8 @@ This document provides guidance for CM developers on how to create automated tes 2. **File Structure** - Place your test case scripts in the `tests` folder of the `test_framework` directory in the repo - Name your test scripts descriptively (e.g., `ObliteratedCustomStart.ahk`, `MenuChecks.ahk`) + - Test runner needs to be pointed at a compiled ChapterMaster.exe file, see `CMTestingFramework.appPath` + - use `testing_options.ini` in the %LocalAppData%/ChapterMaster folder to override the path of the ChapterMaster.exe file for your machine if different 3. **Game Settings** - The test runner assumes you are running in **Windowed 720p** in order for pixels to line up. diff --git a/test_framework/logs/test_log.txt b/test_framework/logs/test_log.txt deleted file mode 100644 index f05e10f8d4..0000000000 --- a/test_framework/logs/test_log.txt +++ /dev/null @@ -1,4 +0,0 @@ -00:01:03 - Application launched successfully -08:50:19 - Application launched successfully -08:54:25 - Application launched successfully -08:56:10 - Application launched successfully From bb14bc2804cfc428b02f6a40a82c6d90cb73ffa1 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 10:00:34 +1000 Subject: [PATCH 08/17] fix dup --- test_framework/CMTestingFramework.ahk | 1 - 1 file changed, 1 deletion(-) diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index 4fda675102..c6e4498feb 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -502,7 +502,6 @@ class CMUIMap { ; To populate new coords, use DiscoverUIElements.ahk, see HowToUse.md for instructions __New() { this.MainMenu.NewGame := { x: 629, y: 422 } - this.MainMenu.LoadGame := { x: -820, y: 187 } this.MainMenu.LoadGame := { x: 625, y: 452 } this.MainMenu.Options := { x: 634, y: 487 } this.MainMenu.Exit := { x: 634, y: 522 } From b35de009efc78836917854b96eb76b1208f08bd8 Mon Sep 17 00:00:00 2001 From: Luke Van Epen Date: Sun, 20 Apr 2025 10:12:44 +1000 Subject: [PATCH 09/17] Update test_framework/DiscoverUIElements.ahk Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- test_framework/DiscoverUIElements.ahk | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test_framework/DiscoverUIElements.ahk b/test_framework/DiscoverUIElements.ahk index 25667ffdbe..69ce4fb316 100644 --- a/test_framework/DiscoverUIElements.ahk +++ b/test_framework/DiscoverUIElements.ahk @@ -1,6 +1,5 @@ -; CM Application Test Case: Create New Customer Record +; UI Element Discovery Tool ; This test case demonstrates how to use the CMTestingFramework - #Requires AutoHotkey v2.0 #Include CMTestingFramework.ahk From 4c48cd9c3c063c79ab282a5f3d3b318ef57a1a23 Mon Sep 17 00:00:00 2001 From: Luke Van Epen Date: Sun, 20 Apr 2025 10:15:46 +1000 Subject: [PATCH 10/17] Update test_framework/tests/TestObliteratedStart.ahk Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- test_framework/tests/TestObliteratedStart.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_framework/tests/TestObliteratedStart.ahk b/test_framework/tests/TestObliteratedStart.ahk index 1a5fa68634..e53508208a 100644 --- a/test_framework/tests/TestObliteratedStart.ahk +++ b/test_framework/tests/TestObliteratedStart.ahk @@ -20,7 +20,7 @@ TestObliteratedStart() { framework.Wait(7000) .ClickElement("MainMenu.NewGame") ; Click on "New Game" in the main menu .Wait(8500) - .ClickElement("Creation.CreateCustom") ; Click Dark Angels + .ClickElement("Creation.CreateCustom") ; Click Create Custom Chapter .Wait(3000) .ClickElement("Creation.Homeworld") .ClickElement("Creation.PurityUp") From 273bbb371fcfcb8afb96766d7c5174bb2db57b8c Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 10:24:14 +1000 Subject: [PATCH 11/17] appease the bot --- test_framework/RunAllTests.ahk | 1 + 1 file changed, 1 insertion(+) diff --git a/test_framework/RunAllTests.ahk b/test_framework/RunAllTests.ahk index 8469ad7988..f8dc441fed 100644 --- a/test_framework/RunAllTests.ahk +++ b/test_framework/RunAllTests.ahk @@ -34,6 +34,7 @@ RunAllTests() { ; Display progress MsgBox("Found " . testFiles.Length . " test files. Running tests in sequence...") + DirCreate(scriptDir . "\tests\results") ; Create a log file for results logFile := scriptDir . "\tests\results\TestSuiteResults_" . FormatTime(A_Now, "yyyyMMdd_HHmmss") . ".log" FileAppend("CM Test Suite Run - " . FormatTime(A_Now, "yyyy-MM-dd HH:mm:ss") . "`n`n", logFile) From 20439047637bdc42903efbf90e5c2cc3e33e79a7 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 11:01:47 +1000 Subject: [PATCH 12/17] keypress delay --- test_framework/CMTestingFramework.ahk | 10 ++++++---- test_framework/HowToUse.md | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/test_framework/CMTestingFramework.ahk b/test_framework/CMTestingFramework.ahk index c6e4498feb..159a25197b 100644 --- a/test_framework/CMTestingFramework.ahk +++ b/test_framework/CMTestingFramework.ahk @@ -12,14 +12,16 @@ class CMTestingFramework { this.screenshotDir := A_ScriptDir "\screenshots\" ; Not in use yet this.logDir := A_ScriptDir "\logs\" ; Stores all the framework logs and errors produced by the test runner this.resultDir := A_ScriptDir "\results\" ; Stores all test results - this.mouseSpeed := 8 ; 1-10, with 10 being slowest (more human-like) this.appDataPath := "C:\Users\" . A_UserName . "\AppData\Local\ChapterMaster" ; The base path to CM user folder for saves, logs etc this.errorLogPath := this.appDataPath . "\Logs" ; ChapterMaster logs and error files this.savesPath := this.appDataPath . "\Save Files" ; ChapterMaster saves this.savesIniPath := this.appDataPath . "\saves.ini" ; ChapterMaster save stats this.gameStatsIniPath := this.appDataPath . "\gamestats.ini" ; Can be created by running the cheat code 'dumpstats' ingame, helpful for checking ingame values with the test runner this.testSettingsPath := this.appDataPath . "\testing_options.ini" ; Configuration for this CMTestingFramework app, use to override certain settings per-developer/tester - + + this.mouseSpeed := 8 ; 1-10, with 10 being slowest (more human-like) + this.keypressDelay := 50 ; number of milliseconds between keypresses when typing with .Send(text). + ; State tracking this.currentTest := "" this.testSteps := [] @@ -190,8 +192,8 @@ class CMTestingFramework { ; Send text (typing) SendText(text) { - SetKeyDelay(100) - Send(text) + SetKeyDelay(this.keypressDelay) + SendEvent(text) this.LogStep("Text sent: " . text) if (!this.CheckForCrash()) diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index 07743637df..b0a7887754 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -78,8 +78,9 @@ Here are the core methods to use in your test cases: - `framework.CloseApp()` - Exit CM but keep the test running ### Mouse Interaction -- `framework.Click(x, y)` - Click at specific coordinates -- `framework.DoubleClick(x, y)` - Double-click at coordinates +The `Click`, `DoubleClick`, and `ClickElement` functions have a small builtin wait timer so that you dont have to call Wait explicitly after ever click if you dont actually have to wait. +- `framework.Click(x, y, button="left")` - Moves Mouse to and left-Clicks at specific coordinates. +- `framework.DoubleClick(x, y, button="left")` - Double-click at coordinates - `framework.MoveMouse(x, y)` - Move mouse to coordinates - `framework.DragMouse(x1, y1, x2, y2)` - Click and drag from one point to another - `framework.ClickElement("Section.ElementName")` - Click on a named UI element From 1e87cb8dfe6ba64f508e6eb0317d88813b796265 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 11:12:24 +1000 Subject: [PATCH 13/17] docs update --- test_framework/HowToUse.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index b0a7887754..a23b48f27a 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -10,9 +10,11 @@ This document provides guidance for CM developers on how to create automated tes 2. **File Structure** - Place your test case scripts in the `tests` folder of the `test_framework` directory in the repo - - Name your test scripts descriptively (e.g., `ObliteratedCustomStart.ahk`, `MenuChecks.ahk`) + - Name your test scripts descriptively (e.g., `TestObliteratedCustomStart.ahk`, `TestMenuChecks.ahk`) + - Tests file names need to start with `Test` in order to be picked up by the test suite runner that runs all tests together + - Test filenames and test names and ahk function names dont have to line up, but its easier for everyone if they do - Test runner needs to be pointed at a compiled ChapterMaster.exe file, see `CMTestingFramework.appPath` - - use `testing_options.ini` in the %LocalAppData%/ChapterMaster folder to override the path of the ChapterMaster.exe file for your machine if different + - use `testing_options.ini` in the %LocalAppData%/ChapterMaster folder to override the path of the ChapterMaster.exe file for your machine if different 3. **Game Settings** - The test runner assumes you are running in **Windowed 720p** in order for pixels to line up. @@ -30,6 +32,12 @@ This document provides guidance for CM developers on how to create automated tes - AHK native method - In the file explorer, right-click your test case .ahk file and select "Run Script" (should be first option) - Better for running tests if you're not editing them + - Results are stored in `test_framework/tests/results` and should be sorted by date + - Errors caught by the test framework are stored in `test_framework/tests/logs/error_log.txt` which includes both game crashes and errors with the .ahk tests themselves + +5. **Running all tests as a Suite** + - Alongside CMTestingFramework.ahk should be a file **RunAllTests.ahk**. Running this file using the method above will execute all test files in the `tests` folder whose filenames start with "Test" + - Proper stats on test suite pass/fail/error are wip ## Creating a Basic Test Case From 6354ae2889e200641dba1c6a0c7cf8c521600e3a Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 11:13:39 +1000 Subject: [PATCH 14/17] typo --- test_framework/HowToUse.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index a23b48f27a..bcae94c248 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -86,7 +86,7 @@ Here are the core methods to use in your test cases: - `framework.CloseApp()` - Exit CM but keep the test running ### Mouse Interaction -The `Click`, `DoubleClick`, and `ClickElement` functions have a small builtin wait timer so that you dont have to call Wait explicitly after ever click if you dont actually have to wait. +The `Click`, `DoubleClick`, and `ClickElement` functions have a small builtin wait timer so that you dont have to call Wait explicitly after every click if you dont actually have to wait for something to happen before moving on. - `framework.Click(x, y, button="left")` - Moves Mouse to and left-Clicks at specific coordinates. - `framework.DoubleClick(x, y, button="left")` - Double-click at coordinates - `framework.MoveMouse(x, y)` - Move mouse to coordinates From 00b8cb3deb63048ab75d2631e971402fb0bd6e1b Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 11:22:26 +1000 Subject: [PATCH 15/17] more docs updates --- test_framework/HowToUse.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index bcae94c248..d76e5990e1 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -83,10 +83,16 @@ Here are the core methods to use in your test cases: - `framework.StartTest("TestName")` - Initialize a test with a name - `framework.LaunchApp()` - Launch the CM application - `framework.EndTest()` - Complete the test and save results -- `framework.CloseApp()` - Exit CM but keep the test running +- `framework.CloseApp()` - Exit CM but keep the test running until `EndTest()` is called ### Mouse Interaction The `Click`, `DoubleClick`, and `ClickElement` functions have a small builtin wait timer so that you dont have to call Wait explicitly after every click if you dont actually have to wait for something to happen before moving on. +The property `mouseSpeed` on CMTestingFramework controls how fast the mouse moves. Its default setting is pretty slow, high speeds can cause clicks to not register properly sometimes. +To edit the mouse speed for a single test, you can use the below snippet: +```autohotkey +framework := CMTestingFramework() +framework.mouseSpeed := 2 ; 1 - 10, 10 being slowest +``` - `framework.Click(x, y, button="left")` - Moves Mouse to and left-Clicks at specific coordinates. - `framework.DoubleClick(x, y, button="left")` - Double-click at coordinates - `framework.MoveMouse(x, y)` - Move mouse to coordinates @@ -95,6 +101,14 @@ The `Click`, `DoubleClick`, and `ClickElement` functions have a small builtin wa - `framework.MoveToElement("Section.ElementName")` - Move mouse to a named UI element ### Keyboard Interaction +The property `keypressDelay` on CMTestingFramework controls how fast each keystroke is sent. Its default setting is pretty average, at 50 milliseconds between keypresses. You can increase or decrease this per test. +`keypressDelay` only affects `SendText()` +To edit the keypress speed for a single test, you can use the below snippet: +```autohotkey +framework := CMTestingFramework() +framework.keypressDelay := 10 ; number of milliseconds between keypresses when using .SendText() +``` + - `framework.SendText("text")` - Type text - `framework.SendCombo("{Ctrl down}{s}{Ctrl up}")` - Send key combinations - `framework.KeyDown("key")` and `framework.KeyUp("key")` - Press and release keys @@ -102,6 +116,8 @@ The `Click`, `DoubleClick`, and `ClickElement` functions have a small builtin wa ### Premade Step Sequences - `framework.StartGameAs("ChapterName")` - Opens game, clicks New Game, selects ChapterName, Skips through Creation screen, dismisses intro sprawl - `framework.SaveToSlot("1")` - Opens the ingame menu and saves the game to the slot specified. Only works for slots 1 - 3 +- `framework.DumpGameStats()` - triggers an ingame cheatcode to export some game values to a file called `gamestats.ini` which can be read from to check that ingame values are matching expectations. +- `framework.CopyGameStats()` - copies the `gamestats.ini` file into the test results folder, which can be handy if you want to manually inspect values after running a test ### Wait and Timing - `framework.Wait(milliseconds)` - Pause execution for specified time in ms @@ -113,8 +129,10 @@ The `Click`, `DoubleClick`, and `ClickElement` functions have a small builtin wa ### Error Handling - `framework.CheckForCrash()` - Detect if application crashed +- `framework.CheckForCrashDialog()` - Automatically called by `CheckForCrash()`, this method specifically checks if the Crash popup window was created. - `framework.RestartApp()` - Restart the application after a crash + ## Using Named UI Elements Instead of using raw coordinates, you can reference UI elements by name: From 8e6b9a6407651b0e38b16806438e19d2bcc5a345 Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 11:27:54 +1000 Subject: [PATCH 16/17] update discoveruielement instructions in docs --- test_framework/HowToUse.md | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index d76e5990e1..dde3132985 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -181,12 +181,11 @@ framework.StartTest("StartGameWithNamedElements") The framework includes a tool to help identify and record UI element coordinates: 1. **Launch the Discovery Tool** - ```autohotkey - DiscoverUIElements() - ``` + - Open `DiscoverUIElements.ahk` and run with AHK++ or just run the script from windows explorer 2. **Using the Tool** - Position your mouse over a UI element in the CM application + - Make sure the window is in focus, you can click in the game window to make sure it is - Press F7 to capture the current mouse coordinates - Enter a Section Name (e.g., "MainMenu") - Enter an Element Name (e.g., "NewGameButton") @@ -196,24 +195,11 @@ The framework includes a tool to help identify and record UI element coordinates - The tool saves coordinates to `ui_elements.txt` in your script directory - Each entry is formatted for easy pasting into the `CMUIMap` class -4. **Typical Workflow** - ```autohotkey - ; Example script to discover UI elements - #Requires AutoHotkey v2.0 - #Include ..\CMTestingFramework.ahk - - ; Launch CM - framework := CMTestingFramework() - framework.LaunchApp() - - ; Start the discovery tool - DiscoverUIElements() - ``` - -5. **After Capturing Elements** +4. **After Capturing Elements** - Copy the contents of `ui_elements.txt` - Add them to the `CMUIMap` class in the appropriate sections - Use your newly mapped elements in tests with `ClickElement()` and `MoveToElement()` + - If you need to create a new Section, you need to update `CMTestingFramework.DiscoverUIElements` method and `CMUIMap` class in CMTestingFramework.ahk to add the section otherwise you will get .ahk compiler errors when running tests ### Manual Coordinate Finding From d74cd492f5620e85422b290d7ad34b9522fbc3ce Mon Sep 17 00:00:00 2001 From: Van Weapon Date: Sun, 20 Apr 2025 11:32:53 +1000 Subject: [PATCH 17/17] okay final docs update for reals this time --- test_framework/HowToUse.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/test_framework/HowToUse.md b/test_framework/HowToUse.md index dde3132985..5c842efe79 100644 --- a/test_framework/HowToUse.md +++ b/test_framework/HowToUse.md @@ -274,25 +274,23 @@ TestStartGame() 4. **Add Delays**: Include sufficient wait times between actions (use `framework.Wait()`) -5. **Document with Screenshots**: Take screenshots at key points in your test +5. **Handle Errors**: The framework will report hard errors and crashes, but you can use `framework.LogStep()` and `framework.LogError()` to write your own output to the test file at any point during a test to clarify what went right/wrong -6. **Handle Errors**: Always check for and handle possible application crashes +6. **Organize by Workflow**: Create separate test files for different functional areas -7. **Organize by Workflow**: Create separate test files for different functional areas +7. **Comment Your Code**: Include clear comments explaining the purpose of each step -8. **Comment Your Code**: Include clear comments explaining the purpose of each step +8. **Use Consistent Naming**: Name your tests and files with clear, descriptive names -9. **Use Consistent Naming**: Name your tests and files with clear, descriptive names - -10. **Test One Thing**: Each test should focus on a single feature or workflow +9. **Test One Thing**: Each test should focus on a single feature or workflow ## Troubleshooting -- **Coordinate Issues**: If clicks aren't hitting the right spots, verify coordinates in Window Spy or use the DiscoverUIElements tool -- **Timing Problems**: Increase wait times if actions seem to execute before the app is ready -- **Application Not Found**: Check the application path in CMTestingFramework.ahk +- **Coordinate Issues**: If clicks aren't hitting the right spots, first make sure you're running in **Windowed 720p**, see Getting Started section 3. Verify coordinates in Window Spy or use the DiscoverUIElements tool if still having issues +- **Timing Problems**: Increase wait times if actions seem to execute before the app is ready. You can slow down the mouse speed and keyboard typing speed too if needed on a per-test basis. See Mouse Interaction and Keyboard Interaction sections +- **Application Not Found**: Check the application path in CMTestingFramework.ahk. You can overwrite this value with an `testing_options.ini` file in you %LocalAppData%/ChapterMaster folder - **Element Not Found Error**: Verify the element path is correct (e.g., "MainMenu.NewGame") ## Getting Help -If you encounter issues creating test cases, contact the testing team for assistance. +If you encounter issues creating test cases, contact @VanWeapon in the discord.