Tir test generator
Skill tbc-servicos/dataagile-agent-kit/protheus/skills/tir-test-generator
Plugin Claude Code para Protheus e ADVPL/TLPP — base 155k+ registros, Agent Teams, compilação TDS-CLI, testes TIR e MCP PO-UI
npx -y skills add tbc-servicos/dataagile-agent-kit --skill tir-test-generatorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Generate TIR (TOTVS Interface Robot) end-to-end test scripts in Python for Protheus SmartClient/Webapp screens. Supports CRUD screen tests, MVC screen tests, grid interaction, report tests, field validation, and message box assertions. Use when a user says "TIR test", "interface test", "e2e test Protheus", "SmartClient test", "Webapp test", "screen test", "create Python test for Protheus screen", or "automate Protheus UI test".
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
19.2 KB, as published. Nobody here has run it
⚠️ O TIR não é a forma oficial de testar neste plugin — ele não tem visão computacional. A engine oficial de E2E é o Playwright (
/protheus:test-web, com screenshots e validação visual). Use esta skill apenas para gerar suíte de regressão CI re-executável sem LLM, derivada de uma sessão Playwright já validada (ver/protheus:qa).
Protheus TIR Test Generator
Overview
Generate end-to-end interface test scripts in Python for the TOTVS Protheus ERP using the TIR (TOTVS Interface Robot) framework. TIR automates Protheus screens through the SmartClient/Webapp interface, validating complete user workflows including screen navigation, field interaction, grid manipulation, button clicks, and result assertions.
TIR tests use Python's unittest framework combined with the tir.Webapp class to:
- Connect to a running Protheus Webapp environment
- Navigate to specific routines via
Program() - Interact with screen elements (fields, grids, buttons, folders)
- Assert field values, screen states, and data integrity
- Tear down the session cleanly
When to Use
Use this skill when:
- Creating end-to-end UI tests for Protheus screens (CRUD, workflows)
- Testing MVC-generated screens via the SmartClient/Webapp interface → use
Webappclass - Validating critical user journeys (complete business flows)
- Testing screen navigation, menu access, and dialog handling
- Validating field interactions (fill, check, read values)
- Testing grid operations (add rows, edit cells, scroll, select)
- Validating reports via parameter screens
- Needing visual/interface-level regression tests
When NOT to Use
- TLPP unit tests → Use ProBat (@TestFixture)
Which TIR Class to Use?
| Routine type | TIR class | Import |
|---|---|---|
MVC (ModelDef/ViewDef/MenuDef) | Webapp | from tir import Webapp |
Legacy Browse (MBrowse, AxCadastro) | Webapp | from tir import Webapp |
TIR Three-File Architecture (MANDATORY)
Every TIR test project requires three files:
| File | Responsibility |
|---|---|
{ROUTINE}TESTCASE.py | Test class with unittest.TestCase, setUpClass, test methods (test_*), tearDownClass |
{ROUTINE}TESTSUITE.py | Runner: imports TESTCASE, builds unittest.TestSuite, executes with TextTestRunner |
config.json | Environment configuration — URL, browser, credentials, Protheus environment name |
Without config.json in the same directory, TIR cannot connect to Protheus.
{test_directory}/
├── {ROUTINE}TESTCASE.py ← test class
├── {ROUTINE}TESTSUITE.py ← runner (CI/CD entry point)
└── config.json ← environment config (REQUIRED)
config.json — Webapp (MVC/Legacy)
{
"Url": "http://{server}:{port}/",
"Browser": "Chrome",
"Environment": "{environment_name}",
"User": "{username}",
"Password": "{password}",
"Language": "pt-br"
}
For the complete config.json parameter reference (all sections: Required, Additional, Logging, Database, Chrome), see tir-setup-and-best-practices.md.
Complete File Templates
TESTCASE Template — Webapp (MVC/Legacy)
from tir import Webapp
import unittest
class {ROUTINE}(unittest.TestCase):
"""
TIR E2E Tests for {ROUTINE} - {Description}
Module: {MODULE}
Tables: {TABLE_ALIASES}
"""
@classmethod
def setUpClass(inst):
"""Setup: Initialize Webapp, configure environment, open routine"""
inst.oHelper = Webapp()
inst.oHelper.Setup('{MODULE}', '{DATE}', 'T1', '{BRANCH}')
inst.oHelper.Program('{ROUTINE}')
def test_{ROUTINE}_CT001(self):
"""CT001 — Include record with required fields"""
self.oHelper.SetButton('Incluir')
self.oHelper.SetBranch('{BRANCH}')
self.oHelper.SetValue('{Field Label 1}', '{value1}')
self.oHelper.SetValue('{Field Label 2}', '{value2}')
self.oHelper.SetButton('Salvar')
self.oHelper.SearchBrowse(f'{BRANCH}{search_key}', key='Filial+{Search Column}')
self.oHelper.SetButton('Visualizar')
self.oHelper.CheckResult('{FIELD_NAME}', '{expected_value}')
self.oHelper.SetButton('Cancelar')
self.oHelper.AssertTrue()
def test_{ROUTINE}_CT002(self):
"""CT002 — Edit existing record"""
self.oHelper.SearchBrowse(f'{BRANCH}{search_key}', key='Filial+{Search Column}')
self.oHelper.SetButton('Alterar')
self.oHelper.SetValue('{Field Label}', '{new_value}')
self.oHelper.SetButton('Salvar')
self.oHelper.AssertTrue()
def test_{ROUTINE}_CT003(self):
"""CT003 — Delete record"""
self.oHelper.SearchBrowse(f'{BRANCH}{search_key}', key='Filial+{Search Column}')
self.oHelper.SetButton('Outras Ações', sub_item='Excluir')
self.oHelper.SetButton('Confirmar')
self.oHelper.AssertTrue()
def test_{ROUTINE}_CT004(self):
"""CT004 — Validate required field rule (negative)"""
self.oHelper.SetButton('Incluir')
self.oHelper.SetButton('Salvar')
self.oHelper.CheckHelp(text_help='{HELP_ID}', button='Fechar')
self.oHelper.SetButton('Cancelar')
self.oHelper.AssertFalse()
@classmethod
def tearDownClass(inst):
"""Teardown: Close session and collect results"""
inst.oHelper.TearDown()
if __name__ == '__main__':
unittest.main()
TESTSUITE Template
import unittest
from {ROUTINE}TESTCASE import {ROUTINE}
suite = unittest.TestSuite()
suite.addTest({ROUTINE}('test_{ROUTINE}_CT001'))
suite.addTest({ROUTINE}('test_{ROUTINE}_CT002'))
suite.addTest({ROUTINE}('test_{ROUTINE}_CT003'))
suite.addTest({ROUTINE}('test_{ROUTINE}_CT004'))
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
TIR Webapp Key Methods Reference
For the complete TIR Webapp API reference (Setup & Lifecycle, Field Interaction, Browse & Navigation, Grid Operations, Assertions & Validation, Dialog Handling, Parameters & Configuration), see tir-webapp-methods-reference.md.
Test Script Patterns
For all complete test script templates (Standard CRUD, Master-Detail with Grid, Tabs/Folders, View/Validate, Validation Error, Other Actions Menu, Report with Parameters, Grid Checkbox Selection), see tir-test-patterns.md.
Step-by-Step Process
When generating TIR test scripts, follow this process:
1. Identify the Screen Type
| Screen Type | Characteristics | Key Methods |
|---|---|---|
| Simple CRUD | Single form, no grid | SetValue, SetButton, CheckResult |
| Master-Detail | Form + grid (e.g., invoice header + items) | SetValue (grid=True), LoadGrid, ClickGridCell |
| Tabbed Screen | Multiple folders/tabs | ClickFolder, SetValue per tab |
| Browse + Actions | Browse grid with action menu | SearchBrowse, SetButton('Outras Ações', sub_item=...) |
| Report | Parameter screen + output | SetValue for params, SetButton('OK'), WaitProcessing |
| Wizard | Multi-step dialog flow | SetButton('Avançar'), SetButton('Finalizar') |
2. Map Screen Elements
For each test, identify:
- Field labels as they appear on screen (used by
SetValue) - Field technical names (used by
CheckResult, e.g.,'A1_COD','B1_DESC') - Button labels (e.g.,
'Incluir','Alterar','Salvar','Cancelar') - Grid columns and their headers
- Tab/folder names
- Search key format for
SearchBrowse— see UI Conventions & Pitfalls below for the right rule
UI Conventions & Pitfalls (LEARNED FROM REAL RUNS)
These are the details that separate a TIR script that runs from a script that fails at the first click. They are not negotiable — they came from running against real Protheus environments.
SearchBrowse — key name and padding
The key= argument must match the browse column header as rendered on screen, not a generic pattern. Two common headers in MVC routines:
| Routine trait | key value | Search term example |
|---|---|---|
Browse keyed only by {ALIAS}_COD | 'Filial+codigo' | 'D MG 01000033' |
Browse keyed by {ALIAS}_COD + {ALIAS}_LOJA (customers, suppliers) | 'Filial+codigo + Loja' | f'D MG 01{cod+loja}' |
| Modern routines with composite semantic header | Copy the header verbatim | — |
Rule of thumb: the browse header is the source of truth. If in doubt, open the browse in a real session and read the sort-selector label. Do not invent 'Filial+codigo + Loja' for a routine that browses only by code — SearchBrowse will scroll past the record and never find it.
Sub-menus with multiple levels
When "Outras Ações" opens a sub-menu that opens another sub-menu (e.g., Outras Ações → Oportunidades → Nova Oportunidade), TIR requires pipe-separated sub-items:
# Correct — nested sub-menu with pipe
self.oHelper.SetButton('Outras Ações', sub_item='Oportunidades|Nova Oportunidade')
# Wrong — loses the intermediate level, click never registers
self.oHelper.SetButton('Outras Ações', sub_item='Nova Oportunidade')
If the spec path has → X → Y (two arrows after the main menu), use sub_item='X|Y'.
Close buttons — Cancelar vs Fechar vs Sair
Different screens use different close labels. The convention in Protheus MVC is:
| Screen context | Button to close |
|---|---|
Form opened by Visualizar, Incluir, Alterar | 'Cancelar' |
| Related-action screen (sub-window) that only lists or reads data | 'Fechar' |
| Wizard last step that only confirms | 'Concluir' or 'Finalizar' |
| Main browse session | TearDown() closes it — never click manually |
Never use 'Sair' unless you have verified on the real screen. 'Sair' is rare in modern MVC and most of the time produces a ghost click that TIR reports as success while nothing happened.
CheckResult — technical name vs label
| Situation | Use |
|---|---|
The field has a clear {ALIAS}_{NAME} in the dictionary and the screen is the main form (Visualizar/Alterar of the routine being tested) | Technical name ('A1_COD', 'C5_CLIENTE') |
| The field belongs to a related screen called as an action (Nova Oportunidade, Novo Apontamento, Facilitador) — you may not know the alias of that secondary model | Screen label ('Cliente', 'Nome') |
Do not guess technical names for related screens. If the spec only gives a label and the routine alias is different (UA_*, AE_*, etc.), use the label. A wrong technical name fails silently or — worse — matches a different field with the same prefix.
Validation of "screen opened"
Validating that a related screen opened is best done by asserting content, not just presence:
# Weak — asserts something appeared but doesn't prove it's the right thing
self.oHelper.WaitShow('Subclientes')
# Strong — confirms the expected child record is listed
self.oHelper.SearchBrowse(f'D MG 01{filho+loja}', key='Filial+codigo + Loja')
Use WaitShow as a safety net before the content assertion, not as a replacement for it.
Setup branch padding
The Setup() branch string and the SearchBrowse branch prefix must be character-for-character identical, including trailing spaces. 'D MG 01' (no space) and 'D MG 01 ' (trailing space) are different keys in the browse. Match what the live environment renders.
3. Define Test Scenarios
| Scenario | Pattern | Assertion |
|---|---|---|
| Include with required fields | Fill fields → Salvar → SearchBrowse → Visualizar → CheckResult → Cancelar | AssertTrue() |
| Edit existing record | SearchBrowse → Alterar → SetValue → Salvar | AssertTrue() |
| Delete record | SearchBrowse → Outras Ações/Excluir → Confirmar | AssertTrue() |
| Required field validation | Incluir → Salvar (empty) → CheckHelp → Cancelar | AssertFalse() |
| Business rule validation | Fill invalid values → Salvar → CheckHelp → Cancelar | AssertFalse() |
4. Handle Common Screen Interactions
| Interaction | Method |
|---|---|
| Open routine | Program('{ROUTINE}') |
| Set branch after Incluir | SetBranch('{BRANCH}') |
| Click a button | SetButton('{Label}') |
| Click sub-menu button | SetButton('{Main}', sub_item='{Sub}') |
| Fill a form field | SetValue('{Label}', '{value}') |
| Fill a grid cell | SetValue('{Column}', '{value}', grid=True, grid_number=1, row=N) |
| Refresh grid | LoadGrid() |
| Switch tab | ClickFolder('{Tab Name}') |
| Search in browse | SearchBrowse(f'{BRANCH}{key}', 'Filial+{Column}') |
| Check field value | CheckResult('{FIELD_NAME}', '{expected}') |
| Handle error dialog | CheckHelp(text_help='{ID}', button='Fechar') |
| Wait for processing | WaitProcessing('{message}') |
| Send keyboard key | SetKey('{KEY}', grid=True/False) |
5. Build the TESTSUITE
After generating the TESTCASE, always generate the TESTSUITE that:
- Imports the test class from the TESTCASE file
- Creates a
unittest.TestSuite() - Adds each test method with
suite.addTest() - Runs with
unittest.TextTestRunner(verbosity=2)
Rationalization Guard
The moments the generator is most tempted to cut corners — and what actually happens.
| Temptation | Reality |
|---|---|
"I don't know the alias of the secondary screen, but UA_CLIENTE looks right for an opportunity, let me guess." | Wrong alias = silent failure or wrong field matched. Use the screen label ('Cliente') when the secondary screen's model is not in your context. Guessing propagates to every future test of that routine. |
"The spec says Outras Ações → Oportunidades → Nova Oportunidade, but I'll just pass 'Nova Oportunidade' — TIR will find it." | No. Sub-menu navigation is literal. Drop the middle level and the click never happens. Use pipes: sub_item='Oportunidades|Nova Oportunidade'. |
"The browse header might be 'Filial+codigo + Loja' — that's the MATA030 pattern, CRMA980 must be the same." | Not guaranteed. Different browses, different headers. Look at the real screen or the MenuDef before committing to key=. |
"'Sair', 'Fechar', 'Cancelar' — whatever, they all close the window." | They close different windows. A wrong label produces a TIR warning that most CI pipelines swallow. See Close buttons above. |
"WaitShow('SubClientes') is enough — if the screen appeared, the test passes." | No. WaitShow only confirms a string is on the page. Assert content (the expected child record, a specific field value). |
"The spec references pre-existing customers 000033, 000050... I'll just use them. If they don't exist, TIR will create an error and we catch it later." | The error is SearchBrowse did not find. The test marks as failure, not as missing-data. Ensure the required records exist in the environment before running the suite — add a setUpClass step that creates them via ExecAuto, or document them as a manual prerequisite in the test file header. |
"I'll leave {server} or {port} in config.json — the user will fix it." | config.json without real values fails at connection, the test never starts, the user debugs the wrong layer. Either fill with sensible defaults (localhost, 8080) or explicitly mark the file with a TODO comment. |
Setup Requirements & Best Practices
For environment setup (Python, Webapp, tir.json configuration) and best practices (test independence, naming conventions, screen interactions, assertions, environment isolation), see tir-setup-and-best-practices.md.
Checklist
Before finalizing generated TIR test scripts, verify:
config.json:
- File present in the same directory as the test scripts
-
Url,Browser,Environment,User,Password,Languageall set - No placeholder values like
{server},{username}remaining
TESTCASE file ({ROUTINE}TESTCASE.py):
- File named
{ROUTINE}TESTCASE.py - Class named
{ROUTINE}inheriting fromunittest.TestCase - Correct class imported:
from tir import Webapp -
setUpClass(inst)contains the correct class instantiation,Setup(), andProgram()calls -
tearDownClass(inst)containsTearDown()call -
Setup()has correct module, date, group, and branch (4 args — 5thmoduleis optional) - Each test method starts with
test_prefix and has a docstring - Positive tests end with
self.oHelper.AssertTrue()— NO parameters - Negative tests end with
self.oHelper.AssertFalse()— NO parameters
Webapp-specific (MVC/Legacy):
-
SetBranch()called afterSetButton('Incluir')when needed -
SearchBrowse(term, key=...)useskey=kwarg with branch-prefixed term -
key=value matches the real browse header — single-key routines use'Filial+codigo', composite-key routines use'Filial+codigo + Loja'(verify, don't assume) - Branch padding in
Setup()matches exactly the branch padding inSearchBrowse(trailing space matters) - Sub-menus with 2+ levels use pipe:
sub_item='Level1\|Level2' - Close buttons:
'Cancelar'for Visualizar/Alterar/Incluir forms;'Fechar'for related-action sub-windows;'Concluir'/'Finalizar'for wizard endings; never'Sair'without verifying on the real screen -
CheckResultuses technical names ('A1_COD') only for the routine's own form; uses screen labels ('Cliente','Nome') for secondary screens whose model alias is unknown — do NOT guess aliases - When a test validates "screen X opened", assert content (child record via
SearchBrowse, specific field viaCheckResult) —WaitShowalone is not enough -
CheckHelp(text_help=..., button=...)for error dialogs -
LoadGrid()called after gridSetValueoperations
TESTSUITE file ({ROUTINE}TESTSUITE.py):
- File named
{ROUTINE}TESTSUITE.py -
import unittestpresent -
from {ROUTINE}TESTCASE import {ROUTINE}present -
suite = unittest.TestSuite()created -
suite.addTest({ROUTINE}('test_{ROUTINE}_CT00N'))for each test method -
runner = unittest.TextTestRunner(verbosity=2)present -
runner.run(suite)present
Both files:
- No placeholder tokens like
{ROUTINE},{VALUE},{FIELD}remaining - No
AssertTrue(value, msg)with parameters — TIR does not accept parameters
Related Skills
| Skill | When to Use Instead |
|---|---|
mvc-generator | Creating MVC routines (the screens being tested) |
tlpp-rest-endpoint-generator | Creating REST endpoints |