-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathweb-automation.ts
More file actions
266 lines (232 loc) · 7.16 KB
/
Copy pathweb-automation.ts
File metadata and controls
266 lines (232 loc) · 7.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import { chromium, Browser, Page } from 'playwright';
export class WebAutomation {
private browser: Browser | null = null;
private page: Page | null = null;
async init(headless: boolean = false, devtools: boolean = false): Promise<void> {
const args = ['--start-maximized'];
if (devtools) {
args.push('--auto-open-devtools-for-tabs');
}
this.browser = await chromium.launchPersistentContext('./browser-data', {
headless,
slowMo: 100,
viewport: null,
args,
});
this.page = this.browser.pages()[0] || await this.browser.newPage();
}
async navigateToLogin(url: string): Promise<void> {
if (!this.page) throw new Error('Browser not initialized');
console.log(`🌐 Navigating to ${url}`);
await this.page.goto(url, { waitUntil: 'networkidle' });
}
/**
* Enters email and submits the form
* Adjust selectors based on actual Expensify login page structure
*/
async enterEmail(email: string): Promise<boolean> {
if (!this.page) throw new Error('Browser not initialized');
// Wait for email input to be visible
// Try multiple possible selectors
const emailSelectors = [
'input[type="email"]',
'input[name="email"]',
'input[id*="email"]',
'input[placeholder*="email" i]',
'input[placeholder*="Email" i]',
];
let emailInput = null;
for (const selector of emailSelectors) {
try {
emailInput = await this.page.waitForSelector(selector, { timeout: 5000 });
if (emailInput) break;
} catch (e) {
// Try next selector
}
}
if (!emailInput) {
const url = this.page.url();
if (!url.includes('/login') && !url.includes('/signin')) {
console.log('Already logged in, skipping login process');
return false;
}
throw new Error('Could not find email input field');
}
console.log(`✉️ Entering email: ${email}`);
await this.page.fill(emailSelectors.find(s => emailInput) || emailSelectors[0], email);
// Find and click submit button
const submitSelectors = [
'button[type="submit"]',
'button:has-text("Continue")',
'button:has-text("Send")',
'button:has-text("Next")',
'[role="button"]:has-text("Continue")',
];
let submitted = false;
for (const selector of submitSelectors) {
try {
const button = await this.page.$(selector);
if (button) {
await button.click();
submitted = true;
console.log('✅ Email submitted');
break;
}
} catch (e) {
// Try next selector
}
}
if (!submitted) {
// Try pressing Enter as fallback
await this.page.press(emailSelectors[0], 'Enter');
console.log('✅ Email submitted (via Enter key)');
}
// Wait a bit for the form to process
await this.page.waitForTimeout(1000);
return true;
}
/**
* Enters the verification code and submits
*/
async enterCode(code: string): Promise<void> {
if (!this.page) throw new Error('Browser not initialized');
// Wait for code input to appear
// Ordered by likelihood for Expensify
const codeSelectors = [
'input[inputmode="numeric"]',
'input[type="number"]',
'input[type="text"][name*="code" i]',
'input[type="text"][id*="code" i]',
'input[placeholder*="code" i]',
'input[placeholder*="magic" i]',
];
console.log('⏳ Waiting for code input field...');
let codeInput = null;
let foundSelector = '';
for (const selector of codeSelectors) {
try {
codeInput = await this.page.waitForSelector(selector, { timeout: 2000 });
if (codeInput) {
foundSelector = selector;
console.log(`🔑 Found code input with selector: ${selector}`);
break;
}
} catch (e) {
// Try next selector
}
}
if (!codeInput || !foundSelector) {
// Log page content for debugging
console.error('Available inputs on page:', await this.page.$$eval('input', (inputs) =>
inputs.map(i => ({
type: i.type,
name: i.name,
id: i.id,
placeholder: i.placeholder,
inputmode: i.inputMode,
}))
));
throw new Error('Could not find code input field');
}
console.log(`🔑 Entering code: ${code}`);
await this.page.fill(foundSelector, code);
// Submit the code
const submitSelectors = [
'button[type="submit"]',
'button:has-text("Continue")',
'button:has-text("Verify")',
'button:has-text("Login")',
'[role="button"]:has-text("Continue")',
];
let submitted = false;
for (const selector of submitSelectors) {
try {
const button = await this.page.$(selector);
if (button) {
await button.click();
submitted = true;
console.log('✅ Code submitted');
break;
}
} catch (e) {
// Try next selector
}
}
if (!submitted) {
// Try pressing Enter as fallback
await this.page.press(foundSelector, 'Enter');
console.log('✅ Code submitted (via Enter key)');
}
// Wait a bit for login to complete
await this.page.waitForTimeout(2000);
}
/**
* Waits for login success indicator
*/
async waitForLoginSuccess(
successIndicators: string[] = [
'[data-testid="workspace"]',
'.workspace',
'[aria-label*="workspace" i]',
'nav',
'header',
]
): Promise<void> {
if (!this.page) throw new Error('Browser not initialized');
console.log('⏳ Waiting for login to complete...');
// Wait for any success indicator
for (const selector of successIndicators) {
try {
await this.page.waitForSelector(selector, { timeout: 10000 });
console.log('✅ Login successful!');
return;
} catch (e) {
// Try next indicator
}
}
// If no specific indicator found, just wait a bit and check URL
await this.page.waitForTimeout(3000);
const url = this.page.url();
if (!url.includes('/login') && !url.includes('/signin')) {
console.log('✅ Login successful! (URL changed)');
return;
}
console.log('⚠️ Could not confirm login success, but continuing...');
}
/**
* Logs out from Expensify
*/
async logout(): Promise<void> {
if (!this.page) throw new Error('Browser not initialized');
const logoutSelectors = [
'button:has-text("Logout")',
'button:has-text("Log out")',
'[data-testid="logout"]',
'[aria-label*="logout" i]',
'a[href*="logout"]',
];
for (const selector of logoutSelectors) {
try {
const button = await this.page.$(selector);
if (button) {
await button.click();
console.log('👋 Logged out');
await this.page.waitForTimeout(1000);
return;
}
} catch (e) {
// Try next selector
}
}
console.log('⚠️ Could not find logout button');
}
async close(): Promise<void> {
if (this.browser) {
await this.browser.close();
console.log('🔒 Browser closed');
}
}
getPage(): Page | null {
return this.page;
}
}