Flipping to comp demotes the inspiration instead of stacking a second slot

Two defects in the same few lines, both from `enterComp` hand-building a media
slot after the deal instead of reaching the shape a comp-first render serves.

A code-led card carrying catalog art shows that art as its face. Flipping to
comp inserted a fresh shimmer slot above the body and left the face alone, so
the card rendered the inspiration full-bleed with the rendering comp stacked
under it: two images of equal weight, which is the one thing the corner
treatment exists to prevent. The flip now converts that slot in place, moving
the art into the `figure.pip` and dropping the face label, and flipping back
restores it, so a round-trip leaves the card as it was dealt.

The slot it built also carried no chips, and the zoom handlers were bound per
element at load, so a comp that streamed in after a flip could not be opened at
all: no expand affordance, and no click handler on the art. The three lightbox
handlers are now delegated, which is what makes any later-built slot work, and
a converted slot keeps the chips it already had. Polling learned to stop on a
slot that stays in the DOM but loses its pending state, which only happens now
that a flip back can restore rather than remove.

The existing toggle test covered a wireframe card, where the schematic is
hidden and a fresh slot inserted; that branch was fine, which is why this went
unseen. The new test drives the art-carrying card and fails on the stacking
assertion without this change.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-08-13 23:32:02 -04:00
co-authored by Claude Opus 5
parent 4f08ff3bfc
commit ec189f4536
2 changed files with 169 additions and 24 deletions
+72
View File
@@ -609,6 +609,78 @@ describe('new-work-e2e: serve-question decision page', () => {
}
});
// The flip used to be tested only on a wireframe card, where the schematic
// is hidden and a fresh slot inserted. A card carrying catalog art instead
// took the other branch: the inspiration stayed the face and the comp slot
// was inserted below it, so the card showed two stacked images. Comp-first
// demotes the inspiration to the corner, and a flip has to reach the same
// shape a comp-first render would have served.
it('(f2) flipping to comp demotes an inspiration face to the corner instead of stacking a second slot', async () => {
const cwd = makeWorkspace();
const key = 'flipinspo';
const hero = makeFakeImage(cwd, 'catalog inspiration', 'inspo.png');
const payload = {
title: 'Choose the visual world',
buildPath: { value: 'code', toggle: true },
options: [
{
id: 'assigned', label: 'The Ledger Spine', kicker: 'THE ROLL', hero,
comp: '.impeccable/mocks/decision/assigned.webp',
viewport: 'A ruled masthead over a dense grid.', risk: 'Reads editorial.',
},
],
};
const { url } = await startDaemon(cwd, payload, key);
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(url, { waitUntil: 'load' });
const front = '.card[data-id="assigned"] .face.front';
// Code-led: the catalog art is the face, labeled so it never reads as a promise.
assert.equal(await page.$$eval(`${front} .media`, (els) => els.length), 1, 'one media slot before the flip');
assert.ok(await page.$(`${front} .media .media-label`), 'the inspiration face is labeled');
assert.equal(await page.$(`${front} .pip`), null, 'no corner inspiration while code-led');
await page.click('.bp-opt[data-bp="comp"]');
await page.waitForSelector('#bp-confirm:not([hidden])');
await page.click('#bp-confirm [data-confirm]');
await page.waitForSelector(`${front} .media.comp-pending`);
// The whole point: one slot, not two.
assert.equal(await page.$$eval(`${front} .media`, (els) => els.length), 1,
'the flip converts the inspiration slot rather than stacking a second one');
assert.ok(await page.$(`${front} .media.comp-pending .pip img`), 'the inspiration moved into the corner');
assert.equal(await page.$(`${front} .media > .media-label`), null, 'it is no longer presented as the face');
assert.ok(await page.$(`${front} .media .chip.expand`), 'the converted slot keeps its expand affordance');
const flipped = await waitLoop(cwd, key, { poll: 10 });
assert.match(flipped.out, /BUILD PATH FLIPPED: comp/);
// A comp that streams in must be openable. The zoom handlers used to be
// bound once at deal time, so anything built by the flip was inert.
mkdirSync(path.join(cwd, '.impeccable', 'mocks', 'decision'), { recursive: true });
makeFakeImage(path.join(cwd, '.impeccable', 'mocks', 'decision'), 'ledger spine comp', 'assigned.webp');
await page.waitForSelector(`${front} .media img.comp:not([hidden])`, { timeout: 15000 });
await page.click(`${front} .media .chip.expand`);
await page.waitForSelector('#lightbox:not([hidden])');
assert.ok(
await page.$eval('#lightbox img', (img) => Boolean(img.getAttribute('src'))),
'the streamed-in comp opens full screen',
);
await page.click('#lightbox');
// Flipping back restores the card as it was dealt.
await page.click('.bp-opt[data-bp="code"]');
await page.waitForTimeout(200);
assert.equal(await page.$$eval(`${front} .media`, (els) => els.length), 1, 'still one slot after flipping back');
await context.close();
} finally {
await stopDaemon(cwd, key);
rmSync(cwd, { recursive: true, force: true });
}
});
it('(g) a wireframe card draws its schematic in the media slot and keeps its full read on the front', async () => {
const cwd = makeWorkspace();
const key = 'wire';