pandorga 1.6.7 → 1.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +3 -2
- data/_includes/page/content-runtime.html +3 -0
- data/_includes/page/network-graph.html +5 -2
- data/_includes/page/timeline-section.html +13 -1
- data/_includes/theme/favicons.html +3 -1
- data/_layouts/articles.html +5 -2
- data/_layouts/bibliography.html +5 -2
- data/_layouts/blog.html +5 -2
- data/_layouts/cv.html +5 -2
- data/_layouts/home.html +10 -3
- data/_layouts/projects.html +5 -2
- data/_layouts/resources.html +5 -2
- data/_layouts/shell.html +6 -1
- data/assets/css/ledger/home.css +20 -0
- data/assets/css/ledger/rascunhos.css +40 -2
- data/assets/icons/apple-touch-icon.png +0 -0
- data/assets/icons/favicon-32.png +0 -0
- data/assets/icons/favicon.ico +0 -0
- data/assets/icons/favicon.svg +23 -0
- data/assets/icons/icon-192.png +0 -0
- data/assets/icons/icon-32.png +0 -0
- data/assets/icons/icon-512.png +0 -0
- data/assets/icons/icon-maskable-192.png +0 -0
- data/assets/icons/icon-maskable-512.png +0 -0
- data/assets/icons/studio-apple-touch-icon.png +0 -0
- data/assets/icons/studio-icon-192.png +0 -0
- data/assets/icons/studio-icon-512.png +0 -0
- data/assets/icons/studio-logo-32.png +0 -0
- data/assets/icons/studio-logo.ico +0 -0
- data/assets/icons/studio-logo.svg +25 -5
- data/assets/icons/studio-maskable-192.png +0 -0
- data/assets/icons/studio-maskable-512.png +0 -0
- data/lib/pandorga/jekyll/plugins/web_manifest.rb +81 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/check_home_lists.mjs +209 -0
- data/scripts/lighthouse-home.sh +6 -0
- data/scripts/render-pwa-icons.py +141 -0
- data/scripts/test/test-home-articles.rb +30 -0
- data/scripts/test/test-home-lists.rb +74 -0
- data/scripts/test/test-pwa-icons.rb +280 -0
- data/scripts/test/test-rascunhos-licence.rb +43 -0
- data/scripts/validate.sh +4 -0
- data/studio/index.html +4 -1
- data/studio-app/index.html +4 -1
- metadata +24 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Load the built example home and fail when a listing band is still empty.
|
|
3
|
+
// The runtime fills those bands after the document loads. A score-only
|
|
4
|
+
// Lighthouse run treats an empty home as a fast page.
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
8
|
+
import { createServer } from 'node:net';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
function flag(name) {
|
|
16
|
+
const index = args.indexOf(name);
|
|
17
|
+
if (index === -1 || !args[index + 1]) {
|
|
18
|
+
console.error(`ERROR: missing ${name}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
return args[index + 1];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const chromePath = flag('--chrome');
|
|
25
|
+
const pageUrl = flag('--url');
|
|
26
|
+
|
|
27
|
+
const COUNTS = `({
|
|
28
|
+
projects: document.querySelectorAll('#home-projects .home-project-inventory-row').length,
|
|
29
|
+
articles: document.querySelectorAll('#home-articles .home-featured, #home-articles .ledger-card').length,
|
|
30
|
+
posts: document.querySelectorAll('#home-blog .home-draft-card').length,
|
|
31
|
+
projectsText: (document.querySelector('#home-projects') || {}).innerText || '',
|
|
32
|
+
articlesText: (document.querySelector('#home-articles') || {}).innerText || ''
|
|
33
|
+
})`;
|
|
34
|
+
|
|
35
|
+
const VIEWPORTS = [
|
|
36
|
+
{ name: 'mobile', width: 390, height: 844, mobile: true, deviceScaleFactor: 2 },
|
|
37
|
+
{ name: 'desktop', width: 1280, height: 800, mobile: false, deviceScaleFactor: 1 }
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function freePort() {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const server = createServer();
|
|
43
|
+
server.once('error', reject);
|
|
44
|
+
server.listen(0, '127.0.0.1', () => {
|
|
45
|
+
const { port } = server.address();
|
|
46
|
+
server.close(() => resolve(port));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class Cdp {
|
|
52
|
+
constructor(ws) {
|
|
53
|
+
this.ws = ws;
|
|
54
|
+
this.next = 1;
|
|
55
|
+
this.pending = new Map();
|
|
56
|
+
this.listeners = [];
|
|
57
|
+
ws.addEventListener('message', (event) => {
|
|
58
|
+
const message = JSON.parse(event.data);
|
|
59
|
+
if (message.id && this.pending.has(message.id)) {
|
|
60
|
+
const { resolve, reject } = this.pending.get(message.id);
|
|
61
|
+
this.pending.delete(message.id);
|
|
62
|
+
if (message.error) reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
63
|
+
else resolve(message.result || {});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (message.method) this.listeners.forEach((listen) => listen(message));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
send(method, params = {}, sessionId) {
|
|
71
|
+
const id = this.next++;
|
|
72
|
+
const payload = { id, method, params };
|
|
73
|
+
if (sessionId) payload.sessionId = sessionId;
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
this.pending.set(id, { resolve, reject });
|
|
76
|
+
this.ws.send(JSON.stringify(payload));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
once(method, sessionId) {
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
const listen = (message) => {
|
|
83
|
+
if (message.method !== method) return;
|
|
84
|
+
if (sessionId && message.sessionId !== sessionId) return;
|
|
85
|
+
this.listeners = this.listeners.filter((entry) => entry !== listen);
|
|
86
|
+
resolve(message.params || {});
|
|
87
|
+
};
|
|
88
|
+
this.listeners.push(listen);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function waitForDevtools(port) {
|
|
94
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
97
|
+
if (response.ok) return response.json();
|
|
98
|
+
} catch (_error) {
|
|
99
|
+
// Chrome is still starting.
|
|
100
|
+
}
|
|
101
|
+
await delay(100);
|
|
102
|
+
}
|
|
103
|
+
throw new Error('Chrome DevTools did not answer');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function openSocket(url) {
|
|
107
|
+
const ws = new WebSocket(url);
|
|
108
|
+
await new Promise((resolve, reject) => {
|
|
109
|
+
ws.addEventListener('open', resolve, { once: true });
|
|
110
|
+
ws.addEventListener('error', () => reject(new Error('DevTools socket failed')), { once: true });
|
|
111
|
+
});
|
|
112
|
+
return ws;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function navigate(browser, sessionId, url) {
|
|
116
|
+
const loaded = browser.once('Page.loadEventFired', sessionId);
|
|
117
|
+
await browser.send('Page.navigate', { url }, sessionId);
|
|
118
|
+
await Promise.race([
|
|
119
|
+
loaded,
|
|
120
|
+
delay(20000).then(() => {
|
|
121
|
+
throw new Error('home did not finish loading');
|
|
122
|
+
})
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function counts(browser, sessionId) {
|
|
127
|
+
const deadline = Date.now() + 15000;
|
|
128
|
+
let last = { projects: 0, articles: 0, posts: 0, projectsText: '', articlesText: '' };
|
|
129
|
+
while (Date.now() < deadline) {
|
|
130
|
+
const evaluated = await browser.send('Runtime.evaluate', {
|
|
131
|
+
expression: COUNTS,
|
|
132
|
+
returnByValue: true
|
|
133
|
+
}, sessionId);
|
|
134
|
+
if (evaluated.exceptionDetails) {
|
|
135
|
+
throw new Error('home list probe failed');
|
|
136
|
+
}
|
|
137
|
+
last = (evaluated.result && evaluated.result.value) || last;
|
|
138
|
+
if (last.projects > 0 && last.articles > 0 && last.posts > 0) return last;
|
|
139
|
+
await delay(200);
|
|
140
|
+
}
|
|
141
|
+
return last;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function filled(result) {
|
|
145
|
+
return result.projects > 0 && result.articles > 0 && result.posts > 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function main() {
|
|
149
|
+
const profile = await mkdtemp(join(tmpdir(), 'pandorga-home-'));
|
|
150
|
+
const port = await freePort();
|
|
151
|
+
const chrome = spawn(chromePath, [
|
|
152
|
+
'--headless=new',
|
|
153
|
+
'--no-sandbox',
|
|
154
|
+
'--disable-gpu',
|
|
155
|
+
'--disable-dev-shm-usage',
|
|
156
|
+
'--no-first-run',
|
|
157
|
+
'--remote-allow-origins=*',
|
|
158
|
+
`--remote-debugging-port=${port}`,
|
|
159
|
+
`--user-data-dir=${profile}`,
|
|
160
|
+
'about:blank'
|
|
161
|
+
], { stdio: 'ignore' });
|
|
162
|
+
|
|
163
|
+
let failed = false;
|
|
164
|
+
try {
|
|
165
|
+
const version = await waitForDevtools(port);
|
|
166
|
+
const ws = await openSocket(version.webSocketDebuggerUrl);
|
|
167
|
+
const browser = new Cdp(ws);
|
|
168
|
+
const created = await browser.send('Target.createTarget', { url: 'about:blank' });
|
|
169
|
+
const attached = await browser.send('Target.attachToTarget', {
|
|
170
|
+
targetId: created.targetId,
|
|
171
|
+
flatten: true
|
|
172
|
+
});
|
|
173
|
+
const sessionId = attached.sessionId;
|
|
174
|
+
await browser.send('Page.enable', {}, sessionId);
|
|
175
|
+
await browser.send('Runtime.enable', {}, sessionId);
|
|
176
|
+
|
|
177
|
+
for (const viewport of VIEWPORTS) {
|
|
178
|
+
await browser.send('Emulation.setDeviceMetricsOverride', {
|
|
179
|
+
width: viewport.width,
|
|
180
|
+
height: viewport.height,
|
|
181
|
+
deviceScaleFactor: viewport.deviceScaleFactor,
|
|
182
|
+
mobile: viewport.mobile
|
|
183
|
+
}, sessionId);
|
|
184
|
+
await navigate(browser, sessionId, pageUrl);
|
|
185
|
+
const result = await counts(browser, sessionId);
|
|
186
|
+
console.log(
|
|
187
|
+
`home lists (${viewport.name}): projects=${result.projects} articles=${result.articles} posts=${result.posts}`
|
|
188
|
+
);
|
|
189
|
+
if (!filled(result)) {
|
|
190
|
+
console.error(`ERROR: ${viewport.name} home is missing project, article, or post items`);
|
|
191
|
+
console.error(`projects band: ${JSON.stringify(String(result.projectsText).slice(0, 180))}`);
|
|
192
|
+
console.error(`articles band: ${JSON.stringify(String(result.articlesText).slice(0, 180))}`);
|
|
193
|
+
failed = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
ws.close();
|
|
197
|
+
} finally {
|
|
198
|
+
chrome.kill('SIGKILL');
|
|
199
|
+
await delay(100);
|
|
200
|
+
await rm(profile, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (failed) process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
main().catch((error) => {
|
|
207
|
+
console.error(`ERROR: ${error.message}`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
});
|
data/scripts/lighthouse-home.sh
CHANGED
|
@@ -57,6 +57,12 @@ for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
|
|
57
57
|
done
|
|
58
58
|
[[ "${ready}" == "1" ]] || fail "example server did not answer on port ${PORT}"
|
|
59
59
|
|
|
60
|
+
# Empty bands still score well. This fails in warn mode and in enforce mode.
|
|
61
|
+
step "Home lists after load (mobile and desktop)"
|
|
62
|
+
node "${ROOT}/scripts/check_home_lists.mjs" \
|
|
63
|
+
--chrome "${CHROME}" \
|
|
64
|
+
--url "http://127.0.0.1:${PORT}/"
|
|
65
|
+
|
|
60
66
|
step "Lighthouse mobile (one run)"
|
|
61
67
|
rm -f "${OUT}"
|
|
62
68
|
npx --yes lighthouse@13.5.0 "http://127.0.0.1:${PORT}/" \
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rasterize the site and Studio launcher icons from the site stamp.
|
|
3
|
+
|
|
4
|
+
Both icons use the square mark in `_includes/theme/site-logo.html` and the
|
|
5
|
+
light-on-dark plate of `assets/icons/favicon.svg`: dark frame, pastel triad.
|
|
6
|
+
Studio adds a small plate in the lower right. Nothing important sits outside
|
|
7
|
+
the maskable safe zone (the center 80% circle). File names match
|
|
8
|
+
`lib/pandorga/jekyll/plugins/web_manifest.rb`. Requires Pillow.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from PIL import Image, ImageDraw
|
|
17
|
+
|
|
18
|
+
# 32×32 square site stamp.
|
|
19
|
+
PRIMARY = [(0, 0), (23, 0), (15, 17), (0, 20)]
|
|
20
|
+
SECONDARY = [(23, 0), (32, 0), (32, 32), (17, 21), (15, 17)]
|
|
21
|
+
TERTIARY = [(0, 20), (15, 17), (17, 21), (32, 32), (0, 32)]
|
|
22
|
+
|
|
23
|
+
PLATE = (0x5E, 0x5D, 0x59)
|
|
24
|
+
FILL_PRIMARY = (0xA4, 0xC8, 0xDA)
|
|
25
|
+
FILL_SECONDARY = (0x95, 0xC8, 0xA6)
|
|
26
|
+
FILL_TERTIARY = (0xDD, 0xD8, 0xA8)
|
|
27
|
+
BADGE = (0xB8, 0xB7, 0xAE)
|
|
28
|
+
|
|
29
|
+
# The mark's box is 52% of the canvas. Its corners stay inside the 80% circle
|
|
30
|
+
# (half-diagonal 0.367 of the canvas, safe radius 0.40).
|
|
31
|
+
MARK_SCALE = 0.52
|
|
32
|
+
# Badge center, as an offset from the canvas center, and its side. The badge
|
|
33
|
+
# corners stay inside the same circle.
|
|
34
|
+
BADGE_OFFSET = 0.18
|
|
35
|
+
BADGE_SIDE = 0.09
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _polygon(points: list[tuple[int, int]], size: int) -> list[tuple[float, float]]:
|
|
39
|
+
span = size * MARK_SCALE
|
|
40
|
+
offset = (size - span) / 2
|
|
41
|
+
return [(offset + x / 32 * span, offset + y / 32 * span) for x, y in points]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ink_bounds(image: Image.Image) -> tuple[int, int, int, int]:
|
|
45
|
+
pixels = image.load()
|
|
46
|
+
width, height = image.size
|
|
47
|
+
min_x, min_y = width, height
|
|
48
|
+
max_x, max_y = -1, -1
|
|
49
|
+
for y in range(height):
|
|
50
|
+
for x in range(width):
|
|
51
|
+
if pixels[x, y] == PLATE:
|
|
52
|
+
continue
|
|
53
|
+
min_x = min(min_x, x)
|
|
54
|
+
min_y = min(min_y, y)
|
|
55
|
+
max_x = max(max_x, x)
|
|
56
|
+
max_y = max(max_y, y)
|
|
57
|
+
return (min_x, min_y, max_x, max_y)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def render(size: int, studio: bool) -> Image.Image:
|
|
61
|
+
image = Image.new("RGB", (size, size), PLATE)
|
|
62
|
+
draw = ImageDraw.Draw(image)
|
|
63
|
+
draw.polygon(_polygon(PRIMARY, size), fill=FILL_PRIMARY)
|
|
64
|
+
draw.polygon(_polygon(SECONDARY, size), fill=FILL_SECONDARY)
|
|
65
|
+
draw.polygon(_polygon(TERTIARY, size), fill=FILL_TERTIARY)
|
|
66
|
+
if studio:
|
|
67
|
+
# The badge may only replace stamp pixels. Painting the plate would
|
|
68
|
+
# grow the art and make Studio a different size on the home screen.
|
|
69
|
+
stamped = image.copy()
|
|
70
|
+
side = size * BADGE_SIDE
|
|
71
|
+
center = size * (0.5 + BADGE_OFFSET)
|
|
72
|
+
origin = center - side / 2
|
|
73
|
+
draw.rectangle([origin, origin, origin + side, origin + side], fill=BADGE)
|
|
74
|
+
source = stamped.load()
|
|
75
|
+
painted = image.load()
|
|
76
|
+
for y in range(size):
|
|
77
|
+
for x in range(size):
|
|
78
|
+
if source[x, y] == PLATE:
|
|
79
|
+
painted[x, y] = PLATE
|
|
80
|
+
return image
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main() -> None:
|
|
84
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--repo-root",
|
|
87
|
+
type=Path,
|
|
88
|
+
default=Path(__file__).resolve().parents[1],
|
|
89
|
+
)
|
|
90
|
+
args = parser.parse_args()
|
|
91
|
+
icons = args.repo_root / "assets" / "icons"
|
|
92
|
+
icons.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
site = {
|
|
95
|
+
512: ["icon-512.png", "icon-maskable-512.png"],
|
|
96
|
+
192: ["icon-192.png", "icon-maskable-192.png"],
|
|
97
|
+
180: ["apple-touch-icon.png"],
|
|
98
|
+
32: ["icon-32.png", "favicon-32.png"],
|
|
99
|
+
}
|
|
100
|
+
studio = {
|
|
101
|
+
512: ["studio-icon-512.png", "studio-maskable-512.png"],
|
|
102
|
+
192: ["studio-icon-192.png", "studio-maskable-192.png"],
|
|
103
|
+
180: ["studio-apple-touch-icon.png"],
|
|
104
|
+
32: ["studio-logo-32.png"],
|
|
105
|
+
}
|
|
106
|
+
for size in site:
|
|
107
|
+
plain = render(size, studio=False)
|
|
108
|
+
marked = render(size, studio=True)
|
|
109
|
+
if plain.size != marked.size or _ink_bounds(plain) != _ink_bounds(marked):
|
|
110
|
+
raise SystemExit(
|
|
111
|
+
f"{size}px site and Studio art bounds differ: "
|
|
112
|
+
f"{plain.size} {_ink_bounds(plain)} vs {marked.size} {_ink_bounds(marked)}"
|
|
113
|
+
)
|
|
114
|
+
for name in site[size]:
|
|
115
|
+
plain.save(icons / name, "PNG")
|
|
116
|
+
for name in studio[size]:
|
|
117
|
+
marked.save(icons / name, "PNG")
|
|
118
|
+
icon32 = render(32, studio=False)
|
|
119
|
+
icon16 = render(16, studio=False)
|
|
120
|
+
studio32 = render(32, studio=True)
|
|
121
|
+
studio16 = render(16, studio=True)
|
|
122
|
+
if _ink_bounds(icon16) != _ink_bounds(studio16):
|
|
123
|
+
raise SystemExit("16px site and Studio art bounds differ")
|
|
124
|
+
icon32.save(
|
|
125
|
+
icons / "favicon.ico",
|
|
126
|
+
format="ICO",
|
|
127
|
+
sizes=[(16, 16), (32, 32)],
|
|
128
|
+
append_images=[icon16],
|
|
129
|
+
)
|
|
130
|
+
studio32.save(
|
|
131
|
+
icons / "studio-logo.ico",
|
|
132
|
+
format="ICO",
|
|
133
|
+
sizes=[(16, 16), (32, 32)],
|
|
134
|
+
append_images=[studio16],
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
print(f"Wrote launcher icons under {icons.relative_to(args.repo_root)}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# PLT-AC-27. Home Articles band: titles are 2pt larger, and on a phone the
|
|
5
|
+
# featured card has more room above its text. Listed cards keep their padding.
|
|
6
|
+
|
|
7
|
+
require "pathname"
|
|
8
|
+
|
|
9
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
10
|
+
|
|
11
|
+
def fail!(message)
|
|
12
|
+
warn "FAIL test-home-articles: #{message}"
|
|
13
|
+
exit 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
home = ROOT.join("assets/css/ledger/home.css").read
|
|
17
|
+
|
|
18
|
+
fail!("featured article title is not 2pt larger") unless home.include?(
|
|
19
|
+
'[data-home-kind="articles"] .home-featured-title'
|
|
20
|
+
) && home.include?("calc(var(--home-featured-title-size) + 2pt)")
|
|
21
|
+
|
|
22
|
+
fail!("listed article titles are not 2pt larger") unless home.include?(
|
|
23
|
+
"calc(var(--type-card) + 2pt)"
|
|
24
|
+
) && home.include?('[data-home-kind="articles"] .home-card-grid .card-title')
|
|
25
|
+
|
|
26
|
+
phone = home[/^@media \(max-width: 767px\) \{\n \[data-home-kind="articles"\] \.home-featured \{.*?\n \}/m]
|
|
27
|
+
fail!("phone featured card has no extra top padding") unless phone && phone.include?("padding-top:")
|
|
28
|
+
fail!("phone featured padding changes the listed cards") if phone.include?("ledger-card") || phone.include?("home-card-grid")
|
|
29
|
+
|
|
30
|
+
puts "PASS test-home-articles"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# PLT-AC-26. Listing scripts follow the page, and the runtime stays after
|
|
5
|
+
# it. Those scripts have to start once the runtime is assigned. The
|
|
6
|
+
# Lighthouse script then loads the built example home and fails when the
|
|
7
|
+
# project, article, or post bands are still empty.
|
|
8
|
+
|
|
9
|
+
require "pathname"
|
|
10
|
+
require "open3"
|
|
11
|
+
|
|
12
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
13
|
+
|
|
14
|
+
def fail!(message)
|
|
15
|
+
warn "FAIL test-home-lists: #{message}"
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
runtime = ROOT.join("_includes/page/content-runtime.html").read
|
|
20
|
+
assigned = runtime.index("window.ContentRuntime = (function () {")
|
|
21
|
+
dispatched = runtime.index("document.dispatchEvent(new CustomEvent('pandorga:runtime'))")
|
|
22
|
+
fail!("runtime does not assign ContentRuntime") unless assigned
|
|
23
|
+
fail!("runtime does not announce pandorga:runtime") unless dispatched
|
|
24
|
+
fail!("runtime announces itself before the assignment") unless dispatched > assigned
|
|
25
|
+
fail!("runtime moved back ahead of the page") unless ROOT.join("_layouts/shell.html").read.index("{{ content }}") <
|
|
26
|
+
ROOT.join("_layouts/shell.html").read.index("page/content-runtime.html")
|
|
27
|
+
|
|
28
|
+
pages = %w[
|
|
29
|
+
_layouts/home.html
|
|
30
|
+
_layouts/articles.html
|
|
31
|
+
_layouts/blog.html
|
|
32
|
+
_layouts/projects.html
|
|
33
|
+
_layouts/cv.html
|
|
34
|
+
_layouts/resources.html
|
|
35
|
+
_layouts/bibliography.html
|
|
36
|
+
_includes/page/network-graph.html
|
|
37
|
+
_includes/page/timeline-section.html
|
|
38
|
+
]
|
|
39
|
+
pages.each do |relative|
|
|
40
|
+
text = ROOT.join(relative).read
|
|
41
|
+
fail!("#{relative} does not wait for pandorga:runtime") unless text.include?("pandorga:runtime")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
home = ROOT.join("_layouts/home.html").read
|
|
45
|
+
fail!("home paint still runs before the runtime") unless home.scan("pandorga:runtime").length >= 2
|
|
46
|
+
fail!("home bands still return when the runtime is missing") if home.include?("if (!runtime) return")
|
|
47
|
+
|
|
48
|
+
marked = ROOT.join("_includes/content-runtime/00-config.html").read
|
|
49
|
+
fail!("markdown library no longer defers") unless marked.include?("<script defer ")
|
|
50
|
+
|
|
51
|
+
shell = ROOT.join("_layouts/shell.html").read
|
|
52
|
+
fail!("text faces no longer wait for the first paint") unless shell.scan("requestAnimationFrame").length >= 2
|
|
53
|
+
fail!("icon subset link was removed") unless shell.include?("__pandorgaIconsHref")
|
|
54
|
+
|
|
55
|
+
spec = ROOT.join("docs/spec.md").read
|
|
56
|
+
fail!("spec is missing PLT-AC-26") unless spec.include?("PLT-AC-26")
|
|
57
|
+
fail!("spec no longer keeps the runtime after the page") unless spec.include?("runtime stays after the page")
|
|
58
|
+
|
|
59
|
+
lighthouse = ROOT.join("scripts/lighthouse-home.sh").read
|
|
60
|
+
checker = lighthouse.index("check_home_lists.mjs")
|
|
61
|
+
score = lighthouse.index("Lighthouse mobile (one run)")
|
|
62
|
+
fail!("lighthouse script does not load the home lists") unless checker
|
|
63
|
+
fail!("list check runs only after the score") unless score && checker < score
|
|
64
|
+
|
|
65
|
+
probe = ROOT.join("scripts/check_home_lists.mjs").read
|
|
66
|
+
fail!("probe ignores the project inventory") unless probe.include?("#home-projects .home-project-inventory-row")
|
|
67
|
+
fail!("probe ignores article cards") unless probe.include?("#home-articles .home-featured, #home-articles .ledger-card")
|
|
68
|
+
fail!("probe ignores the phone viewport") unless probe.include?("name: 'mobile'")
|
|
69
|
+
fail!("probe ignores the desktop viewport") unless probe.include?("name: 'desktop'")
|
|
70
|
+
|
|
71
|
+
_out, err, status = Open3.capture3("node", "--check", ROOT.join("scripts/check_home_lists.mjs").to_s)
|
|
72
|
+
fail!("home list probe is not valid JavaScript\n#{err}") unless status.success?
|
|
73
|
+
|
|
74
|
+
puts "PASS test-home-lists"
|