mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
feat(e2e): extensions tab tests, CI parallelization, and 3 production bug fixes (#584)
* feat(e2e): extensions tab tests, CI parallelization, and 3 bug fixes ## E2E test coverage - Add tests/e2e/scenarios/test_extensions.py with 57 tests covering all extensions tab flows: installed WASM tool/MCP/channel cards, configure modal (open, fields, cancel, save, OAuth, error), auth card (token, OAuth, submit, cancel, error, multi-extension coexistence), activate flow, install/remove flows, WASM channel stepper states, and tab reload behaviour. All network calls intercepted via page.route() — no real binaries or external registries needed. - Expand tests/e2e/helpers.py with 50+ new CSS selectors for the extensions tab UI. - Add tests/e2e/README.md documentation on the page.route() mocking pattern, LIFO handler ordering, and page.evaluate() injection. ## CI parallelization - Split .github/workflows/e2e.yml into a build job (compile once, upload artifact) and a 3-way parallel test matrix (core / features / extensions), matching the pattern in test.yml. Reduces wall-clock time from ~15–20 min serial to ~10–12 min. Adds an e2e roll-up job for branch protection. ## Bug fixes in app.js (found via test-driven code review) - Fix null crash: renderExtensionCard() called ext.tools.length without a null guard; add ext.tools && check (regression: test_ext_tools_null). - Fix modal UX: submitConfigureModal() closed the overlay before checking success, making failures unrecoverable without reopening; close only on success, re-enable buttons and keep modal open on failure (regression: test_configure_modal_stays_open_on_save_failure). - Fix URL injection: all window.open() calls for server-supplied auth_url now go through openOAuthUrl() which rejects non-HTTPS schemes (regression: test_oauth_url_injection_blocked). Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * refactor(e2e): prune extensions tests 57→46 by merging redundant setups Merge 11 tests that shared identical fixture+navigation overhead: - Group A: 3 empty-state tests → test_extensions_empty_tab_layout - Group B: card_renders absorbs ext_tools_list_shown (same _WASM_TOOL fixture) - Group B: auth_dot_unauthed + unauthed_shows_configure_btn → test_installed_wasm_tool_unauthed_state - Group D: installed + configured states → test_wasm_channel_setup_states (identical UI) - Group D: failed_state + stepper_failed_circle → test_wasm_channel_failed_renders - Group G: 5 field badge tests → test_configure_modal_field_variants (4 fields, one pass) - Group H: submit_success + enter_key_submits → test_auth_card_submit_success Coverage preserved: all assertions kept, no unique behaviors removed. Extensions CI job estimated to drop from ~7 min to ~5 min. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(e2e): fix configure_input selector scoping in merged field variants test modal.locator(".configure-modal input[type='password']") scoped the absolute selector inside .configure-modal, effectively searching for a nested .configure-modal which never exists → count() == 0. Use page.locator() instead, consistent with all other tests in the file. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(e2e): address PR review comments — replace fixed sleeps with deterministic waits - Remove unnecessary wait_for_timeout(1000) from test_remove_cancelled_keeps_card (window.confirm = () => false is synchronous; DOM is unchanged when click() returns) - Replace wait_for_timeout(800) with wait_for_function() for window._lastOpenedUrl checks in configure_modal_save_oauth and activate_with_auth_url_opens_popup - Replace wait_for_timeout(300) with nth(1).wait_for(visible) in test_auth_card_multiple_extensions_coexist - Remove wait_for_timeout(800/300) in test_auth_card_submit_empty_noop and test_auth_completed_sse_dismisses_card (both check synchronous JS side-effects) - Add comment in test_oauth_url_injection_blocked explaining why timeout is kept (negative assertion — cannot use wait_for_function for absence of event) Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(e2e): address remaining PR review comments - Remove unused `import pytest` from test_extensions.py - Fix unawaited coroutine bug: convert lambda route handlers to async def in test_extensions_tab_reloads_on_revisit and test_auth_completed_sse_triggers_extensions_reload (lambda r: r.fulfill(...) returns an unawaited coroutine; requests silently fell through to real server) - Fix README.md example to use async def handler (same bug in docs) - Harden openOAuthUrl() in app.js: use URL constructor instead of .startsWith() so non-string server-supplied values (objects, null, etc.) are safely rejected rather than throwing TypeError Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(e2e): address second round of PR review comments - Add timeout-minutes to CI build job to prevent hung workflows - Use parsed.href instead of raw url in openOAuthUrl for safety - Remove unused MessageEvent variable in auth_completed test - Replace wait_for_timeout(800) with expect_response in activate test - Replace wait_for_timeout(300) with tab panel wait_for in reload test [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
37bba72397
commit
b425213c53
@@ -1003,7 +1003,7 @@ function showAuthCard(data) {
|
||||
oauthBtn.className = 'auth-oauth';
|
||||
oauthBtn.textContent = 'Authenticate with ' + data.extension_name;
|
||||
oauthBtn.addEventListener('click', () => {
|
||||
window.open(data.auth_url, '_blank', 'width=600,height=700');
|
||||
openOAuthUrl(data.auth_url);
|
||||
});
|
||||
links.appendChild(oauthBtn);
|
||||
}
|
||||
@@ -1921,7 +1921,7 @@ function renderAvailableExtensionCard(entry) {
|
||||
// OAuth popup if auth started during install (builtin creds)
|
||||
if (res.auth_url) {
|
||||
showToast('Opening authentication for ' + entry.display_name, 'info');
|
||||
window.open(res.auth_url, '_blank', 'width=600,height=700');
|
||||
openOAuthUrl(res.auth_url);
|
||||
}
|
||||
loadExtensions();
|
||||
// Auto-open configure for WASM channels
|
||||
@@ -2079,7 +2079,7 @@ function renderExtensionCard(ext) {
|
||||
card.appendChild(url);
|
||||
}
|
||||
|
||||
if (ext.tools.length > 0) {
|
||||
if (ext.tools && ext.tools.length > 0) {
|
||||
const tools = document.createElement('div');
|
||||
tools.className = 'ext-tools';
|
||||
tools.textContent = 'Tools: ' + ext.tools.join(', ');
|
||||
@@ -2179,7 +2179,7 @@ function activateExtension(name) {
|
||||
// Even on success, the tool may need OAuth (e.g., WASM loaded but no token yet)
|
||||
if (res.auth_url) {
|
||||
showToast('Opening authentication for ' + name, 'info');
|
||||
window.open(res.auth_url, '_blank', 'width=600,height=700');
|
||||
openOAuthUrl(res.auth_url);
|
||||
}
|
||||
loadExtensions();
|
||||
return;
|
||||
@@ -2187,7 +2187,7 @@ function activateExtension(name) {
|
||||
|
||||
if (res.auth_url) {
|
||||
showToast('Opening authentication for ' + name, 'info');
|
||||
window.open(res.auth_url, '_blank');
|
||||
openOAuthUrl(res.auth_url);
|
||||
} else if (res.awaiting_token) {
|
||||
showConfigureModal(name);
|
||||
} else {
|
||||
@@ -2329,20 +2329,21 @@ function submitConfigureModal(name, fields) {
|
||||
body: { secrets },
|
||||
})
|
||||
.then((res) => {
|
||||
closeConfigureModal();
|
||||
if (res.success) {
|
||||
closeConfigureModal();
|
||||
if (res.auth_url) {
|
||||
// OAuth flow started — open consent popup. The auth_completed SSE will
|
||||
// not arrive immediately (it fires after OAuth callback), so show a toast now.
|
||||
showToast('Opening OAuth authorization for ' + name, 'info');
|
||||
window.open(res.auth_url, '_blank', 'width=600,height=700');
|
||||
openOAuthUrl(res.auth_url);
|
||||
loadExtensions();
|
||||
}
|
||||
// For non-OAuth success: the server always broadcasts auth_completed SSE,
|
||||
// which will show the toast and refresh extensions — no need to do it here too.
|
||||
} else {
|
||||
// Keep modal open so the user can correct their input and retry.
|
||||
btns.forEach(function(b) { b.disabled = false; });
|
||||
showToast(res.message || 'Configuration failed', 'error');
|
||||
loadExtensions();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -2356,6 +2357,25 @@ function closeConfigureModal() {
|
||||
if (existing) existing.remove();
|
||||
}
|
||||
|
||||
// Validate that a server-supplied OAuth URL is HTTPS before opening a popup.
|
||||
// Rejects javascript:, data:, and other non-HTTPS schemes to prevent URL-injection.
|
||||
// Uses the URL constructor to safely parse and validate the scheme, which also
|
||||
// handles non-string values (objects, null, etc.) that would throw on .startsWith().
|
||||
function openOAuthUrl(url) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
if (parsed.protocol !== 'https:') {
|
||||
throw new Error('non-HTTPS protocol: ' + parsed.protocol);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Blocked invalid/non-HTTPS OAuth URL:', url, e.message);
|
||||
showToast('Invalid OAuth URL returned by server', 'error');
|
||||
return;
|
||||
}
|
||||
window.open(parsed.href, '_blank', 'width=600,height=700');
|
||||
}
|
||||
|
||||
// --- Pairing ---
|
||||
|
||||
function loadPairingRequests(channel, container) {
|
||||
|
||||
Reference in New Issue
Block a user