Repository navigation
Expand file tree
/
Copy pathreport.mjs
More file actions
148 lines (144 loc) · 6.59 KB
/
Copy pathreport.mjs
File metadata and controls
148 lines (144 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// The survey's tables, and how a run compares with an earlier one (the baseline).
const total = (j) => j.clicks + j.keys + j.prompts + (j.swipes ?? 0);
/** Per-journey changes from `base` to `now`: worse when a journey takes more, or fails where it passed. */
export function compare(now, base) {
const before = new Map(base.journeys.map((j) => [j.id, j]));
const out = [];
for (const j of now.journeys) {
const b = before.get(j.id);
if (!b) {
out.push({ id: j.id, what: "new", worse: false });
continue;
}
if (!j.ok && b.ok) {
out.push({ id: j.id, what: "fails now", worse: true });
continue;
}
if (!j.ok) {
out.push({ id: j.id, what: "still can't be done", worse: false });
continue;
}
if (!b.ok) {
out.push({ id: j.id, what: "can be done now", worse: false });
continue;
}
const d = {
clicks: j.clicks - b.clicks,
keys: j.keys - b.keys,
prompts: j.prompts - b.prompts,
swipes: (j.swipes ?? 0) - (b.swipes ?? 0),
};
if (d.clicks || d.keys || d.prompts || d.swipes)
out.push({
id: j.id,
what: Object.entries(d)
.filter(([, v]) => v)
.map(([k, v]) => `${v > 0 ? "+" : ""}${v} ${k}`)
.join(", "),
delta: d,
worse: total(j) > total(b),
});
}
for (const b of base.journeys) if (!now.journeys.some((j) => j.id === b.id)) out.push({ id: b.id, what: "gone", worse: false });
return out;
}
/** What each page asked of people across all journeys, with how far it is from Home and how busy it is. */
export function perPage(result) {
const pages = new Map();
const get = (key) => {
if (!pages.has(key)) pages.set(key, { page: key, clicks: 0, keys: 0, prompts: 0, swipes: 0, journeys: new Set() });
return pages.get(key);
};
for (const j of result.journeys)
for (const s of j.steps) {
const p = get(s.page);
p.clicks += s.clicks;
p.keys += s.keys;
p.prompts += s.prompts;
p.swipes += s.swipes ?? 0;
p.journeys.add(j.id);
}
for (const s of result.pages || []) Object.assign(get(s.page), { label: s.label, reach: s.reach, controls: s.controls });
return [...pages.values()]
.map((p) => ({ ...p, journeys: [...p.journeys].sort() }))
.sort((a, b) => a.page.localeCompare(b.page));
}
export function markdown(result, base) {
const diff = base ? new Map(compare(result, base).map((d) => [d.id, d])) : null;
const phone = result.device === "phone";
// On a phone clicks are taps, and swipes (bringing a control into view) are counted too
const Clicks = phone ? "Taps" : "Clicks";
const sw = (x) => (phone ? ` ${x} |` : "");
const lines = [
`# ${Clicks}, keystrokes${phone ? ", swipes" : ""} and passkey touches per feature${phone ? " on a phone" : ""}`,
"",
`Surveyed ${result.surveyed}${result.commit ? ` at ${result.commit.slice(0, 7)}` : ""}, Chromium ${result.viewport}.`,
`${Clicks} include the ${phone ? "tap" : "click"} that focuses a field before typing; keys are characters typed plus keys and shortcuts`,
"pressed (a paste is one); prompts are passkey touches." +
(phone ? " Swipes bring a control that is off the screen into view." : "") +
" Generated by ci/clicks; see its README for the rules.",
"",
];
const modules = [...new Set(result.journeys.map((j) => j.module))];
let sum = { clicks: 0, keys: 0, prompts: 0, swipes: 0 };
lines.push(`| Module | Feature | ${Clicks} | Keys | Prompts |${sw("Swipes")} Total | Pages${diff ? " | vs baseline" : ""} |`);
lines.push(`|---|---|--:|--:|--:|${sw("--:")}--:|---${diff ? "|---" : ""}|`);
for (const m of modules)
for (const j of result.journeys.filter((x) => x.module === m)) {
sum = { clicks: sum.clicks + j.clicks, keys: sum.keys + j.keys, prompts: sum.prompts + j.prompts, swipes: sum.swipes + (j.swipes ?? 0) };
const pages = [...new Set(j.steps.map((s) => s.page))].join(", ");
const d = diff?.get(j.id);
const feature = j.ok ? j.feature : `${j.feature} (failed: ${j.error})`;
lines.push(
`| ${m} | ${feature} | ${j.clicks} | ${j.keys} | ${j.prompts} |${sw(j.swipes ?? 0)} ${total(j)} | ${pages}${diff ? ` | ${d ? d.what : "same"}` : ""} |`,
);
}
lines.push(
`| **All** | **${result.journeys.length} features** | **${sum.clicks}** | **${sum.keys}** | **${sum.prompts}** |${sw(`**${sum.swipes}**`)} **${sum.clicks + sum.keys + sum.prompts + sum.swipes}** | |`,
);
const covered = (result.pages || []).filter((p) => p.askCovers?.length);
if (covered.length) {
lines.push("", "## Covered by the Ask button", "", "With the page at its end, the floating Ask button lies over these:", "");
for (const p of covered) lines.push(`- ${p.label} (\`${p.page}\`): ${p.askCovers.join(", ")}`);
}
lines.push("", "## Per module", "", `| Module | Features | ${Clicks} | Keys | Prompts |${sw("Swipes")}`, `|---|--:|--:|--:|--:|${sw("--:")}`);
for (const m of modules) {
const js = result.journeys.filter((x) => x.module === m);
const s = (k) => js.reduce((n, j) => n + (j[k] ?? 0), 0);
lines.push(`| ${m} | ${js.length} | ${s("clicks")} | ${s("keys")} | ${s("prompts")} |${sw(s("swipes"))}`);
}
lines.push(
"",
"## Per page",
"",
`Reach is ${Clicks.toLowerCase()} from Home through the navigation${phone ? " (the menu button first)" : ""}; controls are the buttons, links and fields showing in the page's`,
`main area on a fresh install (a measure of how busy it is). ${Clicks} and keys are what the features above did there.`,
"",
`| Page | Reach | Controls | ${Clicks} | Keys | Prompts |${sw("Swipes")} Features |`,
`|---|--:|--:|--:|--:|--:|${sw("--:")}---|`,
);
for (const p of perPage(result))
lines.push(
`| ${p.label ? `${p.label} (\`${p.page}\`)` : `\`${p.page}\``} | ${p.reach ?? ""} | ${p.controls ?? ""} | ${p.clicks || ""} | ${p.keys || ""} | ${p.prompts || ""} |${sw(p.swipes || "")} ${p.journeys.join(", ")} |`,
);
lines.push("", "## Steps", "");
const act = phone ? "tap" : "click";
for (const j of result.journeys) {
lines.push(`### ${j.module}: ${j.feature}`, "");
if (j.note) lines.push(j.note, "");
if (!j.ok) lines.push(`Failed: ${j.error}`, "");
for (const s of j.steps) {
const cost = [
s.swipes && `${s.swipes} swipe${s.swipes > 1 ? "s" : ""}`,
s.clicks && `${s.clicks} ${act}${s.clicks > 1 ? "s" : ""}`,
s.keys && `${s.keys} key${s.keys > 1 ? "s" : ""}`,
s.prompts && "passkey",
]
.filter(Boolean)
.join(" + ");
lines.push(`1. \`${s.page}\` ${s.what} (${cost})`);
}
lines.push("");
}
return lines.join("\n");
}