docs: add implementation plan for Reddit post Playwright sidecar

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Doar Mihai
2026-07-18 18:02:49 +03:00
co-authored by Claude Sonnet 5
parent 44040e7c1e
commit b237617c35
@@ -0,0 +1,441 @@
# Reddit Post Sidecar Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a standalone Playwright acceptance test under `sidecars/reddit-post/` that logs into Reddit and creates a text post in a given subreddit, invocable via a CLI wrapper script that accepts `--username`/`--password`/`--subreddit`/`--title`/`--body`/`--headed` flags and always emits a single JSON result line to stdout with a matching exit code.
**Architecture:** A self-contained Node project (own `package.json`, independent of the Laravel app's PHP/npm toolchain) using `@playwright/test`. One test file drives the browser: login → navigate to submit page → create text post → verify. The test writes its result (success+url, or failure+error) to a fixed JSON file inside its own working directory on every run, success or failure, via a `test.afterEach` hook — this guarantees the file always exists regardless of which assertion failed partway through. A bash wrapper script parses friendly CLI flags, exports them as the env vars the Playwright config/test read, shells out to `npx playwright test`, then reads the result file and echoes it as the final stdout line, exiting with Playwright's own exit code.
**Tech Stack:** Node.js (already installed, v22), `@playwright/test` (installed fresh in this sidecar's own `node_modules`), Chromium (installed via `npx playwright install chromium`), bash for the wrapper script.
## Global Constraints
- This sidecar lives entirely under `sidecars/reddit-post/` — no changes to the Laravel app's `app/`, `routes/`, `database/`, `resources/`, `composer.json`, or root `package.json`.
- Text posts only — no link/image/poll post support.
- No interactive CAPTCHA/2FA handling — if login doesn't reach a logged-in state within a bounded timeout, the test fails with a descriptive error. No human-in-the-loop fallback.
- Headless by default; `HEADLESS=false` (settable via the wrapper's `--headed` flag) switches to a visible browser.
- Output contract, regardless of success or failure: the wrapper's last stdout line is exactly one JSON object — `{"success": true, "url": "..."}` on success, `{"success": false, "error": "..."}` on failure — with exit code `0` or `1` respectively, matching Playwright's own exit code.
- No automated retries in the Playwright config — a retry would mask flakiness in the login/post flow rather than surface it as a real failure.
- `sidecars/reddit-post/node_modules/` and any Playwright output directories (`test-results/`, `playwright-report/`) must be gitignored; they are not currently covered by the repo's root `.gitignore` (which only ignores the anchored `/node_modules` at repo root).
---
## File Structure
- `sidecars/reddit-post/package.json` — new (declares `@playwright/test` dependency)
- `sidecars/reddit-post/playwright.config.js` — new (headless-by-default config, no retries, screenshot-on-failure)
- `sidecars/reddit-post/tests/reddit-post.spec.js` — new (the actual login+post test, writes result JSON via `afterEach`)
- `sidecars/reddit-post/run.sh` — new (CLI wrapper: flags → env vars → `npx playwright test` → echo result JSON → matching exit code)
- `sidecars/reddit-post/README.md` — new (one-time setup note: `npm install && npx playwright install chromium`; usage example)
- `sidecars/reddit-post/.gitignore` — new (`node_modules/`, `test-results/`, `playwright-report/`)
---
## Task 1: Sidecar project scaffold and Playwright config
**Files:**
- Create: `sidecars/reddit-post/package.json`
- Create: `sidecars/reddit-post/playwright.config.js`
- Create: `sidecars/reddit-post/.gitignore`
- Create: `sidecars/reddit-post/README.md`
**Interfaces:**
- Produces: an installable Node project at `sidecars/reddit-post/` with `@playwright/test` available via `npx playwright test`; a config that reads `HEADLESS` (default `true`) from `process.env`, sets a single Chromium project, disables retries, and enables `screenshot: 'only-on-failure'`.
- [ ] **Step 1: Create the package.json**
Create `sidecars/reddit-post/package.json`:
```json
{
"name": "reddit-post-sidecar",
"private": true,
"version": "1.0.0",
"description": "Playwright acceptance test that logs into Reddit and creates a text post, invoked via run.sh",
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.61.1"
}
}
```
- [ ] **Step 2: Install dependencies and the Chromium browser**
Run: `cd sidecars/reddit-post && npm install`
Expected: `node_modules/@playwright/test` installed, `package-lock.json` created.
Run: `cd sidecars/reddit-post && npx playwright install chromium`
Expected: Chromium browser binary downloaded successfully (this can take a minute; look for "Chromium ... downloaded to ..." in the output).
- [ ] **Step 3: Create the Playwright config**
Create `sidecars/reddit-post/playwright.config.js`:
```js
// @ts-check
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests',
timeout: 60_000,
expect: { timeout: 15_000 },
retries: 0,
reporter: [['list']],
use: {
headless: process.env.HEADLESS !== 'false',
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
```
- [ ] **Step 4: Create the sidecar's own .gitignore**
Create `sidecars/reddit-post/.gitignore`:
```
node_modules/
test-results/
playwright-report/
```
- [ ] **Step 5: Create a short README**
Create `sidecars/reddit-post/README.md`:
```markdown
# Reddit Post Sidecar
A Playwright acceptance test that logs into Reddit with supplied credentials
and creates a text post in a given subreddit.
## One-time setup
cd sidecars/reddit-post
npm install
npx playwright install chromium
## Usage
./run.sh --username=YOUR_USERNAME --password=YOUR_PASSWORD \
--subreddit=test --title="Hello" --body="Post body text" [--headed]
Prints a single JSON line to stdout and exits 0 on success, exits 1 on
failure:
{"success": true, "url": "https://www.reddit.com/r/test/comments/.../"}
{"success": false, "error": "Login did not complete — possible CAPTCHA or 2FA prompt"}
```
- [ ] **Step 6: Verify the scaffold is installable and runnable (no test file yet, so expect "no tests found")**
Run: `cd sidecars/reddit-post && npx playwright test`
Expected: Playwright runs and reports "No tests found" (or similar) — this confirms the config loads without errors. This is NOT a failure at this step; there is no test file yet (Task 2 adds it).
- [ ] **Step 7: Commit**
```bash
git add sidecars/reddit-post/package.json sidecars/reddit-post/playwright.config.js sidecars/reddit-post/.gitignore sidecars/reddit-post/README.md
git commit -m "chore: scaffold reddit-post Playwright sidecar project"
```
Note: `package-lock.json` and `node_modules/` are created by Step 2 but `node_modules/` is gitignored (Step 4) — only commit `package-lock.json` alongside the files above if it was generated (it should be):
```bash
git add sidecars/reddit-post/package-lock.json
git commit --amend --no-edit
```
---
## Task 2: Login and post-creation Playwright test
**Files:**
- Create: `sidecars/reddit-post/tests/reddit-post.spec.js`
**Interfaces:**
- Consumes: `playwright.config.js` (Task 1)'s `HEADLESS` env var behavior.
- Produces: a Playwright test named `"logs into Reddit and creates a post"` that reads `REDDIT_USERNAME`, `REDDIT_PASSWORD`, `SUBREDDIT`, `POST_TITLE`, `POST_BODY` from `process.env`; writes its outcome to `sidecars/reddit-post/test-results/result.json` as `{"success": true, "url": string}` or `{"success": false, "error": string}` via an `afterEach` hook that runs regardless of pass/fail.
- [ ] **Step 1: Write the test file**
Create `sidecars/reddit-post/tests/reddit-post.spec.js`:
```js
// @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' };
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}` };
throw new Error(outcome.error);
}
}
try {
await page.goto('https://www.reddit.com/login/');
await page.fill('input[name="username"]', /** @type {string} */ (username));
await page.fill('input[name="password"]', /** @type {string} */ (password));
await page.click('button[type="submit"]');
try {
await expect(page.locator('span[name="avatar"], #USER_DROPDOWN_ID, [icon-name="account-circle-outline"]').first()).toBeVisible({ timeout: 20_000 });
} catch {
outcome = {
success: false,
error: 'Login did not complete — possible CAPTCHA, 2FA prompt, or invalid credentials',
};
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' };
throw new Error(outcome.error);
}
outcome = { success: true, url: page.url() };
expect(outcome.success).toBe(true);
} catch (err) {
if (outcome.success !== false || !outcome.error) {
outcome = { success: false, error: err instanceof Error ? err.message : String(err) };
}
throw err;
}
});
});
```
- [ ] **Step 2: Manually verify against a real (test) Reddit account**
This step requires real Reddit credentials for a test account you control — do not use production credentials during development.
Run:
```bash
cd sidecars/reddit-post
REDDIT_USERNAME=your_test_username \
REDDIT_PASSWORD=your_test_password \
SUBREDDIT=test \
POST_TITLE="Sidecar test post" \
POST_BODY="Verifying the reddit-post sidecar works end to end." \
HEADLESS=false \
npx playwright test
```
Expected: browser opens (since `HEADLESS=false`), logs in, navigates to `r/test/submit`, fills and submits a text post, test passes. Confirm `sidecars/reddit-post/test-results/result.json` contains `{"success": true, "url": "https://www.reddit.com/r/test/comments/..."}`, and that the URL is a real, visitable post.
If Reddit's actual DOM selectors don't match (Reddit's UI changes frequently and the selectors above are best-effort), inspect the live page with Playwright's codegen tool to find correct selectors:
```bash
npx playwright codegen https://www.reddit.com/login/
```
Update the selectors in `reddit-post.spec.js` to match what you observe, then re-run until the flow passes end-to-end.
- [ ] **Step 3: Verify the failure path with wrong credentials**
Run:
```bash
cd sidecars/reddit-post
REDDIT_USERNAME=your_test_username \
REDDIT_PASSWORD=definitely_wrong_password \
SUBREDDIT=test \
POST_TITLE="Should not post" \
POST_BODY="This should fail at login" \
HEADLESS=false \
npx playwright test
```
Expected: test fails (non-zero exit), `test-results/result.json` contains `{"success": false, "error": "Login did not complete — possible CAPTCHA, 2FA prompt, or invalid credentials"}` (or a similar login-failure message — exact wording depends on what Reddit shows for bad credentials, adjust the spec's login-detection logic in Step 1 if needed so this failure is distinguishable).
- [ ] **Step 4: Commit**
```bash
git add sidecars/reddit-post/tests/reddit-post.spec.js
git commit -m "feat: add Reddit login and post-creation Playwright test"
```
---
## Task 3: CLI wrapper script
**Files:**
- Create: `sidecars/reddit-post/run.sh`
**Interfaces:**
- Consumes: `sidecars/reddit-post/tests/reddit-post.spec.js` (Task 2)'s env var contract and its `test-results/result.json` output file.
- Produces: an executable `run.sh` accepting `--username=`, `--password=`, `--subreddit=`, `--title=`, `--body=`, and optional `--headed`; on invocation it runs the Playwright test and, regardless of pass/fail, prints the contents of `test-results/result.json` as the last line of stdout and exits with the same code `npx playwright test` produced.
- [ ] **Step 1: Write the wrapper script**
Create `sidecars/reddit-post/run.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
USERNAME=""
PASSWORD=""
SUBREDDIT=""
TITLE=""
BODY=""
HEADLESS_FLAG="true"
for arg in "$@"; do
case "$arg" in
--username=*) USERNAME="${arg#*=}" ;;
--password=*) PASSWORD="${arg#*=}" ;;
--subreddit=*) SUBREDDIT="${arg#*=}" ;;
--title=*) TITLE="${arg#*=}" ;;
--body=*) BODY="${arg#*=}" ;;
--headed) HEADLESS_FLAG="false" ;;
*)
echo "{\"success\": false, \"error\": \"Unknown argument: $arg\"}"
exit 1
;;
esac
done
MISSING=""
[ -z "$USERNAME" ] && MISSING="$MISSING --username"
[ -z "$PASSWORD" ] && MISSING="$MISSING --password"
[ -z "$SUBREDDIT" ] && MISSING="$MISSING --subreddit"
[ -z "$TITLE" ] && MISSING="$MISSING --title"
[ -z "$BODY" ] && MISSING="$MISSING --body"
if [ -n "$MISSING" ]; then
echo "{\"success\": false, \"error\": \"Missing required argument(s):$MISSING\"}"
exit 1
fi
export REDDIT_USERNAME="$USERNAME"
export REDDIT_PASSWORD="$PASSWORD"
export SUBREDDIT="$SUBREDDIT"
export POST_TITLE="$TITLE"
export POST_BODY="$BODY"
export HEADLESS="$HEADLESS_FLAG"
set +e
npx playwright test tests/reddit-post.spec.js
PLAYWRIGHT_EXIT_CODE=$?
set -e
RESULT_FILE="test-results/result.json"
if [ -f "$RESULT_FILE" ]; then
cat "$RESULT_FILE"
echo
else
echo "{\"success\": false, \"error\": \"Test did not produce a result file\"}"
exit 1
fi
exit "$PLAYWRIGHT_EXIT_CODE"
```
- [ ] **Step 2: Make it executable**
Run: `chmod +x sidecars/reddit-post/run.sh`
- [ ] **Step 3: Verify the wrapper's argument parsing and missing-argument path (no real Reddit call needed for this check)**
Run: `cd sidecars/reddit-post && ./run.sh --username=x`
Expected: prints `{"success": false, "error": "Missing required argument(s): --password --subreddit --title --body"}` and exits 1. This confirms argument parsing and the missing-argument short-circuit work before ever invoking Playwright.
- [ ] **Step 4: Verify the full wrapper path against a real (test) Reddit account**
Run (using real test-account credentials, same caution as Task 2 Step 2):
```bash
cd sidecars/reddit-post
./run.sh --username=your_test_username --password=your_test_password \
--subreddit=test --title="Wrapper test post" \
--body="Verifying run.sh end to end." --headed
```
Expected: last line of stdout is `{"success": true, "url": "https://www.reddit.com/r/test/comments/..."}`, exit code `0` (check with `echo $?` immediately after). Confirm the post is real and visible at that URL.
- [ ] **Step 5: Verify the wrapper's failure exit path**
Run with intentionally wrong credentials:
```bash
cd sidecars/reddit-post
./run.sh --username=your_test_username --password=wrong \
--subreddit=test --title="Should fail" --body="Should fail" --headed
echo "Exit code: $?"
```
Expected: last line of stdout is a `{"success": false, "error": "..."}` JSON object, exit code is `1`.
- [ ] **Step 6: Commit**
```bash
git add sidecars/reddit-post/run.sh
git commit -m "feat: add CLI wrapper script for the reddit-post sidecar"
```
---
## Self-Review Notes
- **Spec coverage:** Login with descriptive failure on non-completion (Task 2), text-only post creation with title/body (Task 2), subreddit argument (Tasks 2 & 3), JSON success/failure output contract with matching exit codes (Tasks 2 & 3), headless-by-default with `--headed` override (Tasks 1 & 3), standalone project structure isolated from the Laravel app (Task 1), gitignore for `node_modules`/Playwright output (Task 1) — all spec requirements are covered.
- **Placeholder scan:** No TBD/TODO markers. Task 2's Reddit DOM selectors are explicitly flagged as best-effort with a concrete fallback procedure (Playwright codegen) rather than left as a vague "figure out selectors" placeholder — this is a real, actionable step, not an unfinished spec.
- **Type consistency:** Env var names (`REDDIT_USERNAME`, `REDDIT_PASSWORD`, `SUBREDDIT`, `POST_TITLE`, `POST_BODY`, `HEADLESS`) are used identically across `playwright.config.js` (Task 1), `reddit-post.spec.js` (Task 2), and `run.sh` (Task 3). The result file path (`test-results/result.json`, relative to `sidecars/reddit-post/`) is consistent between the test's `writeResult()` and the wrapper's `RESULT_FILE` variable.