This commit is contained in:
jjbrunton committed 2026-01-04 20:26:41 +00:00
1 parent c460dd9715
commit b780bff850
219 files changed
+197 -122

No files matched your search

+127 -54
View File
@@ -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
View File
@@ -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'));
});
}