|
1 | | -# Codex Commands - PR_26126_013-preview-generator-v2-reskin-fixes |
| 1 | +# Codex Commands - PR_26126_014-preview-generator-v2-layout-polish |
2 | 2 |
|
3 | 3 | ```bash |
4 | | -codex run "Create PR_26126_013-preview-generator-v2-reskin-fixes. Fix Preview Generator V2 reskin only. Preserve existing preview.html functionality. Remove all copied preview.html CSS and use only Palette Manager V2-style HTML/classes plus Preview Generator V2 wrapper classes where needed. Move STOP into the Palette Manager-style NAV next to Generate Preview. Fix Repo selected so it populates correctly from the selected repo destination/folder. Remove the Repo Sample control entirely. Populate the write folder sample text as \"samples\\phaseXX\\XXXX\\assets\\images\". Ensure Write folder is populated correctly. Remove the \"Paths or IDs\" label/field wrapper from the center column structure shown in preview-generator-v2__paths-field. Move the existing textarea to the top of the layout/control flow. Do not add JSON UI. Do not create schema. Do not modify samples. Update targeted tests if needed and produce review artifacts." |
| 4 | +codex run "Create PR_26126_014-preview-generator-v2-layout-polish. Fix Preview Generator V2 UI polish only. Preserve existing generation behavior. Fix Hide Header & Details so it enters the same fullscreen/collapsed-header behavior used by Palette Manager V2. Move Pick Repo Folder above Repo Selected. Status must not use an accordion; render Status as a normal compact status block. Reduce Output Summary height so it fits content instead of stretching tall; keep Write folder sample and Write folder compact and readable. Do not modify samples. Do not add schema. Produce review artifacts." |
5 | 5 | ``` |
6 | 6 |
|
7 | 7 | ## Validation Commands |
@@ -34,189 +34,67 @@ await page.route('https://cdn.jsdelivr.net/**', async (route) => { |
34 | 34 | }); |
35 | 35 | await page.addInitScript(() => { |
36 | 36 | const writes = []; |
37 | | - class FakeFile { |
38 | | - constructor(text) { |
39 | | - this._text = text; |
40 | | - } |
41 | | - async text() { |
42 | | - return this._text; |
43 | | - } |
44 | | - } |
| 37 | + class FakeFile { constructor(text) { this._text = text; } async text() { return this._text; } } |
45 | 38 | class FakeFileHandle { |
46 | | - constructor(path, existing = null) { |
47 | | - this.kind = 'file'; |
48 | | - this.name = path.split('/').pop(); |
49 | | - this.path = path; |
50 | | - this._existing = existing; |
51 | | - } |
52 | | - async getFile() { |
53 | | - return new FakeFile(this._existing || ''); |
54 | | - } |
55 | | - async createWritable() { |
56 | | - const path = this.path; |
57 | | - return { |
58 | | - async write(content) { |
59 | | - writes.push({ path, content: String(content) }); |
60 | | - }, |
61 | | - async close() {} |
62 | | - }; |
63 | | - } |
| 39 | + constructor(path, existing = null) { this.kind = 'file'; this.name = path.split('/').pop(); this.path = path; this._existing = existing; } |
| 40 | + async getFile() { return new FakeFile(this._existing || ''); } |
| 41 | + async createWritable() { const path = this.path; return { async write(content) { writes.push({ path, content: String(content) }); }, async close() {} }; } |
64 | 42 | } |
65 | 43 | class FakeDirectoryHandle { |
66 | | - constructor(name = 'SelectedRepoFolder', path = '') { |
67 | | - this.kind = 'directory'; |
68 | | - this.name = name; |
69 | | - this.path = path; |
70 | | - this.children = new Map(); |
71 | | - } |
72 | | - async getDirectoryHandle(name) { |
73 | | - const key = `dir:${name}`; |
74 | | - if (!this.children.has(key)) { |
75 | | - const nextPath = this.path ? `${this.path}/${name}` : name; |
76 | | - this.children.set(key, new FakeDirectoryHandle(name, nextPath)); |
77 | | - } |
78 | | - return this.children.get(key); |
79 | | - } |
80 | | - async getFileHandle(name, options = {}) { |
81 | | - const key = `file:${name}`; |
82 | | - if (!this.children.has(key)) { |
83 | | - if (!options.create) { |
84 | | - throw new DOMException('Not found', 'NotFoundError'); |
85 | | - } |
86 | | - const nextPath = this.path ? `${this.path}/${name}` : name; |
87 | | - this.children.set(key, new FakeFileHandle(nextPath)); |
88 | | - } |
89 | | - return this.children.get(key); |
90 | | - } |
| 44 | + constructor(name = 'SelectedRepoFolder', path = '') { this.kind = 'directory'; this.name = name; this.path = path; this.children = new Map(); } |
| 45 | + async getDirectoryHandle(name) { const key = `dir:${name}`; if (!this.children.has(key)) { const nextPath = this.path ? `${this.path}/${name}` : name; this.children.set(key, new FakeDirectoryHandle(name, nextPath)); } return this.children.get(key); } |
| 46 | + async getFileHandle(name, options = {}) { const key = `file:${name}`; if (!this.children.has(key)) { if (!options.create) { throw new DOMException('Not found', 'NotFoundError'); } const nextPath = this.path ? `${this.path}/${name}` : name; this.children.set(key, new FakeFileHandle(nextPath)); } return this.children.get(key); } |
91 | 47 | } |
92 | 48 | window.__previewGeneratorV2Writes = writes; |
93 | 49 | window.showDirectoryPicker = async () => new FakeDirectoryHandle(); |
94 | 50 | }); |
95 | 51 |
|
96 | 52 | await page.goto(`${server.baseUrl}/tools/preview-generator-v2/index.html`, { waitUntil: 'domcontentloaded' }); |
97 | 53 | await page.waitForSelector('#shared-theme-header'); |
| 54 | +await page.waitForFunction(() => document.querySelector('[data-preview-generator-v2-summary]')?.dataset.toolsPlatformSummaryActive === '1'); |
98 | 55 |
|
99 | | -const requiredSelectors = [ |
100 | | - '.palette-manager-v2__menu-sample', |
101 | | - '#executeBtn', |
102 | | - '#stopBtn', |
103 | | - '#pickRepoBtn', |
104 | | - '#targetTypeSamples', |
105 | | - '#targetTypeGames', |
106 | | - '#targetTypeTools', |
107 | | - '#baseUrl', |
108 | | - '#waitMs', |
109 | | - '#assetFolder', |
110 | | - '#forceRewrite', |
111 | | - '#onlyCaptureTimeout', |
112 | | - '#captureModeFullScreen', |
113 | | - '#captureModeCanvasOnly', |
114 | | - '#sampleList', |
115 | | - '#repoSelectedValue', |
116 | | - '#writeFolderSampleValue', |
117 | | - '#writeFolderActualValue', |
118 | | - '#status', |
119 | | - '#log', |
120 | | - '#frame' |
121 | | -]; |
122 | | -for (const selector of requiredSelectors) { |
123 | | - const locator = page.locator(selector); |
124 | | - if (await locator.count() !== 1) { |
125 | | - throw new Error(`Expected one ${selector}`); |
126 | | - } |
127 | | -} |
128 | | -
|
129 | | -const menuButtons = await page.locator('.palette-manager-v2__menu-sample button').evaluateAll((buttons) => buttons.map((button) => button.textContent.trim())); |
130 | | -if (JSON.stringify(menuButtons) !== JSON.stringify(['Generate Preview', 'Stop'])) { |
131 | | - throw new Error(`Unexpected menu buttons: ${JSON.stringify(menuButtons)}`); |
132 | | -} |
133 | | -
|
134 | | -const hasCopiedPreviewCss = await page.locator('link[href*="shared/preview/preview-pages.css"]').count(); |
135 | | -if (hasCopiedPreviewCss !== 0) { |
136 | | - throw new Error('Copied preview stylesheet is still loaded.'); |
137 | | -} |
138 | | -const copiedClassCounts = await page.evaluate(() => ({ |
139 | | - row: document.querySelectorAll('.row').length, |
140 | | - inline: document.querySelectorAll('.inline').length, |
141 | | - inlineLabel: document.querySelectorAll('.inline-label').length, |
142 | | - valueBox: document.querySelectorAll('.value-box').length, |
143 | | - pathsField: document.querySelectorAll('.preview-generator-v2__paths-field').length |
144 | | -})); |
145 | | -for (const [name, count] of Object.entries(copiedClassCounts)) { |
146 | | - if (count !== 0) { |
147 | | - throw new Error(`Copied preview class still present: ${name}=${count}`); |
148 | | - } |
149 | | -} |
150 | | -
|
151 | | -const forbiddenSelectors = [ |
152 | | - '#applyToGameButton', |
153 | | - '#exportImageButton', |
154 | | - '#destinationDataInput', |
155 | | - 'textarea[aria-label*="JSON" i]', |
156 | | - 'textarea[id*="json" i]', |
157 | | - 'pre[id*="json" i]' |
158 | | -]; |
159 | | -for (const selector of forbiddenSelectors) { |
160 | | - if (await page.locator(selector).count() !== 0) { |
161 | | - throw new Error(`Forbidden JSON/action UI found: ${selector}`); |
162 | | - } |
163 | | -} |
164 | | -
|
165 | | -const sectionNames = await page.locator('.accordion-v2__header span:first-child').evaluateAll((items) => items.map((item) => item.textContent.trim())); |
166 | | -for (const expected of ['Repo Destination', 'Target Source', 'Render Controls', 'Output Summary', 'Status']) { |
167 | | - if (!sectionNames.includes(expected)) { |
168 | | - throw new Error(`Missing section: ${expected}`); |
169 | | - } |
170 | | -} |
171 | | -if (sectionNames.includes('Paths or IDs')) { |
172 | | - throw new Error('Paths or IDs accordion header should not exist.'); |
173 | | -} |
174 | | -
|
175 | | -if (await page.locator('label[for="sampleList"]').count() !== 0) { |
176 | | - throw new Error('Paths or IDs field label wrapper should not exist.'); |
177 | | -} |
| 56 | +const repoOrder = await page.locator('#repoDestinationAccordionContent > *').evaluateAll((nodes) => nodes.map((node) => node.textContent.trim().replace(/\s+/g, ' '))); |
| 57 | +if (!repoOrder[0].includes('Pick Repo Folder') || !repoOrder[1].includes('Repo selected')) throw new Error(`Repo destination order is wrong: ${JSON.stringify(repoOrder)}`); |
| 58 | +if (await page.locator('#statusAccordionContent').count() !== 0) throw new Error('Status accordion content still exists.'); |
| 59 | +if (await page.locator('.accordion-v2__header span:text-is("Status")').count() !== 0) throw new Error('Status is still rendered as an accordion header.'); |
| 60 | +if (await page.locator('.preview-generator-v2__status-block #status').count() !== 1) throw new Error('Status block does not contain status text.'); |
178 | 61 |
|
| 62 | +const outputFlex = await page.locator('.preview-generator-v2__output-summary').evaluate((node) => getComputedStyle(node).flexGrow); |
| 63 | +if (outputFlex !== '0') throw new Error(`Output Summary should not flex-grow; got ${outputFlex}`); |
179 | 64 | const initialSampleText = await page.locator('#writeFolderSampleValue').innerText(); |
180 | | -if (initialSampleText !== 'samples\\phaseXX\\XXXX\\assets\\images') { |
181 | | - throw new Error(`Unexpected write-folder sample text: ${initialSampleText}`); |
182 | | -} |
| 65 | +if (initialSampleText !== 'samples\\phaseXX\\XXXX\\assets\\images') throw new Error(`Unexpected write-folder sample text: ${initialSampleText}`); |
| 66 | +
|
| 67 | +const summary = page.locator('[data-preview-generator-v2-summary]'); |
| 68 | +await summary.click(); |
| 69 | +await page.waitForFunction(() => document.querySelector('.is-collapsible')?.open === false); |
| 70 | +await page.waitForFunction(() => document.querySelector('[data-preview-generator-v2-summary]')?.dataset.toolsPlatformSummaryState === 'collapsed'); |
| 71 | +await summary.click(); |
| 72 | +await page.waitForFunction(() => document.querySelector('.is-collapsible')?.open === true); |
183 | 73 |
|
184 | 74 | await page.fill('#baseUrl', server.baseUrl); |
185 | 75 | await page.fill('#waitMs', '3000'); |
186 | 76 | await page.fill('#sampleList', '0107'); |
187 | 77 | await page.check('#forceRewrite'); |
188 | 78 | await page.click('#pickRepoBtn'); |
189 | 79 | await page.waitForFunction(() => !document.getElementById('executeBtn').disabled); |
190 | | -const repoSelected = await page.locator('#repoSelectedValue').innerText(); |
191 | | -if (repoSelected !== 'SelectedRepoFolder') { |
192 | | - throw new Error(`Repo selected did not populate from folder handle: ${repoSelected}`); |
193 | | -} |
194 | 80 | await page.waitForFunction(() => document.getElementById('writeFolderActualValue').textContent === 'samples\\phase-01\\0107\\assets\\images'); |
195 | 81 | await page.click('#executeBtn'); |
196 | 82 | await page.waitForFunction(() => document.getElementById('log').textContent.includes('===== SUMMARY ====='), null, { timeout: 35000 }); |
197 | 83 | const writes = await page.evaluate(() => window.__previewGeneratorV2Writes || []); |
198 | | -if (writes.length !== 1) { |
199 | | - throw new Error(`Expected exactly one preview write, got ${writes.length}`); |
200 | | -} |
201 | | -if (!writes[0].path.endsWith('samples/phase-01/0107/assets/images/preview.svg')) { |
202 | | - throw new Error(`Unexpected write path: ${writes[0].path}`); |
203 | | -} |
204 | | -if (!writes[0].content.includes('<svg')) { |
205 | | - throw new Error('Generated content is not SVG-like.'); |
206 | | -} |
207 | | -if (errors.length || consoleErrors.length) { |
208 | | - throw new Error([...errors, ...consoleErrors].join(' | ')); |
209 | | -} |
| 84 | +if (writes.length !== 1) throw new Error(`Expected exactly one preview write, got ${writes.length}`); |
| 85 | +if (!writes[0].path.endsWith('samples/phase-01/0107/assets/images/preview.svg')) throw new Error(`Unexpected write path: ${writes[0].path}`); |
| 86 | +if (!writes[0].content.includes('<svg')) throw new Error('Generated content is not SVG-like.'); |
| 87 | +if (errors.length || consoleErrors.length) throw new Error([...errors, ...consoleErrors].join(' | ')); |
210 | 88 | await browser.close(); |
211 | 89 | await server.close(); |
212 | | -console.log('preview-generator-v2 reskin fixes browser smoke valid'); |
| 90 | +console.log('preview-generator-v2 layout polish browser smoke valid'); |
213 | 91 | '@ | node --input-type=module - |
214 | 92 | ``` |
215 | 93 |
|
216 | 94 | ## Notes |
217 | 95 |
|
218 | | -The targeted browser smoke validates the Palette Manager-style reskin fixes, confirms Stop is in the nav, verifies the old copied preview CSS/classes are gone, checks the write-folder text behavior, and exercises the existing preview generation write path. |
| 96 | +The targeted Playwright smoke validates the Preview Generator V2 layout polish, including Palette Manager-style collapsed header behavior, Pick Repo order, normal Status block, compact Output Summary sizing, and the existing preview generation write path. |
219 | 97 |
|
220 | 98 | `npm run test:workspace-v2` was attempted, but the script is not defined in this checkout. |
221 | 99 |
|
222 | | -Full samples smoke test was skipped because this PR is scoped to Preview Generator V2 only. |
| 100 | +Full samples smoke test was skipped because this PR is scoped to Preview Generator V2 UI polish only. |
0 commit comments