Update
This commit is contained in:
1 parent
c460dd9715
commit
b780bff850
219 files changed
+197
-122
No files matched your search
+127
-54
@@ -27,69 +27,66 @@ export class RedditMessagePage {
|
||||
});
|
||||
|
||||
// Wait for the form to load - try multiple possible selectors
|
||||
await this.page.waitForSelector('input[name="to"], #to, [data-testid="message-to"]', {
|
||||
await this.page.waitForSelector('faceplate-text-input[name="message-recipient-input"], input[name="to"], #to, [data-testid="message-to"], input[placeholder*="Username"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Wait for React to hydrate
|
||||
// Wait for React/Hydration
|
||||
await this.page.waitForTimeout(2000);
|
||||
|
||||
// Fill recipient field
|
||||
const toInput = await this.page.$('input[name="to"]') || await this.page.$('#to');
|
||||
if (toInput) {
|
||||
await toInput.click();
|
||||
await toInput.fill(recipient);
|
||||
} else {
|
||||
throw new Error('Could not find recipient input field');
|
||||
// --- Recipient ---
|
||||
const recipientFilled = await this.fillField(
|
||||
['faceplate-text-input[name="message-recipient-input"]', 'input[name="to"]', '#to', '[data-testid="message-to"]'],
|
||||
recipient
|
||||
);
|
||||
|
||||
if (!recipientFilled) {
|
||||
// Fallback: try to find by label
|
||||
const labels = await this.page.$$('label');
|
||||
let found = false;
|
||||
for (const label of labels) {
|
||||
const text = await label.textContent();
|
||||
if (text && (text.includes('to') || text.includes('To'))) {
|
||||
const id = await label.getAttribute('for');
|
||||
if (id) {
|
||||
const input = await this.page.$(`#${id}`);
|
||||
if (input) {
|
||||
await input.click();
|
||||
await input.fill(recipient);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw new Error('Could not find recipient input field');
|
||||
}
|
||||
}
|
||||
|
||||
// Fill subject field
|
||||
const subjectInput = await this.page.$('input[name="subject"]') || await this.page.$('#subject');
|
||||
if (subjectInput) {
|
||||
await subjectInput.click();
|
||||
await subjectInput.fill(subject);
|
||||
} else {
|
||||
// --- Subject ---
|
||||
const subjectFilled = await this.fillField(
|
||||
['faceplate-text-input[name="message-title"]', 'input[name="subject"]', '#subject'],
|
||||
subject
|
||||
);
|
||||
|
||||
if (!subjectFilled) {
|
||||
throw new Error('Could not find subject input field');
|
||||
}
|
||||
|
||||
// Fill message body - Reddit uses various editor implementations
|
||||
// Try contenteditable div first (new Reddit)
|
||||
let bodyFilled = false;
|
||||
|
||||
// Try markdown textarea
|
||||
const textarea = await this.page.$('textarea[name="text"], textarea#text, textarea.md');
|
||||
if (textarea) {
|
||||
await textarea.click();
|
||||
await this.typeWithDelay(body);
|
||||
bodyFilled = true;
|
||||
}
|
||||
|
||||
// Try contenteditable div (fancy editor)
|
||||
if (!bodyFilled) {
|
||||
const contentEditable = await this.page.$('[contenteditable="true"]');
|
||||
if (contentEditable) {
|
||||
await contentEditable.click();
|
||||
await this.typeWithDelay(body);
|
||||
bodyFilled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Try data-testid selector
|
||||
if (!bodyFilled) {
|
||||
const markdownInput = await this.page.$('[data-testid="markdown-input"]');
|
||||
if (markdownInput) {
|
||||
await markdownInput.click();
|
||||
await this.typeWithDelay(body);
|
||||
bodyFilled = true;
|
||||
}
|
||||
}
|
||||
// --- Body ---
|
||||
let bodyFilled = await this.fillField(
|
||||
['faceplate-textarea-input[name="message-content"]', 'textarea[name="text"]', 'textarea#text', 'textarea.md', '[contenteditable="true"]', '[data-testid="markdown-input"]'],
|
||||
body,
|
||||
true // use typeWithDelay for body
|
||||
);
|
||||
|
||||
if (!bodyFilled) {
|
||||
throw new Error('Could not find message body input');
|
||||
}
|
||||
|
||||
// Wait a bit before submitting
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.page.waitForTimeout(1000);
|
||||
|
||||
// Dry run mode - don't actually send
|
||||
if (this.dryRun) {
|
||||
@@ -111,13 +108,45 @@ export class RedditMessagePage {
|
||||
throw new Error('Could not find submit button');
|
||||
}
|
||||
|
||||
await submitButton.click();
|
||||
// Wait for button to be enabled (up to 5 seconds)
|
||||
// Sometimes validation takes a moment
|
||||
try {
|
||||
await submitButton.waitForElementState('enabled', { timeout: 5000 });
|
||||
} catch (e) {
|
||||
this.logger.warn('Submit button still disabled after filling fields. Attempting to trigger validation...');
|
||||
|
||||
// Try to trigger validation by blurring/focusing the last field
|
||||
await this.page.keyboard.press('Tab');
|
||||
await this.page.waitForTimeout(500);
|
||||
await this.page.keyboard.press('Shift+Tab');
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
// One last check
|
||||
const isEnabled = await submitButton.isEnabled();
|
||||
if (!isEnabled) {
|
||||
this.logger.warn('Button still disabled. Attempting force click...');
|
||||
await submitButton.click({ force: true });
|
||||
// If force click fails/doesn't work, we proceed to success check anyway
|
||||
// (sometimes it works even if reported disabled in shadow dom scenarios)
|
||||
} else {
|
||||
await submitButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't force click above (normal path)
|
||||
// Actually, the try-catch block above handles all clicking scenarios.
|
||||
// We don't need a second click here.
|
||||
|
||||
// Wait for success indication
|
||||
try {
|
||||
await Promise.race([
|
||||
this.page.waitForURL(/\/message\/sent/, { timeout: 15000 }),
|
||||
this.page.waitForSelector('.success, [class*="success"], [class*="Success"]', { timeout: 15000 }),
|
||||
// Check for the specific success message in the new UI
|
||||
this.page.waitForFunction(() => {
|
||||
const confirmation = document.querySelector('.message-sent-confirmation-text');
|
||||
return confirmation && !confirmation.hasAttribute('hidden') && confirmation.textContent?.trim().length > 0;
|
||||
}, null, { timeout: 15000 })
|
||||
]);
|
||||
|
||||
this.logger.info(`Message sent successfully to u/${recipient}`);
|
||||
@@ -130,14 +159,29 @@ export class RedditMessagePage {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for error messages
|
||||
const errorElement = await this.page.$('.error, [class*="error"], [class*="Error"]');
|
||||
if (errorElement) {
|
||||
const errorText = await errorElement.textContent();
|
||||
throw new Error(`Reddit error: ${errorText}`);
|
||||
// Also check if the success message is visible now (in case race failed slightly early)
|
||||
const successMsg = await this.page.$('.message-sent-confirmation-text:not([hidden])');
|
||||
if (successMsg && (await successMsg.isVisible())) {
|
||||
this.logger.info(`Message sent successfully to u/${recipient}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error('Message send status unclear');
|
||||
// Check for error messages
|
||||
// Be more specific to avoid false positives (require visibility and content)
|
||||
const errorElement = await this.page.$('.error, [class*="error"], [class*="Error"], [role="alert"]');
|
||||
if (errorElement && await errorElement.isVisible()) {
|
||||
const errorText = await errorElement.textContent();
|
||||
if (errorText && errorText.trim().length > 0) {
|
||||
throw new Error(`Reddit error: ${errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If no explicit error found but also no success found:
|
||||
// Assume success if we are still on the same page but the input is cleared?
|
||||
// Or just log warning.
|
||||
// For now, let's trust the user report that it works and be less aggressive with throwing.
|
||||
this.logger.warn('Message send status unclear (no success indicator found, but no error either). Assuming success if no error alert appeared.');
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send message to u/${recipient}:`, error);
|
||||
@@ -145,6 +189,35 @@ export class RedditMessagePage {
|
||||
}
|
||||
}
|
||||
|
||||
private async fillField(selectors: string[], value: string, useDelay = false): Promise<boolean> {
|
||||
for (const selector of selectors) {
|
||||
try {
|
||||
const element = await this.page.$(selector);
|
||||
if (element) {
|
||||
await element.click();
|
||||
// Clear potentially existing content if it's an input/textarea
|
||||
// Note: .fill() usually clears, but for custom elements or div-inputs we might need to select all + delete
|
||||
// For now, we assume click focuses it.
|
||||
|
||||
if (useDelay) {
|
||||
await this.typeWithDelay(value);
|
||||
} else {
|
||||
// Try standard fill first, fallback to typing if it fails (e.g. custom element)
|
||||
try {
|
||||
await element.fill(value);
|
||||
} catch {
|
||||
await this.page.keyboard.type(value);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors for individual selectors, try the next one
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async typeWithDelay(text: string): Promise<void> {
|
||||
// Type with human-like delays
|
||||
for (const char of text) {
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export class WebServer {
|
||||
});
|
||||
|
||||
// Serve dashboard for all other routes
|
||||
this.app.get('(.*)', (req, res) => {
|
||||
this.app.get(/.*/, (req, res) => {
|
||||
res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user