Skip to content

Testing OpenGL viewports

Many Qt applications render their central view with OpenGL: CAD viewports, map canvases, 3D previews, game editors. veriCue drives them out of the box, because it never needs to understand what the viewport draws. The server synthesizes ordinary Qt input events (QMouseEvent, QWheelEvent, QTouchEvent) and sends them to the widget; your application's own camera code reacts exactly as it would to a real user.

Anything that is a QOpenGLWidget (or any QWidget subclass hosting a GL context, or a QQuickItem) is resolvable by path like every other widget:

python
VIEWPORT = "MainWindow/glViewport"

Driving the camera

Orbit, pan and zoom are just input sequences:

python
# Orbit: left-drag across the viewport. drag() sends a press, ten
# interpolated mouse moves, and a release - like a real hand.
await client.drag(VIEWPORT, 100, 100, 220, 160)

# Pan: most apps map it to middle-drag or a modifier.
await client.drag(VIEWPORT, 100, 100, 200, 150, button="middle")
await client.drag(VIEWPORT, 100, 100, 200, 150, modifiers=["shift"])

# Zoom: dy is in wheel notches (the server converts to angle delta)...
await client.scroll(VIEWPORT, dy=2)

# ...or a two-finger pinch. end > start zooms in; `rotation` (degrees)
# rotates the fingers while pinching.
await client.pinch(VIEWPORT, 100, 220, rotation=30.0)

Free-form multi-touch gestures can be composed point-by-point with multi_touch. Note: the widget must opt into touch with setAttribute(Qt::WA_AcceptTouchEvents), same as for real touchscreens.

Asserting on camera state, not pixels

The scene inside a GL viewport is pixels, not QObjects - there is no object tree to query inside the canvas. The robust pattern is to expose the camera (or scene) state as Q_PROPERTY with NOTIFY on the viewport widget:

cpp
class GLViewport : public QOpenGLWidget {
    Q_OBJECT
    Q_PROPERTY(qreal yaw READ yaw WRITE setYaw NOTIFY cameraChanged)
    Q_PROPERTY(qreal distance READ distance WRITE setDistance NOTIFY cameraChanged)
    // ...
};

Then tests read and synchronize on it like on any other property:

python
before = await client.get_properties(VIEWPORT, ["yaw", "distance"])

await client.drag(VIEWPORT, 100, 100, 190, 100)

result = await client.wait_for_property(VIEWPORT, "yaw",
                                        before["yaw"] + 45.0, timeout=2000)
assert result["matched"]

This keeps assertions exact and GPU-independent: input handling and property updates go through Qt's event and meta-object machinery, not through the GL pipeline, so they do not need a real GL context.

How far that gets you headless depends on the application. veriCue's own gl_app suite runs under QT_QPA_PLATFORM=offscreen, but a widget or scene that insists on a usable GL context at startup will not come up that way - the offscreen plugin cannot obtain one on a machine with no display server. If your app fails to start headless, run it under xvfb-run with the normal xcb plugin (optionally with LIBGL_ALWAYS_SOFTWARE=1); see the CI guide.

Pixel-level verification still works when you want it: screenshot and screenshot_compare capture the composited widget (QWidget::grab() reads back the GL framebuffer). For GL content prefer a slightly higher threshold, since GPU drivers may rasterize with minor differences across machines.

Working example

The public vericue-examples repository ships gl_app - a spinning-cube viewport with an orbit/pan/zoom camera exposed as properties. veriCue's own integration suite drives it with drag, scroll and pinch and asserts on the camera state. Use it as a template for your own viewport.

Limitations

  • Objects inside the GL scene are not individually addressable - interaction is coordinate-based on the viewport widget. If you need to click scene entities by name, expose a pick API from the app (for example an invoke_method-callable slot that returns the screen position of an entity).
  • A bare QOpenGLWindow/QWindow without a widget container is not currently resolvable; host the GL surface in a QOpenGLWidget or wrap it with QWidget::createWindowContainer inside a widget hierarchy.

Released under a commercial licence. Privacy · Terms