|
| 1 | +# Codex Commands - PR_26126_025-preview-generator-v2-phase-wildcard-support |
| 2 | + |
| 3 | +```bash |
| 4 | +codex run "Create PR_26126_025-preview-generator-v2-phase-wildcard-support. Update Preview Generator V2 to support phase-wide sample generation. Add \"samples/phase-xx\" to the Samples examples as a valid input pattern meaning generate previews for all sample images in that phase folder. Implement supporting code so when the Paths or IDs input receives a phase folder pattern such as samples/phase-01 or samples/phase-xx, the tool resolves all eligible sample index.html entries/images under that phase and processes them using the existing preview generation behavior. Preserve existing single sample and game behavior. Do not modify samples. Do not add schema. Produce review artifacts." |
| 5 | +``` |
| 6 | + |
| 7 | +## Validation Commands |
| 8 | + |
| 9 | +```bash |
| 10 | +git diff --check -- tools/preview-generator-v2/index.html docs/dev/codex_commands.md docs/dev/commit_comment.txt |
| 11 | +git diff --cached --name-only -- samples games start_of_day tools/shared tools/schemas |
| 12 | +npm run test:workspace-v2 |
| 13 | +npm run codex:review-artifacts |
| 14 | +``` |
| 15 | + |
| 16 | +```powershell |
| 17 | +@' |
| 18 | +import { chromium } from '@playwright/test'; |
| 19 | +import { startRepoServer } from './tests/helpers/playwrightRepoServer.mjs'; |
| 20 | +
|
| 21 | +const server = await startRepoServer(); |
| 22 | +const browser = await chromium.launch({ headless: true }); |
| 23 | +const page = await browser.newPage({ viewport: { width: 1366, height: 900 } }); |
| 24 | +const errors = []; |
| 25 | +const consoleErrors = []; |
| 26 | +page.on('pageerror', (error) => errors.push(error.message)); |
| 27 | +page.on('console', (message) => { |
| 28 | + if (message.type() === 'error') consoleErrors.push(message.text()); |
| 29 | +}); |
| 30 | +await page.route('https://cdn.jsdelivr.net/**', async (route) => { |
| 31 | + await route.fulfill({ status: 200, contentType: 'text/javascript', body: 'window.html2canvas = window.html2canvas || undefined;' }); |
| 32 | +}); |
| 33 | +await page.route('**/samples/phase-01/**/index.html', async (route) => { |
| 34 | + await route.fulfill({ |
| 35 | + status: 200, |
| 36 | + contentType: 'text/html', |
| 37 | + body: '<!doctype html><html><body><canvas id="c" width="12" height="12"></canvas><script>const c=document.getElementById("c");const ctx=c.getContext("2d");ctx.fillStyle="#00ff00";ctx.fillRect(0,0,12,12);</script></body></html>' |
| 38 | + }); |
| 39 | +}); |
| 40 | +await page.addInitScript(() => { |
| 41 | + const writes = []; |
| 42 | + class FakeFile { |
| 43 | + constructor(text) { this._text = text; } |
| 44 | + async text() { return this._text; } |
| 45 | + } |
| 46 | + class FakeFileHandle { |
| 47 | + constructor(name, path, text = '') { this.kind = 'file'; this.name = name; this.path = path; this._text = text; } |
| 48 | + async getFile() { return new FakeFile(this._text); } |
| 49 | + async createWritable() { |
| 50 | + const handle = this; |
| 51 | + return { |
| 52 | + async write(content) { |
| 53 | + handle._text = String(content); |
| 54 | + writes.push({ path: handle.path, content: String(content) }); |
| 55 | + }, |
| 56 | + async close() {} |
| 57 | + }; |
| 58 | + } |
| 59 | + } |
| 60 | + class FakeDirectoryHandle { |
| 61 | + constructor(name = 'HTML-JavaScript-Gaming', path = '') { this.kind = 'directory'; this.name = name; this.path = path; this.children = new Map(); } |
| 62 | + async getDirectoryHandle(name, options = {}) { |
| 63 | + const key = `dir:${name}`; |
| 64 | + if (!this.children.has(key)) { |
| 65 | + if (!options.create) throw new DOMException('Not found', 'NotFoundError'); |
| 66 | + const nextPath = this.path ? `${this.path}/${name}` : name; |
| 67 | + this.children.set(key, new FakeDirectoryHandle(name, nextPath)); |
| 68 | + } |
| 69 | + return this.children.get(key); |
| 70 | + } |
| 71 | + async getFileHandle(name, options = {}) { |
| 72 | + const key = `file:${name}`; |
| 73 | + if (!this.children.has(key)) { |
| 74 | + if (!options.create) throw new DOMException('Not found', 'NotFoundError'); |
| 75 | + const nextPath = this.path ? `${this.path}/${name}` : name; |
| 76 | + this.children.set(key, new FakeFileHandle(name, nextPath)); |
| 77 | + } |
| 78 | + return this.children.get(key); |
| 79 | + } |
| 80 | + async *entries() { |
| 81 | + const sorted = Array.from(this.children.values()).sort((a, b) => b.name.localeCompare(a.name)); |
| 82 | + for (const child of sorted) yield [child.name, child]; |
| 83 | + } |
| 84 | + } |
| 85 | + async function addFile(root, path, text = '') { |
| 86 | + const parts = path.split('/').filter(Boolean); |
| 87 | + let current = root; |
| 88 | + for (const part of parts.slice(0, -1)) current = await current.getDirectoryHandle(part, { create: true }); |
| 89 | + const fileHandle = await current.getFileHandle(parts.at(-1), { create: true }); |
| 90 | + fileHandle._text = text; |
| 91 | + } |
| 92 | + async function addDir(root, path) { |
| 93 | + const parts = path.split('/').filter(Boolean); |
| 94 | + let current = root; |
| 95 | + for (const part of parts) current = await current.getDirectoryHandle(part, { create: true }); |
| 96 | + } |
| 97 | + window.__previewGeneratorV2Writes = writes; |
| 98 | + window.__previewGeneratorV2Root = new FakeDirectoryHandle(); |
| 99 | + window.__previewGeneratorV2Ready = (async () => { |
| 100 | + await addFile(window.__previewGeneratorV2Root, 'samples/phase-01/0101/index.html'); |
| 101 | + await addFile(window.__previewGeneratorV2Root, 'samples/phase-01/0102/index.html'); |
| 102 | + await addDir(window.__previewGeneratorV2Root, 'samples/phase-01/0199'); |
| 103 | + })(); |
| 104 | + window.showDirectoryPicker = async () => { |
| 105 | + await window.__previewGeneratorV2Ready; |
| 106 | + return window.__previewGeneratorV2Root; |
| 107 | + }; |
| 108 | +}); |
| 109 | +
|
| 110 | +await page.goto(`${server.baseUrl}/tools/preview-generator-v2/index.html`, { waitUntil: 'domcontentloaded' }); |
| 111 | +await page.waitForSelector('#sampleList'); |
| 112 | +const placeholder = await page.locator('#sampleList').getAttribute('placeholder'); |
| 113 | +if (!placeholder.includes('samples/phase-xx')) throw new Error('Phase-wide sample example is missing.'); |
| 114 | +await page.fill('#baseUrl', server.baseUrl); |
| 115 | +await page.fill('#waitMs', '1'); |
| 116 | +await page.fill('#sampleList', 'samples/phase-01'); |
| 117 | +await page.check('#targetTypeSamples'); |
| 118 | +await page.click('#pickRepoBtn'); |
| 119 | +await page.waitForFunction(() => !document.getElementById('executeBtn').disabled); |
| 120 | +await page.click('#executeBtn'); |
| 121 | +await page.waitForFunction(() => document.getElementById('log').textContent.includes('Done.'), null, { timeout: 20000 }); |
| 122 | +const logText = await page.locator('#log').innerText(); |
| 123 | +if (!logText.includes('Resolved 2 samples from samples/phase-01.')) throw new Error(`Missing phase resolution log: ${logText}`); |
| 124 | +if (!logText.includes('OK 0101') || !logText.includes('OK 0102')) throw new Error(`Expected both phase samples to process: ${logText}`); |
| 125 | +if (logText.includes('0199')) throw new Error(`Sample folder without index.html should be skipped: ${logText}`); |
| 126 | +const writes = await page.evaluate(() => window.__previewGeneratorV2Writes || []); |
| 127 | +const paths = writes.map((write) => write.path).sort(); |
| 128 | +const expectedPaths = [ |
| 129 | + 'samples/phase-01/0101/assets/images/preview.svg', |
| 130 | + 'samples/phase-01/0102/assets/images/preview.svg' |
| 131 | +]; |
| 132 | +if (JSON.stringify(paths) !== JSON.stringify(expectedPaths)) throw new Error(`Unexpected writes: ${paths.join(' | ')}`); |
| 133 | +if (errors.length || consoleErrors.length) throw new Error([...errors, ...consoleErrors].join(' | ')); |
| 134 | +await browser.close(); |
| 135 | +await server.close(); |
| 136 | +console.log('preview-generator-v2 phase-wide sample generation smoke valid'); |
| 137 | +'@ | node --input-type=module - |
| 138 | +``` |
| 139 | + |
| 140 | +## Notes |
| 141 | + |
| 142 | +The targeted Playwright smoke validates that the `samples/phase-xx` example is present, `samples/phase-01` expands through the selected repo folder into indexed sample entries, non-indexed folders are skipped, and the existing preview generation path writes `preview.svg` for each resolved sample. |
| 143 | + |
| 144 | +`npm run test:workspace-v2` was attempted, but the script is not defined in this checkout. |
| 145 | + |
| 146 | +Full samples smoke test was skipped because this PR is scoped to Preview Generator V2 phase-folder resolution and uses targeted browser coverage instead. |
0 commit comments