Live verification against a real Reddit login page (from this dev sandbox) showed: - The actual submit button has no [type="submit"] attribute; its accessible name is "Log In" — switched to getByRole with that name. - Username/password fields are better targeted via their visible labels (getByLabel) than brittle name= attribute selectors. - A cookie-consent dialog can overlay the form and intercept clicks; it's now dismissed via "Reject Optional Cookies" before filling in. - This sandbox's network is flagged by Reddit's bot detection with a "blocked by network security" interstitial before the login form even loads — this is unrelated to credentials and unrelated to the target account (confirmed reachable fine from a residential network/browser). Added explicit detection for this state so it reports a clear, distinguishable error instead of a generic CAPTCHA/2FA guess. Full live end-to-end verification (successful login + post creation) must be run from a network Reddit does not block, e.g. the mini PC this app actually runs on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
121 lines
4.4 KiB
JavaScript
121 lines
4.4 KiB
JavaScript
// @ts-check
|
|
const { test, expect } = require('@playwright/test');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const RESULT_PATH = path.join(__dirname, '..', 'test-results', 'result.json');
|
|
|
|
function writeResult(result) {
|
|
fs.mkdirSync(path.dirname(RESULT_PATH), { recursive: true });
|
|
fs.writeFileSync(RESULT_PATH, JSON.stringify(result));
|
|
}
|
|
|
|
test.describe('reddit post', () => {
|
|
let outcome = { success: false, error: 'Test did not complete' };
|
|
let outcomeFinalized = false;
|
|
|
|
test.afterEach(async () => {
|
|
writeResult(outcome);
|
|
});
|
|
|
|
test('logs into Reddit and creates a post', async ({ page }) => {
|
|
const username = process.env.REDDIT_USERNAME;
|
|
const password = process.env.REDDIT_PASSWORD;
|
|
const subreddit = process.env.SUBREDDIT;
|
|
const title = process.env.POST_TITLE;
|
|
const body = process.env.POST_BODY;
|
|
|
|
for (const [name, value] of Object.entries({
|
|
REDDIT_USERNAME: username,
|
|
REDDIT_PASSWORD: password,
|
|
SUBREDDIT: subreddit,
|
|
POST_TITLE: title,
|
|
POST_BODY: body,
|
|
})) {
|
|
if (!value) {
|
|
outcome = { success: false, error: `Missing required environment variable: ${name}` };
|
|
outcomeFinalized = true;
|
|
throw new Error(outcome.error);
|
|
}
|
|
}
|
|
|
|
try {
|
|
await page.goto('https://www.reddit.com/login/');
|
|
|
|
const blockedHeading = page.getByText(/blocked by network security/i);
|
|
if (await blockedHeading.isVisible({ timeout: 3_000 }).catch(() => false)) {
|
|
outcome = {
|
|
success: false,
|
|
error: 'Reddit blocked this request at the network level ("blocked by network security") before login could be attempted — this is not a credentials or selector issue, it means the current network/IP is being flagged as automation by Reddit',
|
|
};
|
|
outcomeFinalized = true;
|
|
throw new Error(outcome.error);
|
|
}
|
|
|
|
const rejectCookies = page.getByRole('button', { name: 'Reject Optional Cookies' });
|
|
if (await rejectCookies.isVisible({ timeout: 5_000 }).catch(() => false)) {
|
|
await rejectCookies.click();
|
|
}
|
|
|
|
const usernameField = page.getByLabel(/email or username/i);
|
|
await usernameField.click();
|
|
await usernameField.fill(/** @type {string} */ (username));
|
|
|
|
const passwordField = page.getByLabel(/^password/i);
|
|
await passwordField.click();
|
|
await passwordField.fill(/** @type {string} */ (password));
|
|
|
|
await page.getByRole('button', { name: 'Log In' }).click();
|
|
|
|
try {
|
|
await expect(page.locator('span[name="avatar"], #USER_DROPDOWN_ID, [icon-name="account-circle-outline"]').first()).toBeVisible({ timeout: 20_000 });
|
|
} catch {
|
|
const loginErrorBanner = page.getByText(/something went wrong logging in|invalid username|invalid password/i);
|
|
const bannerText = await loginErrorBanner.first().textContent().catch(() => null);
|
|
outcome = {
|
|
success: false,
|
|
error: bannerText
|
|
? `Login failed: ${bannerText.trim()}`
|
|
: 'Login did not complete — possible CAPTCHA, 2FA prompt, or invalid credentials',
|
|
};
|
|
outcomeFinalized = true;
|
|
throw new Error(outcome.error);
|
|
}
|
|
|
|
await page.goto(`https://www.reddit.com/r/${subreddit}/submit`);
|
|
|
|
const textTab = page.getByRole('button', { name: /^Text$/i }).first();
|
|
if (await textTab.isVisible({ timeout: 10_000 }).catch(() => false)) {
|
|
await textTab.click();
|
|
}
|
|
|
|
await page.getByPlaceholder(/title/i).first().fill(/** @type {string} */ (title));
|
|
|
|
const bodyField = page.locator('[data-testid="post-composer-rte"], div[role="textbox"]').first();
|
|
await bodyField.click();
|
|
await bodyField.fill(/** @type {string} */ (body));
|
|
|
|
const submitButton = page.getByRole('button', { name: /^Post$/i }).first();
|
|
await expect(submitButton).toBeEnabled({ timeout: 10_000 });
|
|
await submitButton.click();
|
|
|
|
try {
|
|
await page.waitForURL(/\/comments\//, { timeout: 20_000 });
|
|
} catch {
|
|
outcome = { success: false, error: 'Post submission did not complete — no redirect to the new post' };
|
|
outcomeFinalized = true;
|
|
throw new Error(outcome.error);
|
|
}
|
|
|
|
outcome = { success: true, url: page.url() };
|
|
outcomeFinalized = true;
|
|
expect(outcome.success).toBe(true);
|
|
} catch (err) {
|
|
if (!outcomeFinalized) {
|
|
outcome = { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
}
|
|
throw err;
|
|
}
|
|
});
|
|
});
|