| name | odoo_frontend_testing |
| description | Frontend testing patterns for custom Odoo modules using HOOT and JavaScript testing frameworks. Use when creating frontend tests for Odoo modules. |
| version | 1.0.0 |
| author | VPCS Team |
| category | testing |
| odoo_versions | ["17.0","18.0","19.0"] |
| tags | ["odoo","testing","frontend","hoot","javascript","ui"] |
Goal
Define frontend/UI test approach for Odoo customizations (views, JS, OWL), combining xmlrpc-based form interaction testing with JS unit tests.
Scope
- Form/list/wizard UI behavior and user workflows via xmlrpc (post-install validation).
- JS/OWL components and client actions (unit tests).
- Basic cross-browser sanity (Chromium/Firefox) if applicable.
Sandbox execution
When .sandbox/session.json exists, run sandbox/bin/sandboxctl module <session> update <module> before browser testing and require its result JSON
to report succeeded. Do not call odoo-bin; retain bash manage_modules.sh
for local mode.
Primary Approach: Browser-Based Testing with Playwright/Chrome DevTools
Why Browser Testing?
- Real JavaScript Execution: Tests actual JS/OWL code running in browser (not backend-only).
- Console Monitoring: Captures JS errors, warnings, and custom logs before users see them.
- User Interaction: Validates actual clicks, form fills, page navigation, async operations.
- Cross-Browser: Test Chromium, Firefox, WebKit to catch browser-specific issues.
- Visual/DOM Validation: Verifies UI rendering, element visibility, styles, attributes.
- Network Monitoring: Catch failed API calls, slow requests, missing resources.
Structure & Conventions
A. Test File Location
<custom_module>/scripts/
test_browser_ui.py # Main Playwright/Chrome DevTools test script
test_browser_js_errors.py # Console log capture and error validation
test_browser_interactions.py # Complex user workflows (optional)
B. Base Script Template (Playwright + Console Monitoring)
"""
Browser-based frontend testing with console log capture.
Usage: python3 test_browser_ui.py --url http://localhost:8019 --db odoo19 --username admin --password admin
Tests actual browser rendering, JS execution, console errors.
"""
import asyncio
from playwright.async_api import async_playwright, expect
import json
import sys
from argparse import ArgumentParser
from datetime import datetime
class OdooBrowserTest:
def __init__(self, url, db, username, password):
self.url = url
self.db = db
self.username = username
self.password = password
self.browser = None
self.context = None
self.page = None
self.console_logs = []
self.js_errors = []
async def setup(self):
"""Launch browser and setup console log capture."""
self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(headless=True)
.context = .browser.new_context()
.page = .context.new_page()
.page.on(, ._on_console)
.page.on(, ._on_page_error)
()
():
level = msg.
text = msg.text
location = msg.location
log_entry = {
: datetime.now().isoformat(),
: level,
: text,
: location
}
.console_logs.append(log_entry)
level [, ]:
()
():
error_entry = {
: datetime.now().isoformat(),
: ,
: (error)
}
.js_errors.append(error_entry)
()
():
()
.page.goto(, wait_until=)
pre_login_logs = (.console_logs)
.page.fill(, .username)
.page.fill(, .password)
.page.click()
.page.wait_for_load_state()
post_login_logs = .console_logs[pre_login_logs:]
error_logs = [log log post_login_logs log[] [, ]]
error_logs:
()
()
():
()
:
.page.goto(,
wait_until=)
.page.wait_for_selector(, timeout=)
()
error_count = (.js_errors)
console_errors = [log log .console_logs log[] == ]
console_errors:
()
err console_errors[-:]:
()
fields_present = .page.query_selector_all()
()
Exception e:
()
():
()
:
.page.click()
.page.wait_for_selector(, timeout=)
()
.page.fill(, )
.page.press(, )
asyncio.sleep()
onchange_errors = [log log .console_logs log[] == log[].lower()]
onchange_errors:
()
()
.page.click()
.page.wait_for_load_state()
()
Exception e:
()
():
()
:
action_button = .page.query_selector()
action_button:
()
action_button.click()
.page.wait_for_load_state()
()
action_errors = [log log .console_logs log[] == ]
action_errors:
()
status_badge = .page.query_selector()
status_badge:
()
Exception e:
()
():
()
:
warnings = [log log .console_logs log[] == ]
errors = [log log .console_logs log[] == ]
()
()
()
critical_keywords = [, , , ]
critical_errors = [log log errors
(kw.lower() log[].lower() kw critical_keywords)]
critical_errors:
()
err critical_errors[:]:
()
()
Exception e:
()
():
()
:
failed_requests = []
():
response.status >= :
failed_requests.append({
: response.url,
: response.status,
: response.status_text
})
.page.on(, on_response)
.page.goto(,
wait_until=)
.page.remove_listener(, on_response)
failed_requests:
()
req failed_requests[:]:
()
()
Exception e:
()
():
.page:
.page.close()
.context:
.context.close()
.browser:
.browser.close()
.playwright:
.playwright.stop()
(, ) f:
json.dump({
: (.console_logs),
: (.js_errors),
: ([log log .console_logs log[] == ]),
: ([log log .console_logs log[] == ]),
: .console_logs[-:]
}, f, indent=)
()
():
parser = ArgumentParser(description=)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
parser.add_argument(, default=, =)
args = parser.parse_args()
()
()
()
()
tester = OdooBrowserTest(args.url, args.db, args.username, args.password)
:
tester.setup()
tester.login()
results = {
: tester.test_custom_form_rendering(),
: tester.test_create_record_with_validation(),
: tester.test_button_actions_and_state(),
: tester.test_console_for_warnings_and_deprecations(),
: tester.test_network_requests(),
}
:
tester.teardown()
()
()
()
passed = ( v results.values() v)
total = (results)
test_name, result results.items():
status = result
()
()
()
()
passed == total
__name__ == :
exit_code = asyncio.run(main())
sys.exit(exit_code)
Installation & Setup
Install Playwright
pip install playwright
pywright install
Run Tests
cd /path/to/odoo_local_setup
./manage_modules.sh start --version 19
cd /path/to/custom_module/scripts
python3 test_browser_ui.py --url http://localhost:8019 --db odoo19 --username admin --password admin
Console Logs Output
- Saves to
console_logs.json with all messages, errors, warnings
- Flagged in real-time during test execution
- Includes line numbers and file locations for debugging
Secondary Approach: xmlrpc Form Validation (When No Browser Available)
Use when:
- Browser testing not available (CI/CD, headless server)
- Quick pre-flight validation needed before browser tests
- Testing form metadata and field definitions
- Validating onchange side-effects via data (not visual)
Quick xmlrpc Form Check:
import xmlrpc.client
def validate_form_metadata(url, db, username, password, model):
"""Quick xmlrpc check: field structure without browser."""
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
object_rpc = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
uid = common.authenticate(db, username, password, {})
fields = object_rpc.execute_kw(db, uid, password, model, 'fields_get', [],
{'attributes': ['string', 'type', 'required', 'readonly', 'domain']})
required = [f for f, d in fields.items() if d.get('required')]
readonly = [f for f, d in fields.items() if d.get('readonly')]
print(f" ✓ Form has {len(fields)} fields")
print(f" - Required: {required}")
print(f" - Readonly: {readonly}")
return True
validate_form_metadata('http://localhost:8019', 'odoo19', 'admin', 'admin', 'custom_module.custom_model')
Tertiary Approach: JS/OWL Unit Tests (for client logic)
When to use JS unit tests
- Custom OWL components and client actions.
- Complex JS logic (not Odoo standard behaviors).
- Browser-specific behavior (real DOM required).
Practices
- Use QUnit for model-free JS; use OWL test utilities for components.
- Keep tests fast; mock server calls.
- Reuse fixtures; avoid side effects.
Test File Location
<custom_module>/static/tests/
test_components.js # OWL component tests
test_actions.js # Client action tests
Integration: LIVE TEST Sub-Task
Step 1: Install Module
./manage_modules.sh install custom_module --version 19
Step 2: Run Browser Tests (Primary)
pip install playwright
playwright install
cd /path/to/odoo_local_setup
./manage_modules.sh start --version 19
cd /path/to/custom_module/scripts
python3 test_browser_ui.py --url http://localhost:8019 --db odoo19 --username admin --password admin
Step 3: Review Console Logs
cat console_logs.json | jq '.js_errors'
Step 4: Mark LIVE TEST Complete
{
"feature": "Custom Module Feature",
"sub_tasks": [
{"title": "Browser UI tests", "status": "done"},
{"title": "Console error validation", "status": "done"},
{"title": "LIVE TEST", "status": "done"}
]
}
Key Differences: Browser Test vs xmlrpc Form Check
| Aspect | Browser Test (Playwright) | xmlrpc Form Check |
|---|
| Coverage | JavaScript execution, console logs, network, DOM rendering, actual user clicks | Field metadata, domain filters, field definitions only |
| Catches | JS errors, console errors, network failures, async issues, race conditions | Missing fields, incorrect domain syntax, field type mismatches |
| Speed | Slower (browser startup, network round-trips) | Fast (single xmlrpc call) |
| Environment | Requires display (X11/Wayland) or headless browser | Any Python environment |
| Use Case | Post-install acceptance testing (catches frontend issues) | Pre-flight quick validation (catches basic metadata issues) |
| Best For | CI/CD with display, developer testing, regression | Headless CI/CD, quick checks, form structure validation |
Official Odoo Documentation References
Odoo 17.0 External API (XML-RPC)
Odoo 18.0 External API (XML-RPC)
Odoo 19.0 External API (JSON-2 + XML-RPC Legacy)
UI Test Syntax Comparison
Odoo 17/18 (XML-RPC form field validation)
import xmlrpc.client
url = "http://localhost:8018"
db = "odoo18"
username = "admin"
password = "admin"
object_rpc = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})
fields = object_rpc.execute_kw(db, uid, password, 'custom_module.custom_model',
'fields_get', [],
{'attributes': ['string', 'type', 'required', 'readonly', 'domain']})
required = [f for f, d in fields.items() if d.get('required')]
readonly = [f for f, d in fields.items() if d.get('readonly')]
partner_field = fields.get('partner_id')
if partner_field:
domain = partner_field.get('domain', [])
print(f"partner_id domain constraint: {domain}")
Odoo 19.0 (JSON-2 API form field validation - Recommended)
import requests
url = "https://mycompany.example.com/json/2"
api_key = "<your_api_key_here>"
headers = {
"Authorization": f"bearer {api_key}",
"Content-Type": "application/json",
}
res = requests.post(
f"{url}/custom_module.custom_model/fields_get",
headers=headers,
json={"attributes": ["string", "type", "required", "readonly", "domain"]}
)
fields = res.json()
required = [f for f, d in fields.items() if d.get('required')]
readonly = [f for f, d in fields.items() if d.get('readonly')]
partner_field = fields.get('partner_id')
if partner_field:
domain = partner_field.get('domain', [])
print(f"partner_id domain: {domain}")
res = requests.post(
f"{url}/custom_module.custom_model/create",
headers=headers,
json={"name": "Test", "type": "type_a"}
)
record_id = res.json()
res = requests.post(
f"{url}/custom_module.custom_model/read",
headers=headers,
json={"ids": [record_id], : [, ]}
)
record = res.json()[]
()
Summary
Frontend Testing Strategy:
- Primary: Playwright + Chrome DevTools - Real browser testing with console log capture and JS error detection
- Secondary: xmlrpc form metadata validation - Quick checks when browser unavailable
- Tertiary: JS/OWL unit tests - Edge cases and custom component logic
Test Scope Pyramid:
┌─────────────────────────────┐
│ JS Unit Tests (Few) │ Edge cases, custom logic, refactoring safety
├─────────────────────────────┤
│ xmlrpc Form Checks (Some) │ Pre-flight metadata validation
├─────────────────────────────┤
│ Browser Tests (Many) │ Full UI workflows, console validation, user scenarios
└─────────────────────────────┘
What Browser Testing Catches (xmlrpc cannot):
- ✅ JavaScript execution errors (syntax, runtime)
- ✅ Console errors, warnings, and logged issues
- ✅ Network request failures (4xx/5xx responses)
- ✅ Async operations (promises, timeouts)
- ✅ Race conditions and timing issues
- ✅ DOM rendering and visibility
- ✅ Actual user interactions (clicks, field fills)
- ✅ Page load performance metrics
Integration with progress.json LIVE TEST:
- Run browser tests immediately after
install step
- Capture console logs to validate no JS errors occurred
- Browser test = acceptance test for feature completion
- All 5 core tests (form rendering, create, actions, console health, network) must pass before marking LIVE TEST done