From b237617c35aac5d284fc17dbd290e8727b2b28c7 Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Sat, 18 Jul 2026 18:02:49 +0300 Subject: [PATCH] docs: add implementation plan for Reddit post Playwright sidecar Co-Authored-By: Claude Sonnet 5 --- .../plans/2026-07-18-reddit-post-sidecar.md | 441 ++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-18-reddit-post-sidecar.md diff --git a/docs/superpowers/plans/2026-07-18-reddit-post-sidecar.md b/docs/superpowers/plans/2026-07-18-reddit-post-sidecar.md new file mode 100644 index 0000000..8cf9463 --- /dev/null +++ b/docs/superpowers/plans/2026-07-18-reddit-post-sidecar.md @@ -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.