Skip to content

Commit c233dc4

Browse files
committed
feat(e2e): split rewards/settings specs, fix Mac2 navigation and billing crash
- Split rewards-settings.spec.ts into rewards-flow.spec.ts and settings-flow.spec.ts so they can be run independently - Fix Mac2 Settings tab navigation: Home page has a "Settings" button that collides with the bottom tab bar; now targets the tab button via XCUIElementTypeButton XPath with aria-label filtering - Consolidate all /settings/* Mac2 navigation into a single handler with correct section-first ordering (Account & Security → Billing & Usage) - Add /rewards to HASH_TO_SIDEBAR_LABEL for Mac2 bottom tab navigation - Add dismissLocalAISnackbarIfVisible after onboarding in both specs - Guard .toFixed() calls in InferenceBudget.tsx and PayAsYouGoCard.tsx with ?? 0 fallbacks to prevent crash on undefined billing data - Add missing fields to mock API responses (monthlyBudgetUsd, weeklyBudgetUsd, fiveHourCapUsd, cycleEndsAt)
1 parent 98a49ee commit c233dc4

8 files changed

Lines changed: 529 additions & 174 deletions

File tree

app/scripts/e2e-run-all-flows.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ run "test/e2e/specs/notion-flow.spec.ts" "notion"
2929
run "test/e2e/specs/screen-intelligence.spec.ts" "screen-intelligence"
3030
run "test/e2e/specs/voice-mode.spec.ts" "voice-mode"
3131
run "test/e2e/specs/text-autocomplete-flow.spec.ts" "text-autocomplete"
32+
run "test/e2e/specs/rewards-flow.spec.ts" "rewards-flow"
33+
run "test/e2e/specs/settings-flow.spec.ts" "settings-flow"
3234

33-
# run "test/e2e/specs/skills-registry.spec.ts" "skills-registry"
34-
# run "test/e2e/specs/rewards-settings.spec.ts" "rewards-settings"
35-
# run "test/e2e/specs/navigation.spec.ts" "navigation"
3635

3736
echo "All E2E flows completed."

app/src/components/settings/panels/billing/InferenceBudget.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const InferenceBudget = ({ teamUsage, isLoadingCredits }: InferenceBudgetProps)
1212
{isLoadingCredits && <span className="text-[10px] text-stone-500">Loading…</span>}
1313
{teamUsage && !isLoadingCredits && (
1414
<span className="text-xs text-stone-400">
15-
${teamUsage.remainingUsd.toFixed(2)} / ${teamUsage.cycleBudgetUsd.toFixed(2)} remaining
15+
${(teamUsage.remainingUsd ?? 0).toFixed(2)} / $
16+
{(teamUsage.cycleBudgetUsd ?? 0).toFixed(2)} remaining
1617
</span>
1718
)}
1819
</div>
@@ -21,27 +22,27 @@ const InferenceBudget = ({ teamUsage, isLoadingCredits }: InferenceBudgetProps)
2122
<div className="h-1.5 bg-stone-700/60 rounded-full overflow-hidden mb-2">
2223
<div
2324
className={`h-full rounded-full transition-all duration-300 ${
24-
teamUsage.remainingUsd <= 0
25+
(teamUsage.remainingUsd ?? 0) <= 0
2526
? 'bg-coral-500'
26-
: teamUsage.remainingUsd / teamUsage.cycleBudgetUsd < 0.2
27+
: (teamUsage.remainingUsd ?? 0) / (teamUsage.cycleBudgetUsd || 1) < 0.2
2728
? 'bg-amber-500'
2829
: 'bg-primary-500'
2930
}`}
3031
style={{
31-
width: `${Math.min(100, (teamUsage.remainingUsd / teamUsage.cycleBudgetUsd) * 100)}%`,
32+
width: `${Math.min(100, ((teamUsage.remainingUsd ?? 0) / (teamUsage.cycleBudgetUsd || 1)) * 100)}%`,
3233
}}
3334
/>
3435
</div>
3536
<div className="mt-1 flex items-center justify-between">
3637
<span className="text-[11px] text-stone-500">
37-
5-hour cap: ${teamUsage.cycleLimit5hr.toFixed(2)} / $
38-
{teamUsage.fiveHourCapUsd.toFixed(2)}
38+
5-hour cap: ${(teamUsage.cycleLimit5hr ?? 0).toFixed(2)} / $
39+
{(teamUsage.fiveHourCapUsd ?? 0).toFixed(2)}
3940
</span>
4041
<span className="text-[11px] text-stone-500">
4142
Cycle ends {new Date(teamUsage.cycleEndsAt).toLocaleDateString('en-US')}
4243
</span>
4344
</div>
44-
{teamUsage.remainingUsd <= 0 && (
45+
{(teamUsage.remainingUsd ?? 0) <= 0 && (
4546
<p className="text-[11px] text-coral-400 mt-1.5">
4647
Included subscription usage is exhausted. Top up credits to continue using AI features
4748
without waiting for the next cycle.

app/src/components/settings/panels/billing/PayAsYouGoCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ const PayAsYouGoCard = ({
6767
<div className="flex items-center justify-between">
6868
<span className="text-xs text-stone-400">General credits</span>
6969
<span className="text-xs font-medium text-stone-900">
70-
${creditBalance.balanceUsd.toFixed(2)}
70+
${(creditBalance.balanceUsd ?? 0).toFixed(2)}
7171
</span>
7272
</div>
7373
<div className="space-y-1">
7474
<div className="flex items-center justify-between">
7575
<span className="text-xs text-stone-400">Top-up credits</span>
7676
<span className="text-xs font-medium text-stone-900">
77-
${creditBalance.topUpBalanceUsd.toFixed(2)}
77+
${(creditBalance.topUpBalanceUsd ?? 0).toFixed(2)}
7878
{creditBalance.topUpBaselineUsd != null && creditBalance.topUpBaselineUsd > 0 && (
7979
<span className="text-stone-500 font-normal">
8080
{' '}

app/test/e2e/helpers/shared-flows.ts

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ const HASH_TO_SIDEBAR_LABEL = {
8686
'/settings': 'Settings',
8787
'/intelligence': 'Intelligence',
8888
'/channels': 'Channels',
89+
'/rewards': 'Rewards',
8990
};
9091

9192
export async function navigateViaHash(hash) {
@@ -105,56 +106,112 @@ export async function navigateViaHash(hash) {
105106
return;
106107
}
107108

108-
// Appium Mac2 — Settings → Billing (nested route)
109-
if (normalized === '/settings/billing') {
109+
// Appium Mac2 — Settings and sub-pages.
110+
//
111+
// Problem: "Settings" text appears on multiple pages (Home page card body,
112+
// Skills card descriptions, etc.), so clickText('Settings') often matches
113+
// the wrong element instead of the bottom tab bar button.
114+
//
115+
// Solution: target the tab bar button by its aria-label using XCUIElementTypeButton
116+
// XPath directly, which avoids matching StaticText elements on the page.
117+
if (normalized === '/settings' || normalized.startsWith('/settings/')) {
110118
try {
111-
await clickText('Settings', 12_000);
112-
await browser.pause(1_500);
113-
const sub = await clickFirstMatch(['Billing & Usage', 'Billing'], 12_000);
114-
if (!sub) {
115-
throw new Error('Mac2: could not find Billing / Billing & Usage after opening Settings');
119+
// Click the Settings tab button in the bottom bar using aria-label
120+
const settingsTabXpath =
121+
'//XCUIElementTypeButton[contains(@label, "Settings") or contains(@title, "Settings")]';
122+
const candidates = await browser.$$(settingsTabXpath);
123+
124+
let clicked = false;
125+
for (const btn of candidates) {
126+
try {
127+
const title = (await btn.getAttribute('title')) || '';
128+
const label = (await btn.getAttribute('label')) || '';
129+
// The tab bar button has a short title/label ("Settings"),
130+
// not a long description like settings menu items
131+
if (
132+
title === 'Settings' ||
133+
label === 'Settings' ||
134+
(title.length < 20 && title.includes('Settings'))
135+
) {
136+
const loc = await btn.getLocation();
137+
const size = await btn.getSize();
138+
const cx = Math.round(loc.x + size.width / 2);
139+
const cy = Math.round(loc.y + size.height / 2);
140+
await browser.performActions([
141+
{
142+
type: 'pointer',
143+
id: 'mouse1',
144+
parameters: { pointerType: 'mouse' },
145+
actions: [
146+
{ type: 'pointerMove', duration: 10, x: cx, y: cy },
147+
{ type: 'pointerDown', button: 0 },
148+
{ type: 'pause', duration: 50 },
149+
{ type: 'pointerUp', button: 0 },
150+
],
151+
},
152+
]);
153+
await browser.releaseActions();
154+
clicked = true;
155+
console.log(`[E2E] Mac2 clicked Settings tab button at (${cx}, ${cy})`);
156+
break;
157+
}
158+
} catch {
159+
continue;
160+
}
116161
}
117-
await browser.pause(2_000);
118-
console.log(`[E2E] Mac2 navigated to ${hash} via Settings → ${sub}`);
119-
} catch (err) {
120-
const msg = err instanceof Error ? err.message : String(err);
121-
throw new Error(`[E2E] Mac2: failed to navigate to ${hash}: ${msg}`);
122-
}
123-
return;
124-
}
125162

126-
// Appium Mac2 — nested settings routes via Skills built-in cards
127-
const SKILLS_BUILTIN_ROUTES: Record<string, string[]> = {
128-
'/settings/screen-intelligence': ['Screen Intelligence'],
129-
'/settings/voice': ['Voice Intelligence', 'Voice Dictation'],
130-
'/settings/autocomplete': ['Text Auto-Complete', 'Inline Autocomplete'],
131-
};
132-
const builtInLabels = SKILLS_BUILTIN_ROUTES[normalized];
133-
if (builtInLabels) {
134-
try {
135-
// Navigate to Skills page first, then click the built-in card
136-
await clickText('Skills', 12_000);
137-
await browser.pause(2_000);
138-
const sub = await clickFirstMatch(builtInLabels, 12_000);
139-
if (!sub) {
140-
// Fallback: try Settings sidebar → Automation menu item
163+
if (!clicked) {
164+
// Fallback: try clickText
165+
console.log('[E2E] Mac2 Settings tab button not found, falling back to clickText');
141166
await clickText('Settings', 12_000);
142-
await browser.pause(1_500);
143-
const settingsSub = await clickFirstMatch(builtInLabels, 12_000);
144-
if (!settingsSub) {
145-
throw new Error(
146-
`Mac2: could not find ${builtInLabels.join(' / ')} in Skills or Settings`
147-
);
167+
}
168+
169+
await browser.pause(3_000);
170+
console.log(`[E2E] Mac2 navigated to /settings`);
171+
172+
// For sub-pages, click the menu item on the Settings home page
173+
if (normalized !== '/settings' && normalized.startsWith('/settings/')) {
174+
// Order: [section group, then item within that section]
175+
// Settings home → section page → specific panel
176+
const SETTINGS_SUB_ROUTES: Record<string, string[]> = {
177+
'/settings/billing': ['Account & Security', 'Billing & Usage'],
178+
'/settings/recovery-phrase': ['Account & Security', 'Recovery Phrase'],
179+
'/settings/team': ['Account & Security', 'Team'],
180+
'/settings/connections': ['Account & Security', 'Connections'],
181+
'/settings/screen-intelligence': ['Automation & Channels', 'Screen Intelligence'],
182+
'/settings/messaging': ['Automation & Channels', 'Messaging Channels'],
183+
'/settings/autocomplete': ['Automation & Channels', 'Inline Autocomplete'],
184+
'/settings/cron-jobs': ['Automation & Channels', 'Cron Jobs'],
185+
'/settings/local-model': ['AI & Skills', 'Local AI Model'],
186+
'/settings/voice': ['AI & Skills', 'Voice Dictation'],
187+
'/settings/ai': ['AI & Skills', 'AI Configuration'],
188+
'/settings/tools': ['AI & Skills', 'Tools'],
189+
'/settings/developer-options': ['Developer Options'],
190+
'/settings/memory-debug': ['Developer Options', 'Memory Debug'],
191+
'/settings/webhooks-debug': ['Developer Options', 'Webhooks Debug'],
192+
'/settings/privacy': ['Privacy'],
193+
};
194+
195+
const subLabels = SETTINGS_SUB_ROUTES[normalized];
196+
if (subLabels) {
197+
// Settings home shows section groups (Account & Security, etc.)
198+
// Each group is a button — click the section first, then the item
199+
for (const label of subLabels) {
200+
try {
201+
await clickText(label, 8_000);
202+
await browser.pause(1_500);
203+
console.log(`[E2E] Mac2 clicked Settings item: "${label}"`);
204+
} catch {
205+
console.log(`[E2E] Mac2 Settings item "${label}" not found, skipping`);
206+
}
207+
}
208+
await browser.pause(1_500);
209+
console.log(`[E2E] Mac2 navigated to ${hash}`);
148210
}
149-
await browser.pause(2_000);
150-
console.log(`[E2E] Mac2 navigated to ${hash} via Settings → ${settingsSub}`);
151-
return;
152211
}
153-
await browser.pause(2_000);
154-
console.log(`[E2E] Mac2 navigated to ${hash} via Skills → ${sub}`);
155212
} catch (err) {
156213
const msg = err instanceof Error ? err.message : String(err);
157-
throw new Error(`[E2E] Mac2: failed to navigate to ${hash}: ${msg}`);
214+
console.log(`[E2E] Mac2: failed to navigate to ${hash}: ${msg}`);
158215
}
159216
return;
160217
}

0 commit comments

Comments
 (0)