├── .github ├── FUNDING.yml └── workflows │ ├── linux_ci.yml │ └── mac_ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── functions.rs ├── lib.rs ├── macros.rs └── observer.rs └── tests ├── basic.rs └── observer.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: gtk-rs 2 | -------------------------------------------------------------------------------- /.github/workflows/linux_ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | 4 | name: Linux CI 5 | 6 | jobs: 7 | check: 8 | name: Compile and Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install gtk3 and dependencies. 13 | run: | 14 | sudo apt update 15 | sudo apt install \ 16 | libgtk-3-dev \ 17 | libxdo-dev 18 | 19 | - name: Install Rust 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Check Code 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: check 30 | 31 | - name: Install Clippy 32 | run: rustup component add clippy 33 | 34 | - name: Run Clippy 35 | run: cargo clippy --all-targets -- -D warnings -A unknown-lints 36 | 37 | - name: Run headless test 38 | uses: GabrielBB/xvfb-action@v1 39 | with: 40 | run: cargo test -- --test-threads=1 41 | -------------------------------------------------------------------------------- /.github/workflows/mac_ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | 4 | name: MacOS CI 5 | 6 | jobs: 7 | check: 8 | name: Compile and Test 9 | runs-on: macOS-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install gtk3 and dependencies. 13 | run: brew install gtk+3 14 | 15 | - name: Install Rust 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | 22 | - name: Check Code 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: check 26 | 27 | - name: Install Clippy 28 | run: rustup component add clippy 29 | 30 | - name: Run Clippy 31 | run: cargo clippy --all-targets -- -D warnings -A unknown-lints 32 | 33 | - name: Run Tests 34 | uses: actions-rs/cargo@v1 35 | with: 36 | command: test 37 | args: -- --test-threads=1 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gtk-test" 3 | version = "0.18.0" 4 | authors = ["The Gtk-rs Project Developers"] 5 | license = "MIT" 6 | description = "Crate to test GTK UIs" 7 | categories = ["api-bindings", "gui", "test"] 8 | homepage = "https://gtk-rs.org/" 9 | repository = "https://github.com/gtk-rs/gtk-test" 10 | keywords = ["gtk", "gtk-rs", "GUI", "test", "gnome"] 11 | documentation = "https://docs.rs/gtk-test" 12 | edition = "2021" 13 | 14 | [dependencies] 15 | enigo = "^0.0.14" 16 | gtk = "0.18" 17 | 18 | [[test]] 19 | harness = false 20 | name = "basic" 21 | 22 | [[test]] 23 | harness = false 24 | name = "observer" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Guillaume Gomez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gtk-test 2 | 3 | This projects allows you to test your GTK's applications UI. It has to be used with [gtk-rs](https://gtk-rs.org) crates. 4 | 5 | ## How does it work? 6 | 7 | It's quite simple actually (even though you have to perform a few more things on OSX to make it work as expected...) : 8 | 9 | ```rust 10 | gtk::init().unwrap(); // You need to init GTK otherwise it'll just crash... 11 | ``` 12 | 13 | Then you build your UI as you would in normal time (using `Glade` or by hand). Only one thing actually changes: you must not call `gtk::main`! 14 | 15 | Once you have built your UI, just call the `gtk_test` macros/functions to test it. Just one note about this though: sometimes, you need to let time for GTK to process some events. For example, if you clicked on a button and you have an associated action to it, it's more careful to use `gtk_test::wait`. 16 | 17 | Another recommended thing is to give focus to the window in case you have to interact with it (to click on a button or to input some text...): 18 | 19 | ```rust 20 | let w = gtk::Window::new(); 21 | // ... 22 | w.activate_focus(); 23 | ``` 24 | 25 | ### General setup 26 | 27 | When running test, you need to specify that you only want **ONE** thread. To do so: 28 | 29 | ```bash 30 | cargo test -- --test-threads=1 31 | ``` 32 | 33 | Otherwise, GTK contexts might conflict into each others. 34 | 35 | ### Specific setup for OSX 36 | 37 | A few more things have to be done on OSX to make this work. First, you won't be able to add the `#[test]` attribute to your functions, it doesn't work. Instead, you have to write your test just like you would write a normal binary (so with a `main` function as entry point). 38 | 39 | A short example (you can find the full version in the `tests` folder of this repository): 40 | 41 | ```rust 42 | fn main() { 43 | let (w, l, b) = init_ui(); 44 | 45 | assert_text!(l, "Test"); 46 | w.activate_focus(); 47 | gtk_test::click(&b); 48 | gtk_test::wait(1000); // to be sure that GTK has updated the label's text 49 | assert_text!(l, "Clicked"); 50 | } 51 | ``` 52 | 53 | Then you need to add into your `Cargo.toml` file: 54 | 55 | ```toml 56 | [[test]] 57 | harness = false # This is the important line! 58 | name = "basic" 59 | ``` 60 | 61 | It allows your test to be run as a "normal" binary. 62 | 63 | ### Example? 64 | 65 | You can find a few in the tests folder. Just copy/paste it and you're good to go (don't forget to add the missing pieces in your `Cargo.toml` file!). 66 | 67 | ### Using it on CI? 68 | 69 | It's actually possible (only tested for GitHub Actions though). You "just" need a display server. Here's what you have to add in your CI configuration file to make it work: 70 | 71 | Install the following packages: 72 | 73 | * libgtk-3-dev 74 | * libxdo-dev 75 | 76 | Then run the application in a virtual display environment using [xvfb-action.](https://github.com/GabrielBB/xvfb-action) For example: 77 | 78 | ``` 79 | - name: Run headless test 80 | uses: GabrielBB/xvfb-action@v1 81 | with: 82 | run: cargo test -- --test-threads=1 83 | ``` 84 | 85 | Take a look at our `.github/workflows/linux_ci.yml` file to see how we set things up. 86 | -------------------------------------------------------------------------------- /src/functions.rs: -------------------------------------------------------------------------------- 1 | use enigo::{self, Enigo, KeyboardControllable, MouseButton, MouseControllable}; 2 | use gtk::gdk::keys::constants as key; 3 | use gtk::gdk::keys::Key; 4 | use gtk::glib::{Cast, ControlFlow, IsA, Object, Propagation, StaticType}; 5 | use gtk::prelude::{BinExt, ButtonExt, ContainerExt, EditableExt, ToolButtonExt, WidgetExt}; 6 | use gtk::{Bin, Button, Container, Entry, ToolButton, Widget, Window}; 7 | 8 | use crate::observer_new; 9 | 10 | /// Simulate a click on a widget. 11 | /// 12 | /// ## Warning! 13 | /// 14 | /// Please note that the click will "fail" if the window isn't on top of all other windows (this 15 | /// is a common issue on OSX). Don't forget to bring the button's window on top by using: 16 | /// 17 | /// ```ignore 18 | /// window.activate_focus(); 19 | /// ``` 20 | /// 21 | /// Example: 22 | /// 23 | /// ``` 24 | /// extern crate gtk; 25 | /// #[macro_use] 26 | /// extern crate gtk_test; 27 | /// 28 | /// use gtk::{Button, prelude::ButtonExt}; 29 | /// 30 | /// # fn main() { 31 | /// gtk::init().expect("GTK init failed"); 32 | /// let but = Button::new(); 33 | /// but.connect_clicked(|_| { 34 | /// println!("clicked"); 35 | /// }); 36 | /// gtk_test::click(&but); 37 | /// # } 38 | /// ``` 39 | pub fn click + IsA + WidgetExt + IsA>(widget: &W) { 40 | wait_for_draw(widget, || { 41 | let observer = if let Ok(tool_button) = widget.clone().dynamic_cast::() { 42 | observer_new!(tool_button, connect_clicked, |_|) 43 | } else if let Ok(tool_button) = widget.clone().dynamic_cast::