-
Notifications
You must be signed in to change notification settings - Fork 50
fix(e2e): overhaul all E2E specs for Linux tauri-driver #180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
senamakel
merged 53 commits into
tinyhumansai:main
from
CodeGhost21:feat/e2e-linux-ci-default
Apr 1, 2026
Merged
Changes from 52 commits
Commits
Show all changes
53 commits
Select commit
Hold shift + click to select a range
226a0f7
feat(e2e): move CI to Linux by default, keep macOS optional
CodeGhost21 30d246f
fix(e2e): fix login flow — config.toml injection, state cleanup, port…
CodeGhost21 fcea05f
fix(e2e): make onboarding walkthrough conditional in all flow specs
CodeGhost21 fc67275
fix(e2e): fix notion flow — auth assertion and navigation resilience
CodeGhost21 0f259db
fix(e2e): rewrite auth-access-control spec, add missing mock endpoints
CodeGhost21 da3c04b
fix(e2e): rewrite card and crypto payment flow specs
CodeGhost21 f92ec4c
Merge remote-tracking branch 'origin' into feat/e2e-linux-ci-default
CodeGhost21 9b981f3
fix(e2e): fix prettier formatting and login-flow syntax error
CodeGhost21 1b0414a
Merge branch 'main' into feat/e2e-linux-ci-default
senamakel c40af15
fix(e2e): format wdio.conf.ts with prettier
CodeGhost21 e7c80b0
fix(e2e): fix eslint errors — unused timeout param, unused eslint-dis…
CodeGhost21 f5d35c8
fix(e2e): add webkit2gtk-driver for tauri-driver on Linux CI
CodeGhost21 f8eb962
fix(e2e): add build artifact verification step in Linux CI
CodeGhost21 56b79bc
fix(local-ai): Ollama bootstrap failure UX and auto-recovery (#142)
senamakel f6a5326
fix(skills): persist OAuth credentials and fix skill auto-start lifec…
senamakel a01aa30
Update issue templates (#148)
senamakel 82ed6a1
feat(agent): add self-learning subsystem with post-turn reflection (#…
senamakel 8693ab9
feat(auth): Telegram bot registration flow — /auth/telegram endpoint …
senamakel 3c2ff05
feat(webhooks): webhook tunnel routing for skills + remove legacy tun…
senamakel 371e42b
feat(agent): architecture improvements — context guard, cost tracking…
senamakel f6aed0d
refactor(models): standardize to reasoning-v1, agentic-v1, coding-v1 …
senamakel 9cf27b4
fix(skills): debug infrastructure + disconnect credential cleanup (#154)
senamakel 61baf5d
feat(agent): multi-agent harness with 8 archetypes, DAG planning, and…
senamakel bcfe91f
chore(release): v0.50.0
github-actions[bot] 2f0ce09
chore(release): disable Windows build notifications in release workflow
senamakel 7aef315
chore(release): v0.50.1
github-actions[bot] 7682a25
chore(release): v0.50.2
github-actions[bot] 932aac3
chore(release): v0.50.3
github-actions[bot] dd64ea2
fix(e2e): address code review findings
CodeGhost21 6b460be
fix(e2e): add diagnostic logging for Linux CI session timeout
CodeGhost21 f4b57d3
fix(e2e): address code review findings
CodeGhost21 c91749f
fix(e2e): stage sidecar next to app binary for Linux CI
CodeGhost21 3b31175
fix(e2e): address code review findings
CodeGhost21 f5d6e9b
fix(e2e): add diagnostic logging for Linux CI session timeout
CodeGhost21 68a85ba
minor change
CodeGhost21 8b7711a
fix(e2e): make deep-link register_all non-fatal, add RUST_BACKTRACE
CodeGhost21 c420587
fix(e2e): JS click fallback for non-interactable elements on tauri-dr…
CodeGhost21 ee2333e
fix(e2e): scroll element into view before clicking on tauri-driver
CodeGhost21 c09a70b
fix(e2e): fix textExists and Settings navigation on Linux
CodeGhost21 9d4dbd1
Merge remote-tracking branch 'origin/main' into feat/e2e-linux-ci-def…
CodeGhost21 e74346c
fix: prettier formatting
CodeGhost21 8a198cf
fix(e2e): run Linux CI specs individually without fail-fast
CodeGhost21 aa34169
fix(e2e): split Linux CI into core and extended specs, skip macOS E2E
CodeGhost21 fb8ab00
fix(e2e): skip extended specs on Linux CI to avoid timeout
CodeGhost21 f4fc1a0
Merge remote-tracking branch 'origin/main' into feat/e2e-linux-ci-def…
CodeGhost21 3201ffb
fix(e2e): overhaul all E2E specs for Linux tauri-driver compatibility
CodeGhost21 c3a70e9
Merge branch 'main' into feat/e2e-linux-ci-default
CodeGhost21 ea23d2b
fix(e2e): harden specs with self-contained state, assertions, and dia…
CodeGhost21 8540ed4
fix(e2e): resolve typecheck failures and apply prettier formatting
CodeGhost21 6dfebf8
style: format wdio.conf.ts with prettier
CodeGhost21 8247cc7
fix(e2e): resolve eslint errors — remove unused eslint-disable and de…
CodeGhost21 5c26bb6
style: format login-flow.spec.ts with prettier
CodeGhost21 4ffe7fe
fix(e2e): fix CI failures in login-flow error path and onboarding-com…
CodeGhost21 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,319 @@ | ||
| // @ts-nocheck | ||
| /** | ||
| * Shared E2E flow helpers for Linux (tauri-driver). | ||
| * | ||
| * Extracted from individual spec files to avoid duplication. | ||
| * All navigation uses browser.execute() with window.location.hash | ||
| * because sidebar nav buttons are icon-only (aria-label, no text content). | ||
| */ | ||
| import { waitForAppReady, waitForAuthBootstrap } from './app-helpers'; | ||
| import { triggerAuthDeepLink } from './deep-link-helpers'; | ||
| import { | ||
| clickText, | ||
| dumpAccessibilityTree, | ||
| textExists, | ||
| waitForWebView, | ||
| waitForWindowVisible, | ||
| } from './element-helpers'; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Generic helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| export async function waitForRequest(log, method, urlFragment, timeout = 15_000) { | ||
| const deadline = Date.now() + timeout; | ||
| while (Date.now() < deadline) { | ||
| const match = log().find(r => r.method === method && r.url.includes(urlFragment)); | ||
| if (match) return match; | ||
| await browser.pause(500); | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| export async function waitForHomePage(timeout = 15_000) { | ||
| const candidates = [ | ||
| 'Test', | ||
| 'Good morning', | ||
| 'Good afternoon', | ||
| 'Good evening', | ||
| 'Message OpenHuman', | ||
| 'Upgrade to Premium', | ||
| ]; | ||
| const deadline = Date.now() + timeout; | ||
| while (Date.now() < deadline) { | ||
| for (const text of candidates) { | ||
| if (await textExists(text)) return text; | ||
| } | ||
| await browser.pause(1_000); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| export async function waitForTextToDisappear(text, timeout = 10_000) { | ||
| const deadline = Date.now() + timeout; | ||
| while (Date.now() < deadline) { | ||
| if (!(await textExists(text))) return true; | ||
| await browser.pause(500); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Click the first matching text from a list of candidates. | ||
| */ | ||
| export async function clickFirstMatch(candidates, timeout = 5_000) { | ||
| for (const text of candidates) { | ||
| if (await textExists(text)) { | ||
| await clickText(text, timeout); | ||
| return text; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Navigation helpers (JS hash-based — icon-only sidebar buttons) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| export async function navigateViaHash(hash) { | ||
| try { | ||
| await browser.execute(h => { | ||
| window.location.hash = h; | ||
| }, hash); | ||
| await browser.pause(2_000); | ||
| const currentHash = await browser.execute(() => window.location.hash); | ||
| console.log(`[E2E] Navigated to ${hash} (current: ${currentHash})`); | ||
| } catch (err) { | ||
| console.log(`[E2E] Hash navigation to ${hash} failed:`, err); | ||
| } | ||
| } | ||
|
|
||
| export async function navigateToHome() { | ||
| await navigateViaHash('/home'); | ||
| const homeText = await waitForHomePage(10_000); | ||
| if (!homeText) { | ||
| try { | ||
| await browser.execute(() => { | ||
| window.location.hash = '/home'; | ||
| }); | ||
| } catch { | ||
| /* ignore */ | ||
| } | ||
| await browser.pause(2_000); | ||
| await waitForHomePage(10_000); | ||
| } | ||
| } | ||
|
|
||
| export async function navigateToSettings() { | ||
| await navigateViaHash('/settings'); | ||
| } | ||
|
|
||
| export async function navigateToBilling() { | ||
| await navigateViaHash('/settings/billing'); | ||
|
|
||
| const deadline = Date.now() + 15_000; | ||
| let hasBilling = false; | ||
| while (Date.now() < deadline) { | ||
| hasBilling = | ||
| (await textExists('Current Plan')) || | ||
| (await textExists('FREE')) || | ||
| (await textExists('Upgrade')); | ||
| if (hasBilling) break; | ||
| await browser.pause(500); | ||
| } | ||
|
|
||
| if (hasBilling) { | ||
| console.log('[E2E] Billing page loaded'); | ||
| return; | ||
| } | ||
|
|
||
| // Fallback | ||
| const currentHash = await browser.execute(() => window.location.hash); | ||
| console.log(`[E2E] Billing content not found. Current hash: ${currentHash}`); | ||
|
|
||
| await navigateViaHash('/settings'); | ||
| await browser.pause(3_000); | ||
|
|
||
| const clicked = await browser.execute(() => { | ||
| const allText = document.querySelectorAll('*'); | ||
| for (const el of allText) { | ||
| const text = el.textContent?.trim() || ''; | ||
| if ( | ||
| (text === 'Billing & Usage' || text === 'Billing') && | ||
| el.closest('button, [role="button"], a, [class*="MenuItem"]') | ||
| ) { | ||
| (el.closest('button, [role="button"], a, [class*="MenuItem"]') as HTMLElement).click(); | ||
| return 'clicked'; | ||
| } | ||
| } | ||
| window.location.hash = '/settings/billing'; | ||
| return 'hash-fallback'; | ||
| }); | ||
| console.log(`[E2E] Billing fallback: ${clicked}`); | ||
| await browser.pause(3_000); | ||
|
|
||
| // Verify billing actually loaded after fallback | ||
| const finalCheck = | ||
| (await textExists('Current Plan')) || | ||
| (await textExists('FREE')) || | ||
| (await textExists('Upgrade')); | ||
| if (!finalCheck) { | ||
| const finalHash = await browser.execute(() => window.location.hash); | ||
| const tree = await dumpAccessibilityTree(); | ||
| console.log(`[E2E] Billing verification failed after fallback. Hash: ${finalHash}`); | ||
| console.log(`[E2E] Accessibility tree:\n`, tree.slice(0, 4000)); | ||
| throw new Error( | ||
| `navigateToBilling: billing markers not found after fallback (hash: ${finalHash})` | ||
| ); | ||
| } | ||
| console.log('[E2E] Billing page loaded (after fallback)'); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| export async function navigateToSkills() { | ||
| await navigateViaHash('/skills'); | ||
| } | ||
|
|
||
| export async function navigateToIntelligence() { | ||
| await navigateViaHash('/intelligence'); | ||
| } | ||
|
|
||
| export async function navigateToConversations() { | ||
| await navigateViaHash('/conversations'); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Onboarding walkthrough (Onboarding.tsx — 6 real steps) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Walk through the real onboarding steps: | ||
| * Step 0: WelcomeStep — "Continue" | ||
| * Step 1: LocalAIStep — "Use Local Models" | ||
| * Step 2: ScreenPermissions — "Continue Without Permission" | ||
| * Step 3: ToolsStep — "Continue" | ||
| * Step 4: SkillsStep — "Finish Setup" (fires onboarding-complete) | ||
| * Step 5: MnemonicStep — checkbox + "Finish Setup" | ||
| */ | ||
| export async function walkOnboarding(logPrefix = '[E2E]') { | ||
| // Detect onboarding overlay. The Onboarding.tsx parent renders a "Skip" defer | ||
| // button (top-right), and step 0 is WelcomeStep with "Continue". | ||
| const onboardingVisible = | ||
| (await textExists('Welcome')) || | ||
| (await textExists('Skip')) || | ||
| (await textExists('Use Local Models')) || | ||
| (await textExists('Continue')); | ||
|
|
||
| if (!onboardingVisible) { | ||
| console.log(`${logPrefix} Onboarding overlay not visible — skipping`); | ||
| await browser.pause(3_000); | ||
| return; | ||
| } | ||
|
|
||
| // Step 0: WelcomeStep | ||
| if (await textExists('Welcome')) { | ||
| const clicked = await clickFirstMatch(['Continue'], 10_000); | ||
| if (clicked) console.log(`${logPrefix} WelcomeStep: clicked "${clicked}"`); | ||
| await browser.pause(2_000); | ||
| } | ||
|
|
||
| // Step 1: LocalAIStep — only has "Use Local Models" button now (no skip phase) | ||
| { | ||
| const clicked = await clickFirstMatch(['Use Local Models', 'Continue'], 10_000); | ||
| if (clicked) { | ||
| console.log(`${logPrefix} LocalAIStep: clicked "${clicked}"`); | ||
| await browser.pause(2_000); | ||
| } | ||
| } | ||
|
|
||
| // Step 2: ScreenPermissionsStep | ||
| { | ||
| const clicked = await clickFirstMatch(['Continue Without Permission', 'Continue'], 10_000); | ||
| if (clicked) { | ||
| console.log(`${logPrefix} ScreenPermissionsStep: clicked "${clicked}"`); | ||
| await browser.pause(2_000); | ||
| } | ||
| } | ||
|
|
||
| // Step 3: ToolsStep | ||
| { | ||
| if (await textExists('Enable Tools')) { | ||
| const clicked = await clickFirstMatch(['Continue'], 10_000); | ||
| if (clicked) { | ||
| console.log(`${logPrefix} ToolsStep: clicked "${clicked}"`); | ||
| await browser.pause(2_000); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Step 4: SkillsStep | ||
| { | ||
| if (await textExists('Install Skills')) { | ||
| const clicked = await clickFirstMatch(['Finish Setup'], 10_000); | ||
| if (clicked) { | ||
| console.log(`${logPrefix} SkillsStep: clicked "${clicked}"`); | ||
| await browser.pause(3_000); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Step 5: MnemonicStep | ||
| { | ||
| if (await textExists('Your Recovery Phrase')) { | ||
| console.log(`${logPrefix} MnemonicStep: visible`); | ||
| try { | ||
| await browser.execute(() => { | ||
| const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement; | ||
| if (checkbox && !checkbox.checked) checkbox.click(); | ||
| }); | ||
| } catch (err) { | ||
| console.log(`${logPrefix} MnemonicStep: checkbox failed:`, err); | ||
| } | ||
| await browser.pause(1_000); | ||
| const clicked = await clickFirstMatch(['Finish Setup'], 10_000); | ||
| if (clicked) { | ||
| console.log(`${logPrefix} MnemonicStep: clicked "${clicked}"`); | ||
| await browser.pause(3_000); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Full login flow | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * @param token Deep link token string. | ||
| * @param logPrefix Prefix for console log lines. | ||
| * @param postLoginVerifier Optional async callback invoked after the Home page | ||
| * is confirmed. Receives `logPrefix` so it can log consistently. If the | ||
| * verifier throws, performFullLogin propagates the error — callers can use | ||
| * this to assert that auth side-effects (e.g. token consume, profile fetch) | ||
| * actually occurred rather than relying on UI alone. | ||
| */ | ||
| export async function performFullLogin( | ||
| token = 'e2e-test-token', | ||
| logPrefix = '[E2E]', | ||
| postLoginVerifier?: (logPrefix: string) => Promise<void> | ||
| ) { | ||
| await triggerAuthDeepLink(token); | ||
| await waitForWindowVisible(25_000); | ||
| await waitForWebView(15_000); | ||
| await waitForAppReady(15_000); | ||
| await waitForAuthBootstrap(15_000); | ||
|
|
||
| await walkOnboarding(logPrefix); | ||
|
|
||
| const homeText = await waitForHomePage(15_000); | ||
| if (!homeText) { | ||
| const tree = await dumpAccessibilityTree(); | ||
| console.log(`${logPrefix} Home page not reached after login. Tree:\n`, tree.slice(0, 4000)); | ||
| throw new Error('Full login did not reach Home page'); | ||
| } | ||
|
|
||
| if (postLoginVerifier) { | ||
| await postLoginVerifier(logPrefix); | ||
| } | ||
|
|
||
| console.log(`${logPrefix} Home page confirmed: found "${homeText}"`); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
navigateToBilling()can still report success while the app is on Home.textExists()is a contains match, so bareUpgradealso matches Home’sUpgrade to Premium. Both the initial loop and the final fallback check can therefore succeed without any billing-only marker on screen, and downstream specs can end up clicking the wrong CTA.🧭 Tighten the billing check
Also applies to: 155-160
🤖 Prompt for AI Agents