diff --git a/README.md b/README.md index 8d157dee..54659dd4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1179 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1184 checks (GM_STEPS=20 for one step) # every tool signs in first – see the Accounts page node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs diff --git a/tools/e2e/47-vectorart.mjs b/tools/e2e/47-vectorart.mjs index b29f038d..23892ce1 100644 --- a/tools/e2e/47-vectorart.mjs +++ b/tools/e2e/47-vectorart.mjs @@ -11,7 +11,9 @@ import { LANES, MAKE_OBJECT } from './lib/scaffold.mjs'; * The shapes of the first blocks are put into the model rather than drawn with the mouse: those * checks are about the rasteriser, not about the tool. From 47c on it is the tool that is measured – * the pen puts points down, the arrow drags one and takes one away, and what comes out is read from - * the model, which is the only place a point's coordinates exist exactly. + * the model, which is the only place a point's coordinates exist exactly. The last block, 47f, + * walks the way a user walks instead: the asset comes out of the + menu, its row opens the tab, + * and a sprite is given the drawing through its Texture row. */ const CANVAS = 256; @@ -341,4 +343,110 @@ export async function vectorArt(t) { added.length === before47e + 1 && (await json('__gm.vectorArt.selected()')) === added.at(-1), JSON.stringify({ added: added.length, selected: await json('__gm.vectorArt.selected()') }), ); + + // 47f (stage 5). The way a user takes, which the blocks before took by hand: the asset is made + // from the + menu, its row opens the tab, and a sprite wears the drawing through its + // Texture row – the picture picker of #185, which until here only unit tests had seen. + // "Vector art" is the menu entry *and* the label of the open tab, so the entry is picked + // inside the menu's own box rather than by text alone. + const inOverlay = async (text) => { + const boxes = await json('__gm.overlayRect()'); + const hits = (await json(`__gm.locateAll(${JSON.stringify(text)})`)) ?? []; + return ( + hits.find( + (hit) => + hit.x >= boxes.x && hit.x <= boxes.x + boxes.w && hit.y >= boxes.y && hit.y <= boxes.y + boxes.h, + ) ?? null + ); + }; + + await clickText('+'); + const plusRows = (await json('__gm.menuRows()')) ?? []; + const entry = await inOverlay('Vector art'); + check( + 'the + menu offers Vector art among the types that only live in the library', + plusRows.includes('Vector art') && Boolean(entry), + JSON.stringify({ offered: plusRows.includes('Vector art'), entry }), + ); + await click(entry.x, entry.y); + await settle(); + const madeId = ( + await json("__gm.state.project.assets.filter((a) => a.type === 'vectorart').map((a) => a.id)") + ).at(-1); + const madeRow = await json(`__gm.assetRow(${JSON.stringify(madeId)})`); + check( + 'the entry makes a vector art asset, selected and standing in the panel', + madeId !== 'va-art' && (await json('__gm.state.selection?.assetId')) === madeId && Boolean(madeRow), + JSON.stringify({ madeId, row: madeRow }), + ); + + // Its row opens the tab on that asset – until here the tab was opened by `openDocument`. + await showTab('scene'); + await doubleClick(madeRow.x, madeRow.y); + await settle(); + check( + 'a double-click on the row opens the Vector art tab on that asset', + (await json('__gm.vectorArt.current()')) === madeId, + JSON.stringify({ current: await json('__gm.vectorArt.current()'), wanted: madeId }), + ); + + // A sprite that wears nothing yet: the drawing has to be reachable from its Texture row. + const Y2 = Y - 400; + await mutate(` + const project = __gm.state.project; + const scene = __gm.state.activeScene; + ${MAKE_OBJECT} + const bare = structuredClone(project.assets.find((a) => a.id === 'va-sprite')); + bare.id = 'va-bare'; + bare.name = 'Bare sprite'; + bare.props = { ...bare.props, texture: '' }; + project.assets.push(bare); + scene.objects.push(make('va-bare-object', 'va-bare', '', ${Y2}, 'Bare object')); + __gm.state.select({ kind: 'asset', assetId: 'va-bare' })`); + await showTab('scene'); + await evaluate(`__gm.lookAt(0, ${Y2})`); + await settle(); + + const textureRow = await json("__gm.inspectorField('Texture', true)"); + await click(textureRow.x, textureRow.y); + await settle(); + const offers = (await json('__gm.menuRows()')) ?? []; + // Measured against the model rather than against a fixed list: run alone the fixture holds no + // uploaded picture at all, while after module 12 it holds several. What has to hold either way + // is that every picture asset is offered by name, the drawings among them and sorted with them. + const pictures = await json( + "__gm.state.project.assets.filter((a) => a.type === 'texture' || a.type === 'vectorart')" + + '.map((a) => a.name).sort((a, b) => a.localeCompare(b))', + ); + const offeredAssets = offers.filter((row) => pictures.includes(row)); + check( + 'the Texture row offers the drawing by name, sorted among the other pictures', + offers.includes('Emblem') && + offeredAssets.join() === pictures.join() && + // The row it wears right now is marked, so "None" arrives with a bullet in front of it. + offers.some((row) => typeof row === 'string' && row.endsWith('None')), + JSON.stringify({ offers, pictures }), + ); + + const drawingEntry = await inOverlay('Emblem'); + await click(drawingEntry.x, drawingEntry.y); + await settle(); + await sleep(300); + const bareBox = await json("__gm.objectRect('va-bare-object')"); + const wornAt = (cx, cy) => + json( + `__gm.pixelAt(${Math.round(bareBox.x + (cx / CANVAS - 0.5) * bareBox.w)}, ${Math.round(bareBox.y + (cy / CANVAS - 0.5) * bareBox.h)})`, + ); + // The field was recoloured in 47e and sent to the back, so this is the colour of that row. + const wornPixel = await wornAt(48, 200); + check( + 'picking it there makes the sprite wear the drawing, as the scene shows', + (await json("__gm.state.project.assets.find((a) => a.id === 'va-bare').props.texture")) === 'va-art' && + colourDistance(wornPixel, '#20c0e8') < 14, + JSON.stringify({ + worn: await json("__gm.state.project.assets.find((a) => a.id === 'va-bare').props.texture"), + wornPixel, + }), + ); + await shot('47-picture-picker'); }