|
| 1 | +// Submit a Comment-style review on PR #121 (no Approve, no Request changes |
| 2 | +// — won't advance the stage label) and capture the resulting post-submit |
| 3 | +// states on the Conversation and Files-changed tabs. |
| 4 | +// |
| 5 | +// Outputs: |
| 6 | +// planning/assets/captures/conversation-post-submit.png |
| 7 | +// planning/assets/captures/files-changed-post-submit.png |
| 8 | +// planning/assets/captures/post-submit.coords.json |
| 9 | +// |
| 10 | +// The submitted review is clearly marked as a documentation-screenshot |
| 11 | +// artifact in the comment body so future viewers of PR #121 don't |
| 12 | +// mistake it for a real review. |
| 13 | + |
| 14 | +import { chromium } from 'playwright'; |
| 15 | +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; |
| 16 | +import { resolve } from 'node:path'; |
| 17 | + |
| 18 | +const AUTH_FILE = resolve(process.cwd(), '.playwright-auth', 'github.json'); |
| 19 | +const OUT_DIR = resolve(process.cwd(), 'planning', 'assets', 'captures'); |
| 20 | +const FILES_URL = 'https://github.com/USACE-RMC/RMC-Software-Documentation/pull/121/files'; |
| 21 | +const PR_URL = 'https://github.com/USACE-RMC/RMC-Software-Documentation/pull/121'; |
| 22 | + |
| 23 | +const REVIEW_BODY = '**[Documentation training sandbox]** This review was submitted programmatically by the figure-capture pipeline to produce screenshots for the Reviewer Workflow chapter. It is not a real review of the PR. Ignore.'; |
| 24 | + |
| 25 | +if (!existsSync(AUTH_FILE)) { console.error('Missing auth file.'); process.exit(1); } |
| 26 | +mkdirSync(OUT_DIR, { recursive: true }); |
| 27 | + |
| 28 | +const browser = await chromium.launch({ headless: true }); |
| 29 | +const context = await browser.newContext({ storageState: AUTH_FILE, viewport: { width: 1440, height: 900 } }); |
| 30 | +const page = await context.newPage(); |
| 31 | + |
| 32 | +console.log(`→ ${FILES_URL}`); |
| 33 | +await page.goto(FILES_URL, { waitUntil: 'domcontentloaded' }); |
| 34 | +await page.waitForTimeout(2500); |
| 35 | +await page.locator('a:has-text("Dismiss"), button:has-text("Dismiss"), button:has-text("Got it")').first().click({ timeout: 1500 }).catch(() => {}); |
| 36 | +await page.waitForTimeout(500); |
| 37 | + |
| 38 | +// 1. Open the Submit-review dialog |
| 39 | +console.log(' opening submit-review dialog...'); |
| 40 | +await page.locator('button:has-text("Submit review"), button:has-text("Add your review"), button:has-text("Finish your review")').first().click({ timeout: 4000 }); |
| 41 | +await page.waitForTimeout(1200); |
| 42 | + |
| 43 | +// 2. Fill the comment textarea |
| 44 | +console.log(' typing review body...'); |
| 45 | +const textarea = page.locator('textarea[placeholder*="comment" i], textarea[name*="body" i], textarea').first(); |
| 46 | +await textarea.fill(REVIEW_BODY, { timeout: 4000 }); |
| 47 | +await page.waitForTimeout(400); |
| 48 | + |
| 49 | +// 3. Confirm "Comment" radio is selected (it's the default, but be explicit) |
| 50 | +const commentRadio = page.locator('input[type="radio"][value="comment" i], input[type="radio"]').first(); |
| 51 | +const checked = await commentRadio.isChecked().catch(() => true); |
| 52 | +if (!checked) await commentRadio.click({ timeout: 2000 }).catch(() => {}); |
| 53 | + |
| 54 | +// 4. Click the dialog's Submit button |
| 55 | +console.log(' submitting review...'); |
| 56 | +// The dialog has its own Submit button — find by its position inside the |
| 57 | +// overlay rather than the top-right open-dialog button. |
| 58 | +await page.locator('div[role="dialog"] button:has-text("Submit review"), .Overlay button:has-text("Submit review")').first().click({ timeout: 5000 }).catch(async () => { |
| 59 | + // Fallback — click any Submit review button visible after dialog open |
| 60 | + await page.locator('button:has-text("Submit review")').last().click({ timeout: 4000 }); |
| 61 | +}); |
| 62 | + |
| 63 | +// Wait for the dialog to close and the page to refresh state |
| 64 | +await page.waitForTimeout(4000); |
| 65 | +console.log(' review submitted.'); |
| 66 | + |
| 67 | +// 5. Capture Files-changed post-submit state (no "Add your review" anymore) |
| 68 | +await page.locator('a:has-text("Dismiss"), button:has-text("Dismiss"), button:has-text("Got it")').first().click({ timeout: 1500 }).catch(() => {}); |
| 69 | +await page.screenshot({ path: resolve(OUT_DIR, 'files-changed-post-submit.png'), fullPage: false }); |
| 70 | +console.log(' wrote files-changed-post-submit.png'); |
| 71 | + |
| 72 | +// 6. Navigate to Conversation tab to see the review timeline entry |
| 73 | +console.log(`→ ${PR_URL}`); |
| 74 | +await page.goto(PR_URL, { waitUntil: 'domcontentloaded' }); |
| 75 | +await page.waitForTimeout(3000); |
| 76 | +await page.locator('a:has-text("Dismiss"), button:has-text("Dismiss"), button:has-text("Got it")').first().click({ timeout: 1500 }).catch(() => {}); |
| 77 | + |
| 78 | +// Scroll down to find the new review timeline entry |
| 79 | +await page.evaluate(() => { |
| 80 | + const allText = document.body.innerText; |
| 81 | + // Scroll to the bottom so the latest review event is in viewport |
| 82 | + window.scrollTo(0, document.documentElement.scrollHeight); |
| 83 | +}); |
| 84 | +await page.waitForTimeout(800); |
| 85 | + |
| 86 | +await page.screenshot({ path: resolve(OUT_DIR, 'conversation-post-submit.png'), fullPage: true }); |
| 87 | +console.log(' wrote conversation-post-submit.png (full page)'); |
| 88 | + |
| 89 | +// 7. Probe for coords of the new review entry |
| 90 | +const coords = await page.evaluate(() => { |
| 91 | + const r = (el) => { if (!el) return null; const b = el.getBoundingClientRect(); return { x: Math.round(b.x + window.scrollX), y: Math.round(b.y + window.scrollY), w: Math.round(b.width), h: Math.round(b.height) }; }; |
| 92 | + // Look for "rmctestreviewer reviewed" or "left a review" event in the timeline |
| 93 | + let reviewEvent = null; |
| 94 | + for (const el of document.querySelectorAll('div, li, article')) { |
| 95 | + const t = (el.innerText || '').slice(0, 300); |
| 96 | + if (/rmctestreviewer (left a |reviewed|commented)/i.test(t) && el.getBoundingClientRect().height > 40 && el.getBoundingClientRect().height < 800) { |
| 97 | + reviewEvent = el; |
| 98 | + break; |
| 99 | + } |
| 100 | + } |
| 101 | + // Look for the review summary card with the training-sandbox text |
| 102 | + let reviewCard = null; |
| 103 | + for (const el of document.querySelectorAll('div, article, [data-testid="comment-body"], .js-comment, .timeline-comment')) { |
| 104 | + const t = (el.innerText || '').slice(0, 300); |
| 105 | + if (/Documentation training sandbox/i.test(t)) { |
| 106 | + reviewCard = el; |
| 107 | + break; |
| 108 | + } |
| 109 | + } |
| 110 | + return { reviewEvent: r(reviewEvent), reviewCard: r(reviewCard), pageHeight: document.documentElement.scrollHeight }; |
| 111 | +}); |
| 112 | + |
| 113 | +writeFileSync(resolve(OUT_DIR, 'post-submit.coords.json'), JSON.stringify({ url: PR_URL, capturedAt: new Date().toISOString(), elements: coords }, null, 2)); |
| 114 | +console.log(` coords: ${JSON.stringify(coords)}`); |
| 115 | + |
| 116 | +await browser.close(); |
| 117 | +console.log('\nDone.'); |
0 commit comments