|
| 1 | +// @ts-nocheck |
| 2 | +/** |
| 3 | + * Shared E2E flow helpers for Linux (tauri-driver). |
| 4 | + * |
| 5 | + * Extracted from individual spec files to avoid duplication. |
| 6 | + * All navigation uses browser.execute() with window.location.hash |
| 7 | + * because sidebar nav buttons are icon-only (aria-label, no text content). |
| 8 | + */ |
| 9 | +import { waitForAppReady, waitForAuthBootstrap } from './app-helpers'; |
| 10 | +import { triggerAuthDeepLink } from './deep-link-helpers'; |
| 11 | +import { |
| 12 | + clickText, |
| 13 | + dumpAccessibilityTree, |
| 14 | + textExists, |
| 15 | + waitForWebView, |
| 16 | + waitForWindowVisible, |
| 17 | +} from './element-helpers'; |
| 18 | + |
| 19 | +// --------------------------------------------------------------------------- |
| 20 | +// Generic helpers |
| 21 | +// --------------------------------------------------------------------------- |
| 22 | + |
| 23 | +export async function waitForRequest(log, method, urlFragment, timeout = 15_000) { |
| 24 | + const deadline = Date.now() + timeout; |
| 25 | + while (Date.now() < deadline) { |
| 26 | + const match = log().find(r => r.method === method && r.url.includes(urlFragment)); |
| 27 | + if (match) return match; |
| 28 | + await browser.pause(500); |
| 29 | + } |
| 30 | + return undefined; |
| 31 | +} |
| 32 | + |
| 33 | +export async function waitForHomePage(timeout = 15_000) { |
| 34 | + const candidates = [ |
| 35 | + 'Test', |
| 36 | + 'Good morning', |
| 37 | + 'Good afternoon', |
| 38 | + 'Good evening', |
| 39 | + 'Message OpenHuman', |
| 40 | + 'Upgrade to Premium', |
| 41 | + ]; |
| 42 | + const deadline = Date.now() + timeout; |
| 43 | + while (Date.now() < deadline) { |
| 44 | + for (const text of candidates) { |
| 45 | + if (await textExists(text)) return text; |
| 46 | + } |
| 47 | + await browser.pause(1_000); |
| 48 | + } |
| 49 | + return null; |
| 50 | +} |
| 51 | + |
| 52 | +export async function waitForTextToDisappear(text, timeout = 10_000) { |
| 53 | + const deadline = Date.now() + timeout; |
| 54 | + while (Date.now() < deadline) { |
| 55 | + if (!(await textExists(text))) return true; |
| 56 | + await browser.pause(500); |
| 57 | + } |
| 58 | + return false; |
| 59 | +} |
| 60 | + |
| 61 | +/** |
| 62 | + * Click the first matching text from a list of candidates. |
| 63 | + */ |
| 64 | +export async function clickFirstMatch(candidates, timeout = 5_000) { |
| 65 | + for (const text of candidates) { |
| 66 | + if (await textExists(text)) { |
| 67 | + await clickText(text, timeout); |
| 68 | + return text; |
| 69 | + } |
| 70 | + } |
| 71 | + return null; |
| 72 | +} |
| 73 | + |
| 74 | +// --------------------------------------------------------------------------- |
| 75 | +// Navigation helpers (JS hash-based — icon-only sidebar buttons) |
| 76 | +// --------------------------------------------------------------------------- |
| 77 | + |
| 78 | +export async function navigateViaHash(hash) { |
| 79 | + try { |
| 80 | + await browser.execute((h) => { window.location.hash = h; }, hash); |
| 81 | + await browser.pause(2_000); |
| 82 | + const currentHash = await browser.execute(() => window.location.hash); |
| 83 | + console.log(`[E2E] Navigated to ${hash} (current: ${currentHash})`); |
| 84 | + } catch (err) { |
| 85 | + console.log(`[E2E] Hash navigation to ${hash} failed:`, err); |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +export async function navigateToHome() { |
| 90 | + await navigateViaHash('/home'); |
| 91 | + const homeText = await waitForHomePage(10_000); |
| 92 | + if (!homeText) { |
| 93 | + try { |
| 94 | + await browser.execute(() => { window.location.hash = '/home'; }); |
| 95 | + } catch { /* ignore */ } |
| 96 | + await browser.pause(2_000); |
| 97 | + await waitForHomePage(10_000); |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +export async function navigateToSettings() { |
| 102 | + await navigateViaHash('/settings'); |
| 103 | +} |
| 104 | + |
| 105 | +export async function navigateToBilling() { |
| 106 | + await navigateViaHash('/settings/billing'); |
| 107 | + |
| 108 | + const deadline = Date.now() + 15_000; |
| 109 | + let hasBilling = false; |
| 110 | + while (Date.now() < deadline) { |
| 111 | + hasBilling = (await textExists('Current Plan')) || |
| 112 | + (await textExists('FREE')) || |
| 113 | + (await textExists('Upgrade')); |
| 114 | + if (hasBilling) break; |
| 115 | + await browser.pause(500); |
| 116 | + } |
| 117 | + |
| 118 | + if (hasBilling) { |
| 119 | + console.log('[E2E] Billing page loaded'); |
| 120 | + return; |
| 121 | + } |
| 122 | + |
| 123 | + // Fallback |
| 124 | + const currentHash = await browser.execute(() => window.location.hash); |
| 125 | + console.log(`[E2E] Billing content not found. Current hash: ${currentHash}`); |
| 126 | + |
| 127 | + await navigateViaHash('/settings'); |
| 128 | + await browser.pause(3_000); |
| 129 | + |
| 130 | + const clicked = await browser.execute(() => { |
| 131 | + const allText = document.querySelectorAll('*'); |
| 132 | + for (const el of allText) { |
| 133 | + const text = el.textContent?.trim() || ''; |
| 134 | + if ((text === 'Billing & Usage' || text === 'Billing') && |
| 135 | + el.closest('button, [role="button"], a, [class*="MenuItem"]')) { |
| 136 | + (el.closest('button, [role="button"], a, [class*="MenuItem"]') as HTMLElement).click(); |
| 137 | + return 'clicked'; |
| 138 | + } |
| 139 | + } |
| 140 | + window.location.hash = '/settings/billing'; |
| 141 | + return 'hash-fallback'; |
| 142 | + }); |
| 143 | + console.log(`[E2E] Billing fallback: ${clicked}`); |
| 144 | + await browser.pause(3_000); |
| 145 | +} |
| 146 | + |
| 147 | +export async function navigateToSkills() { |
| 148 | + await navigateViaHash('/skills'); |
| 149 | +} |
| 150 | + |
| 151 | +export async function navigateToIntelligence() { |
| 152 | + await navigateViaHash('/intelligence'); |
| 153 | +} |
| 154 | + |
| 155 | +export async function navigateToConversations() { |
| 156 | + await navigateViaHash('/conversations'); |
| 157 | +} |
| 158 | + |
| 159 | +// --------------------------------------------------------------------------- |
| 160 | +// Onboarding walkthrough (Onboarding.tsx — 6 real steps) |
| 161 | +// --------------------------------------------------------------------------- |
| 162 | + |
| 163 | +/** |
| 164 | + * Walk through the real onboarding steps: |
| 165 | + * Step 0: WelcomeStep — "Continue" |
| 166 | + * Step 1: LocalAIStep — "Setup later" → "Continue" (skip Ollama) |
| 167 | + * Step 2: ScreenPermissions — "Continue Without Permission" |
| 168 | + * Step 3: ToolsStep — "Continue" |
| 169 | + * Step 4: SkillsStep — "Finish Setup" (fires onboarding-complete) |
| 170 | + * Step 5: MnemonicStep — checkbox + "Finish Setup" |
| 171 | + */ |
| 172 | +export async function walkOnboarding(logPrefix = '[E2E]') { |
| 173 | + const onboardingVisible = (await textExists('Welcome')) || |
| 174 | + (await textExists('Set up later')) || |
| 175 | + (await textExists('Continue')); |
| 176 | + |
| 177 | + if (!onboardingVisible) { |
| 178 | + console.log(`${logPrefix} Onboarding overlay not visible — skipping`); |
| 179 | + await browser.pause(3_000); |
| 180 | + return; |
| 181 | + } |
| 182 | + |
| 183 | + // Step 0: WelcomeStep |
| 184 | + if (await textExists('Welcome')) { |
| 185 | + const clicked = await clickFirstMatch(['Continue'], 10_000); |
| 186 | + if (clicked) console.log(`${logPrefix} WelcomeStep: clicked "${clicked}"`); |
| 187 | + await browser.pause(2_000); |
| 188 | + } |
| 189 | + |
| 190 | + // Step 1: LocalAIStep |
| 191 | + { |
| 192 | + const clicked = await clickFirstMatch(['Setup later', 'Use Local Models', 'Continue'], 10_000); |
| 193 | + if (clicked) { |
| 194 | + console.log(`${logPrefix} LocalAIStep: clicked "${clicked}"`); |
| 195 | + await browser.pause(2_000); |
| 196 | + if (clicked === 'Setup later') { |
| 197 | + const cont = await clickFirstMatch(['Continue'], 5_000); |
| 198 | + if (cont) { |
| 199 | + console.log(`${logPrefix} LocalAIStep (skipped): clicked "Continue"`); |
| 200 | + await browser.pause(2_000); |
| 201 | + } |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + // Step 2: ScreenPermissionsStep |
| 207 | + { |
| 208 | + const clicked = await clickFirstMatch(['Continue Without Permission', 'Continue'], 10_000); |
| 209 | + if (clicked) { |
| 210 | + console.log(`${logPrefix} ScreenPermissionsStep: clicked "${clicked}"`); |
| 211 | + await browser.pause(2_000); |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + // Step 3: ToolsStep |
| 216 | + { |
| 217 | + if (await textExists('Enable Tools')) { |
| 218 | + const clicked = await clickFirstMatch(['Continue'], 10_000); |
| 219 | + if (clicked) { |
| 220 | + console.log(`${logPrefix} ToolsStep: clicked "${clicked}"`); |
| 221 | + await browser.pause(2_000); |
| 222 | + } |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + // Step 4: SkillsStep |
| 227 | + { |
| 228 | + if (await textExists('Install Skills')) { |
| 229 | + const clicked = await clickFirstMatch(['Finish Setup'], 10_000); |
| 230 | + if (clicked) { |
| 231 | + console.log(`${logPrefix} SkillsStep: clicked "${clicked}"`); |
| 232 | + await browser.pause(3_000); |
| 233 | + } |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + // Step 5: MnemonicStep |
| 238 | + { |
| 239 | + if (await textExists('Your Recovery Phrase')) { |
| 240 | + console.log(`${logPrefix} MnemonicStep: visible`); |
| 241 | + try { |
| 242 | + await browser.execute(() => { |
| 243 | + const checkbox = document.querySelector('input[type="checkbox"]') as HTMLInputElement; |
| 244 | + if (checkbox && !checkbox.checked) checkbox.click(); |
| 245 | + }); |
| 246 | + } catch (err) { |
| 247 | + console.log(`${logPrefix} MnemonicStep: checkbox failed:`, err); |
| 248 | + } |
| 249 | + await browser.pause(1_000); |
| 250 | + const clicked = await clickFirstMatch(['Finish Setup'], 10_000); |
| 251 | + if (clicked) { |
| 252 | + console.log(`${logPrefix} MnemonicStep: clicked "${clicked}"`); |
| 253 | + await browser.pause(3_000); |
| 254 | + } |
| 255 | + } |
| 256 | + } |
| 257 | +} |
| 258 | + |
| 259 | +// --------------------------------------------------------------------------- |
| 260 | +// Full login flow |
| 261 | +// --------------------------------------------------------------------------- |
| 262 | + |
| 263 | +export async function performFullLogin(token = 'e2e-test-token', logPrefix = '[E2E]') { |
| 264 | + await triggerAuthDeepLink(token); |
| 265 | + await waitForWindowVisible(25_000); |
| 266 | + await waitForWebView(15_000); |
| 267 | + await waitForAppReady(15_000); |
| 268 | + await waitForAuthBootstrap(15_000); |
| 269 | + |
| 270 | + await walkOnboarding(logPrefix); |
| 271 | + |
| 272 | + const homeText = await waitForHomePage(15_000); |
| 273 | + if (!homeText) { |
| 274 | + const tree = await dumpAccessibilityTree(); |
| 275 | + console.log(`${logPrefix} Home page not reached after login. Tree:\n`, tree.slice(0, 4000)); |
| 276 | + throw new Error('Full login did not reach Home page'); |
| 277 | + } |
| 278 | + console.log(`${logPrefix} Home page confirmed: found "${homeText}"`); |
| 279 | +} |
0 commit comments