Writing Plugins
Plugin structure
A plugin is a TOML file with this structure:
[plugin]name = "my-plugin"plugin_version = "0.1.0"vanya_version = "0.1"language = "python"file_extension = ".py"
[types.my_type]allowed_children = []modifiers = { key = "quoted" }template = "ctx = setup()"
[[types.my_type.patterns]]name = "do-something"pattern = "do something with <value:quoted>"template = "do_something({{ value }})"Loading custom plugins
There are two ways to load custom plugins:
Option 1: —plugin-dir flag
Point vanya to a directory containing plugin TOML files:
vanya check --plugin-dir ./plugins mytest.vanyavanya build --plugin-dir ./plugins mytest.vanya -o out/All .toml files in the directory (except vanya.config.toml) will be loaded as plugins.
Option 2: vanya.config.toml
Create a vanya.config.toml file in your project:
[project]plugins = [ "plugins/my-plugin.toml", "plugins/another-plugin.toml",]Then reference it with --config:
vanya check --config vanya.config.toml mytest.vanyavanya build --config vanya.config.toml mytest.vanya -o out/Plugin paths in the config file are relative to the config file’s location.
Using your plugin
Once loaded, reference your plugin in .vanya files:
Vanya test scenario (vanya-lang.org/v0.1)Script title: "Test with custom plugin"
Within the MyContext (plugin: my-plugin, type: my_type) do something with "value"Sections
[plugin]
Plugin metadata:
[plugin]name = "my-plugin" # Required: unique plugin identifierplugin_version = "0.1.0" # Optional: plugin version (default: "0.1.0")vanya_version = "0.1" # Optional: vanya compatibility version (default: "0.1")language = "python" # Optional: target language (default: "python")file_extension = ".py" # Optional: output file extension (default: ".py")[types.*]
Define available types within the plugin:
[types.browser]allowed_children = ["element", "failure_block"]modifiers = { url = "quoted", headless = "quoted" }template = """ctx = playwright.chromium.launch().new_page(){% if url %}ctx.goto("{{ url }}"){% endif %}"""
[[types.browser.patterns]]name = "click"pattern = "click <target:unquoted>"modifiers = { timeout = "quoted" }template = 'ctx.locator("{{ target }}").click()'Type fields:
allowed_children- List of type names that can be nested within this typemodifiers- Map of modifier names to their value types ("quoted"or"unquoted")template- Jinja2 template for the type’s initialization codepatterns- Array of action patterns (use[[types.*.patterns]]syntax) or the string"passthrough"
[[types.*.patterns]]
Define action patterns using TOML array-of-tables syntax:
[[types.browser.patterns]]name = "go-to"pattern = "go to <url:quoted>"modifiers = { timeout = "quoted" }template = 'ctx.goto("{{ url }}")'
[[types.browser.patterns]]name = "click"pattern = "click <target:unquoted>"template = 'ctx.locator("{{ target }}").click()'
[[types.browser.patterns]]name = "expect-failure"pattern = "expect failure"modifiers = { error = "quoted", capture = "quoted" }returns = "failure_block"template = '''{% if error %}with pytest.raises({{ error }}): {{ children | indent(4) }}{% else %}with pytest.raises(Exception): {{ children | indent(4) }}{% endif %}'''Pattern fields:
name- Unique identifier for the pattern within the typepattern- The pattern string with slot placeholdersmodifiers- Optional map of modifier names to value typestemplate- Jinja2 template for the generated codereturns- Optional type name for actions that create nested blocks
Passthrough types
For types that pass through child actions without modification:
[types.failure_block]patterns = "passthrough"Slot types
Slots capture values from action statements using <name:type> syntax:
quoted- String literal (with quotes), e.g.,"hello"unquoted- Identifier without quotes, e.g.,buttona|b|c- Union of keywords, matches one of the listed values
Examples:
pattern = "go to <url:quoted>" # Matches: go to "http://example.com"pattern = "click <target:unquoted>" # Matches: click submit_buttonpattern = "see <q:a|an|any|no> <el:unquoted>" # Matches: see any dialogTemplate variables
Templates use Jinja2 syntax with the following variables:
{{ slot_name }}- Captured slot value (e.g.,{{ url }},{{ target }}){{ children }}- Rendered child blocks (for actions withreturns){{ ctx }}- Current context variable name (snake_case of the block name){{ name }}- The within block’s display name- Modifier values are available by their key names (e.g.,
{{ timeout }})
Template filters
The following Jinja2 filters are available:
indent(n)- Indent text by n spacessnake_case- Convert text to snake_case
Example:
template = '''with pytest.raises({{ error }}): {{ children | indent(4) }}'''Complete example
Here is a minimal API testing plugin:
[plugin]name = "python-requests"plugin_version = "0.1.0"vanya_version = "0.1"language = "python"file_extension = ".py"
[types.api]allowed_children = ["failure_block"]modifiers = { base = "quoted" }template = """ctx = requests.Session()ctx.base_url = "{{ base }}""""
[[types.api.patterns]]name = "get"pattern = "GET <path:quoted>"modifiers = { headers = "quoted", timeout = "quoted" }returns = "http_response"template = 'http_response = ctx.get("{{ path }}")'
[[types.api.patterns]]name = "post"pattern = "POST <path:quoted> body <body:quoted>"returns = "http_response"template = 'http_response = ctx.post("{{ path }}", json={{ body }})'
[types.http_response]
[[types.http_response.patterns]]name = "expect-status"pattern = "expect status <code:unquoted>"template = "assert http_response.status_code == {{ code }}"
[types.failure_block]patterns = "passthrough"