/** * Menus that can grow with the project. Runs last: it fills the scene with throwaway objects * to make the parent picker overflow, which the earlier steps' object counts would not survive. */ export async function menus(t) { const { check, sleep, evaluate, json, shot, click, locate, mouse, mutate } = t; /** Like locate(), but a row scrolled out of view answers with null instead of throwing. */ const rowAt = async (text) => (await evaluate(`__gm.locate(${JSON.stringify(text)}, true)`)) ?? { x: -1, y: -1 }; await mutate(` const scene = __gm.state.activeScene; // Any plain stored object will do – not a placed container, whose content the picker // would list forty times over as well. const source = scene.objects.find(o => o.id === 'otext'); for (let i = 1; i <= 40; i++) { const copy = structuredClone(source); copy.id = 'filler' + i; copy.name = 'Filler ' + i; copy.parentId = ''; copy.overrides = {}; scene.objects.push(copy); } __gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`); await sleep(150); // 11a. A picker with 40+ entries is capped in height and stays fully on screen instead of // running off the bottom edge. const parentRow = await json("__gm.inspectorField('Parent')"); await click(parentRow.x, parentRow.y); const rect = await json('__gm.overlayRect()'); const screenHeight = await json('window.innerHeight'); check( 'a long parent picker is capped instead of growing with the object count', rect && rect.h < 420, JSON.stringify(rect), ); check( 'the capped picker stays fully on screen', rect && rect.y >= 0 && rect.y + rect.h <= screenHeight, JSON.stringify({ rect, screenHeight }), ); await shot('31-long-parent-menu'); // 11b. The entries scroll under the wheel; the search box above them does not move. // Anchored on a filler row read off the open menu: "None" could match an inspector field behind it, // and which fillers are drawn depends on what earlier steps left in the scene. const visibleFillers = (await json('__gm.overlayTexts()')).filter((e) => e.startsWith('Filler ')); const anchor = visibleFillers[visibleFillers.length - 1]; check('the picker lists the filler objects', Boolean(anchor), JSON.stringify(visibleFillers)); const firstBefore = await rowAt(anchor); const searchBefore = await locate('Search…', true); // Move onto the list first: a wheel event goes to whatever is hovered, and the pointer last // touched the field above the menu. await mouse('mouseMoved', rect.x + rect.w / 2, rect.y + rect.h / 2); await sleep(50); await mouse('mouseWheel', rect.x + rect.w / 2, rect.y + rect.h / 2, 'none', { deltaX: 0, deltaY: 240 }); // Measured here: the list had not moved 40 ms after the wheel but had at 340 ms. Both // readings are reported so an early one cannot look like a mystery failure again. await sleep(40); const rightAfter = await rowAt(anchor); await sleep(300); const firstAfter = await rowAt(anchor); const searchAfter = await locate('Search…', true); check( 'the wheel scrolls the entries', firstAfter.y >= 0 && firstAfter.y < firstBefore.y - 100, JSON.stringify({ firstBefore, rightAfter, firstAfter }), ); check('the search box stays put while the entries scroll', searchAfter.y === searchBefore.y); }