Oregami
Repositories/oxedyne/daimond

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
18import path from 'node:path';
19import { fileURLToPath } from 'node:url';
20import { open } from './harness.mjs';
21
22const HERE = path.dirname(fileURLToPath(import.meta.url));
23const 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.
29const 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
38const s = await open({ name: 'rail-shot', signIn: true });
39const p = s.page;
40const pause = (ms) => p.waitForTimeout(ms);
41
42await p.evaluate(() => DaimondPanels.show('rail'));
43await 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.
48async 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.
58async 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.
84for (const d of [...DIAMONDS].reverse()) await makeDiamond(d.name);
85for (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.
92await p.evaluate(() => DaimondPanels.show('rail'));
93await p.$$eval('.diamond-box', (els) => els[0] && els[0].click());
94await 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.
99await p.setViewportSize({ width: 1500, height: 1200 });
100await 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.
104const split = await p.$('#handle-rail-split');
105if (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}
113await p.evaluate(() => { document.getElementById('diamond-list').scrollTop = 0; });
114await 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.
119async 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
154await 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.
159await p.evaluate(() => {
160 const chip = [...document.querySelectorAll('.diamond-box .tag-chip')]
161 .find((c) => (c.textContent || '').trim() === 'me');
162 if (chip) chip.click();
163});
164await pause(900);
165
166const 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}));
172console.log('FILTERED:', JSON.stringify(shown));
173
174await shotRail('filter', shown.names.length);
175
176const 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});
180console.log('RAIL BOX:', JSON.stringify(sizes));
181console.log('WROTE:', ['rail-dark', 'rail-light', 'filter-dark', 'filter-light']
182 .map((n) => `${n}.png`).join(' '));
183
184await s.close();