Skip to main content

Mock xdg-desktop-portal FileChooser D-Bus service for automating native file dialogs in headless/CI GUI and browser tests.

Project description

filechooser-portal-mock

A mock xdg-desktop-portal FileChooser D-Bus service for automating native file dialogs in headless/CI GUI and browser tests.

When a modern application (Chromium, any GTK app) needs to open a file dialog, it does not draw the dialog itself — it makes a D-Bus call to org.freedesktop.portal.Desktop. In a headless or CI environment there is no desktop portal to answer, so the application hangs or times out.

filechooser-portal-mock impersonates that portal. It answers the OpenFile/SaveFile call and emits a Response signal carrying a path you choose, so the application proceeds exactly as if a human had picked it — with no GUI, under Xvfb, in CI.

The flagship use case is loading an unpacked Chrome extension in an automated test (chrome://extensions → "Load unpacked"), but it works for any open/save file dialog routed through the portal.

   app under test                 filechooser-portal-mock
  ┌───────────────┐   OpenFile()  ┌───────────────────────┐
  │  Chrome /      │ ────────────► │  org.freedesktop      │
  │  GTK app       │               │  .portal.Desktop      │
  │               │ ◄──────────── │  (FileChooser mock)   │
  └───────────────┘   Response     └───────────────────────┘
        "user picked file:///path/you/configured"

Why a portal mock (and not clicking the dialog)?

The file picker is a separate, privileged process; you cannot reliably screen-scrape or xdotool it across desktops and versions. Intercepting the request at the D-Bus layer is robust and deterministic. See docs/protocol.md for the protocol details and the hard-won pydbus-vs-dbus-python findings that make this work in subprocess contexts.

Installation

This package binds to your system's GLib via PyGObject and pydbus, which are built against system libraries. Install the system dependencies first.

Debian / Ubuntu:

sudo apt install -y dbus python3-dev pkg-config \
    libcairo2-dev libgirepository-2.0-dev libglib2.0-dev
pip install filechooser-portal-mock

Older distributions provide girepository under libgirepository1.0-dev.

Fedora:

sudo dnf install -y dbus python3-devel pkgconf-pkg-config \
    cairo-devel gobject-introspection-devel glib2-devel
pip install filechooser-portal-mock

If your distribution packages PyGObject and pydbus (e.g. python3-gi, python3-pydbus), you can install those from the system instead of building them from source.

A virtual X server (Xvfb) is only needed by the application under test, not by this tool. The portal itself needs no display.

Quick start

Command line (works from any language / test harness)

# Start a portal on a private bus that answers every file dialog with a path:
filechooser-portal --isolated-bus /abs/path/to/extension
# -> prints:  DBUS_SESSION_BUS_ADDRESS=unix:abstract=...
#             FILECHOOSER_PORTAL_READY

# Other answers:
filechooser-portal --cancel      # simulate the user cancelling
filechooser-portal --error       # simulate a failed request

Point your application at the printed bus address (DBUS_SESSION_BUS_ADDRESS=...) and it will receive that path when it opens a file dialog. The FILECHOOSER_PORTAL_READY line lets you wait for readiness instead of sleeping.

Python (context manager)

from filechooser_portal import serve

with serve("/abs/path/to/extension") as portal:
    launch_app(env=portal.env)   # portal.env has DBUS_SESSION_BUS_ADDRESS set
# portal (and its private bus) are torn down on exit

pytest fixture

Installing the package registers a file_chooser_portal fixture automatically:

def test_load_unpacked(file_chooser_portal):
    portal = file_chooser_portal("/abs/path/to/extension")
    launch_chrome(env=portal.env)
    ...

Loading an unpacked Chrome extension (Selenium)

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from filechooser_portal import serve

with serve("/abs/path/to/extension") as portal:
    options = Options()
    options.add_argument("--ozone-platform=x11")     # run under Xvfb, not headless
    # Run Chrome on the same private bus as the portal:
    import os
    os.environ["DBUS_SESSION_BUS_ADDRESS"] = portal.bus_address
    driver = webdriver.Chrome(options=options)

    driver.get("chrome://extensions")
    # enable developer mode, then click "Load unpacked" via JS; the portal
    # answers the directory chooser with your extension path.

A complete, runnable version is in examples/selenium_load_unpacked.py.

Dynamic behavior (write your own)

The CLI is intentionally static. For per-request logic, subclass the portal and override the hooks, then run it:

from filechooser_portal import FileChooserPortal, Response, run_portal

class MyPortal(FileChooserPortal):
    def on_open_file(self, parent_window, title, options):
        if "extension" in title.lower():
            return Response.select("/abs/path/to/extension")
        return Response.cancel()

run_portal(MyPortal())   # uses $DBUS_SESSION_BUS_ADDRESS

You can also pass a callable as the answer:

portal = FileChooserPortal(
    lambda method, parent, title, options: Response.select("/abs/path")
)

See examples/dynamic_portal.py.

What it implements

  • org.freedesktop.portal.FileChooser: OpenFile, SaveFile, version
  • org.freedesktop.portal.Settings: Read, ReadAll (minimal — Chrome probes color-scheme during portal availability checks)
  • org.freedesktop.portal.Request: the Response signal and Close

Response codes follow the portal spec: 0 success, 1 cancelled, 2 ended.

Limitations

  • Linux / D-Bus only.
  • One configured answer (or one callable/subclass) drives all requests; there is no built-in IPC to change the answer of an already-running CLI process — use the Python API for dynamic behavior.
  • Implements the FileChooser portal, not the entire portal surface.

Versioning and releases

Versions are derived from git via hatch-vcs (git describe). Every push to main is automatically published to PyPI as a post-release (e.g. 0.1.post3 for the third commit after v0.1), so pip install filechooser-portal-mock always installs the latest main build. Pushing a vX.Y tag publishes a clean release (e.g. 0.1).

Development

git clone https://github.com/mithro/filechooser-portal-mock
cd filechooser-portal-mock
uv run --extra test pytest -v

The core tests act as a D-Bus client against the portal on a private bus and need no display.

License

Apache-2.0. Originally developed within the youtube-shortcuts project and extracted into a standalone tool.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

filechooser_portal_mock-0.1.post1.tar.gz (26.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

filechooser_portal_mock-0.1.post1-py3-none-any.whl (22.7 kB view details)

Uploaded Python 3

File details

Details for the file filechooser_portal_mock-0.1.post1.tar.gz.

File metadata

File hashes

Hashes for filechooser_portal_mock-0.1.post1.tar.gz
Algorithm Hash digest
SHA256 a9e6136523e69361f33444ffe23f1b8645fbf96b59ffd61494f9feba1822a4ca
MD5 b4fc113a182b905d448f7da7f0fa5fad
BLAKE2b-256 149156cd0a0c59118760215988da6b7af80a566df611dc36eafa324912d962a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for filechooser_portal_mock-0.1.post1.tar.gz:

Publisher: release.yml on mithro/filechooser-portal-mock

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file filechooser_portal_mock-0.1.post1-py3-none-any.whl.

File metadata

File hashes

Hashes for filechooser_portal_mock-0.1.post1-py3-none-any.whl
Algorithm Hash digest
SHA256 55a23c2799cc63bb6e315471dcc3b06554230d6b42c45bced464fdc97ca1125d
MD5 f29a653e8243397f593ecdfac86f79f6
BLAKE2b-256 bc57ef92a1672c4c0bc382cf6fa6bede22e6944224efc9997fbf3e126ef14be3

See more details on using hashes here.

Provenance

The following attestation bundles were made for filechooser_portal_mock-0.1.post1-py3-none-any.whl:

Publisher: release.yml on mithro/filechooser-portal-mock

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page