oxedyne/daimond/dev/shot_rail.mjs
7.8 KiB, 1 run
created by r2519314175:183, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // shot_rail.mjs — the two rail pictures the daimond.app landing page uses. |
| 2 | // |
| 3 | // It builds a workspace of six named and tagged Diamonds, shoots the rail, then |
| 4 | // narrows it to one tag and shoots it again. Four files come out, a dark and a |
| 5 | // light of each, because a raster cannot follow the reader's colour scheme the |
| 6 | // way the landing page's own CSS does. |
| 7 | // |
| 8 | // eval "$(bash dev/world.sh 5 --env)" |
| 9 | // node dev/shot_rail.mjs # writes into landing/assets/ |
| 10 | // |
| 11 | // This exists because the previous set was shot on 17 July and went stale in |
| 12 | // three ways at once, none of which any build would report: the panel was |
| 13 | // renamed Foci to Diamonds, the search box above the list was removed on |
| 14 | // purpose, and the rail gained the global "Everything" pause row above it. The |
| 15 | // pictures sat on a public page beside three paragraphs about Diamonds, saying |
| 16 | // FOCI. Re-run this whenever the rail changes shape. |
| 17 | |
| 18 | import path from 'node:path'; |
| 19 | import { fileURLToPath } from 'node:url'; |
| 20 | import { open } from './harness.mjs'; |
| 21 | |
| 22 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 23 | const OUT = path.join(HERE, '..', 'landing', 'assets'); |
| 24 | |
| 25 | /// The workspace the pictures show. The names are ordinary work, not a demo: |
| 26 | /// a client, a document, a piece of study, a person and two of the reader's |
| 27 | /// own. The `me` tag is the one the filter shot narrows to, so it has to fall |
| 28 | /// on more than one Diamond and fewer than all of them. |
| 29 | const DIAMONDS = [ |
| 30 | { name: 'My Daimond prefs', tags: ['daimond', 'me'] }, |
| 31 | { name: 'Webster & Fernhill', tags: ['client', 'business'] }, |
| 32 | { name: 'Approval Application', tags: ['document', 'project', 'government'] }, |
| 33 | { name: 'Charles Houston essay', tags: ['university', 'document'] }, |
| 34 | { name: 'Mum', tags: ['person', 'family'] }, |
| 35 | { name: 'My health', tags: ['health', 'me'] }, |
| 36 | ]; |
| 37 | |
| 38 | const s = await open({ name: 'rail-shot', signIn: true }); |
| 39 | const p = s.page; |
| 40 | const pause = (ms) => p.waitForTimeout(ms); |
| 41 | |
| 42 | await p.evaluate(() => DaimondPanels.show('rail')); |
| 43 | await pause(600); |
| 44 | |
| 45 | /// Create one Diamond through the real New Diamond button and its dialog. The |
| 46 | /// dialog is in-DOM rather than native, so there is no `page.on('dialog')` to |
| 47 | /// wire up. |
| 48 | async function makeDiamond(name) { |
| 49 | await p.click('#new-diamond-btn'); |
| 50 | await p.waitForSelector('.dlg-card .dlg-input', { state: 'visible', timeout: 8000 }); |
| 51 | await p.fill('.dlg-card .dlg-input', name); |
| 52 | await p.click('.dlg-card .dlg-ok'); |
| 53 | await pause(1100); |
| 54 | } |
| 55 | |
| 56 | /// Select a Diamond by its visible name and put the given tags on it, through |
| 57 | /// the Tags action and its editor, which is how a user does it. |
| 58 | async function tagDiamond(name, tags) { |
| 59 | await p.evaluate((n) => { |
| 60 | const box = [...document.querySelectorAll('.diamond-box')] |
| 61 | .find((b) => (b.textContent || '').includes(n)); |
| 62 | if (box) box.click(); |
| 63 | }, name); |
| 64 | await pause(700); |
| 65 | await p.evaluate(() => { |
| 66 | const b = [...document.querySelectorAll('.crystal-act')] |
| 67 | .find((x) => /tags/i.test(x.textContent || '')); |
| 68 | if (b) b.click(); |
| 69 | }); |
| 70 | await p.waitForSelector('.tag-editor .tag-input', { state: 'visible', timeout: 8000 }); |
| 71 | for (const t of tags) { |
| 72 | await p.fill('.tag-editor .tag-input', t); |
| 73 | await p.evaluate(() => { |
| 74 | const add = document.querySelector('.tag-add .crystal-act'); |
| 75 | if (add) add.click(); |
| 76 | }); |
| 77 | await pause(400); |
| 78 | } |
| 79 | await pause(300); |
| 80 | } |
| 81 | |
| 82 | // The rail lists newest first, so they are created back to front to come out |
| 83 | // in the order DIAMONDS declares. |
| 84 | for (const d of [...DIAMONDS].reverse()) await makeDiamond(d.name); |
| 85 | for (const d of DIAMONDS) await tagDiamond(d.name, d.tags); |
| 86 | |
| 87 | // One Diamond always carries `.active`, so there is no such thing as a rail |
| 88 | // with nothing current. Tagging walks the list and leaves the last one it |
| 89 | // touched active at the FOOT, half out of frame; put the current one back at |
| 90 | // the top, where a highlight reads as "the one you are in" rather than as a |
| 91 | // crop. |
| 92 | await p.evaluate(() => DaimondPanels.show('rail')); |
| 93 | await p.$$eval('.diamond-box', (els) => els[0] && els[0].click()); |
| 94 | await pause(700); |
| 95 | |
| 96 | // Six Diamonds do not fit the list at the harness's default 950px window, and |
| 97 | // dragging the split alone cannot fix that: the rail is as tall as the window, |
| 98 | // so the space has to come from the window first. Taller window, then the drag. |
| 99 | await p.setViewportSize({ width: 1500, height: 1200 }); |
| 100 | await pause(500); |
| 101 | |
| 102 | // Give the Diamonds list the room for all six by dragging the rail's own split |
| 103 | // handle, which is what a user with six of them would do. |
| 104 | const split = await p.$('#handle-rail-split'); |
| 105 | if (split) { |
| 106 | const b = await split.boundingBox(); |
| 107 | await p.mouse.move(b.x + b.width / 2, b.y + b.height / 2); |
| 108 | await p.mouse.down(); |
| 109 | await p.mouse.move(b.x + b.width / 2, b.y + 430, { steps: 14 }); |
| 110 | await p.mouse.up(); |
| 111 | await pause(600); |
| 112 | } |
| 113 | await p.evaluate(() => { document.getElementById('diamond-list').scrollTop = 0; }); |
| 114 | await pause(300); |
| 115 | |
| 116 | /// Capture the rail at both themes. The app stores the choice in localStorage |
| 117 | /// and stamps `data-theme` on the root, so both are set: the attribute for what |
| 118 | /// is drawn now, the key so nothing on the page rereads the old value. |
| 119 | async function shotRail(file, count) { |
| 120 | for (const theme of ['dark', 'light']) { |
| 121 | await p.evaluate((t) => { |
| 122 | localStorage.setItem('daimond-theme', t); |
| 123 | document.documentElement.setAttribute('data-theme', t); |
| 124 | }, theme); |
| 125 | await pause(600); |
| 126 | // The panel runs from the pause row at the top to the account Status |
| 127 | // strip at the foot, and the strip is developer furniture: a build hash, |
| 128 | // a tools count, whether the account service answered. None of that |
| 129 | // belongs on a public page, so the clip stops at the end of the Diamonds |
| 130 | // list. Measured per shot, because the filtered list is shorter. |
| 131 | const clip = await p.evaluate((n) => { |
| 132 | document.getElementById('diamond-list').scrollTop = 0; |
| 133 | const r = document.getElementById('panel-rail').getBoundingClientRect(); |
| 134 | const l = document.getElementById('diamond-list').getBoundingClientRect(); |
| 135 | const boxes = [...document.querySelectorAll('.diamond-box')] |
| 136 | .filter((b) => b.getClientRects().length); |
| 137 | // Stop after the n-th Diamond. Below them sit the two the app ships |
| 138 | // with, Daimond Optimiser and Daimond Help, which are real and stay in |
| 139 | // the product; they are simply not what these pictures are of, and a |
| 140 | // frame ending part-way through one reads as a broken crop. |
| 141 | const cut = boxes[Math.min(n, boxes.length) - 1]; |
| 142 | const last = cut ? cut.getBoundingClientRect().bottom : l.bottom; |
| 143 | return { |
| 144 | x: Math.round(r.x), |
| 145 | y: Math.round(r.y), |
| 146 | width: Math.round(r.width), |
| 147 | height: Math.round(Math.min(last + 10, l.bottom) - r.y), |
| 148 | }; |
| 149 | }, count); |
| 150 | await p.screenshot({ path: path.join(OUT, `${file}-${theme}.png`), clip }); |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | await shotRail('rail', DIAMONDS.length); |
| 155 | |
| 156 | // Narrow to `me`, which leaves the reader's own two. The chip strip above the |
| 157 | // list is hidden until a tag is active, so the filtered shot is the only one |
| 158 | // that shows it. |
| 159 | await p.evaluate(() => { |
| 160 | const chip = [...document.querySelectorAll('.diamond-box .tag-chip')] |
| 161 | .find((c) => (c.textContent || '').trim() === 'me'); |
| 162 | if (chip) chip.click(); |
| 163 | }); |
| 164 | await pause(900); |
| 165 | |
| 166 | const shown = await p.evaluate(() => ({ |
| 167 | filter: (document.getElementById('diamond-filter') || {}).innerText || '', |
| 168 | names: [...document.querySelectorAll('.diamond-box')] |
| 169 | .filter((b) => b.getClientRects().length) |
| 170 | .map((b) => (b.querySelector('.diamond-name') || b).innerText.split('\n')[0]), |
| 171 | })); |
| 172 | console.log('FILTERED:', JSON.stringify(shown)); |
| 173 | |
| 174 | await shotRail('filter', shown.names.length); |
| 175 | |
| 176 | const sizes = await p.evaluate(() => { |
| 177 | const b = document.getElementById('panel-rail').getBoundingClientRect(); |
| 178 | return { w: Math.round(b.width), h: Math.round(b.height) }; |
| 179 | }); |
| 180 | console.log('RAIL BOX:', JSON.stringify(sizes)); |
| 181 | console.log('WROTE:', ['rail-dark', 'rail-light', 'filter-dark', 'filter-light'] |
| 182 | .map((n) => `${n}.png`).join(' ')); |
| 183 | |
| 184 | await s.close(); |