@luigisbox/luma 0.0.0-stage → 0.2.0
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.
- package/LICENSE.md +90 -0
- package/README.md +615 -2
- package/a11y/audit.mjs +735 -0
- package/a11y/axe.mjs +73 -0
- package/a11y/report.mjs +347 -0
- package/a11y/run.mjs +256 -0
- package/a11y/wcag.mjs +233 -0
- package/bin/config.mjs +60 -0
- package/bin/luma.mjs +99 -0
- package/bin/paths.mjs +11 -0
- package/bin/tools.mjs +43 -0
- package/build/baseline.d.mts +8 -0
- package/build/baseline.mjs +167 -0
- package/build/tooling.mjs +102 -0
- package/build/vite.mjs +507 -0
- package/chrome/block.mjs +8 -0
- package/chrome/run.mjs +150 -0
- package/docs/ACCESSIBILITY.md +501 -0
- package/docs/INTEGRATION-PERFORMANCE.md +213 -0
- package/docs/PARITY.md +946 -0
- package/footprint/model.mjs +184 -0
- package/footprint/run.mjs +331 -0
- package/package.json +58 -4
- package/parity/alignment.mjs +109 -0
- package/parity/archive.mjs +223 -0
- package/parity/args.mjs +137 -0
- package/parity/behaviour.mjs +379 -0
- package/parity/compare.mjs +985 -0
- package/parity/engine.mjs +848 -0
- package/parity/hints.mjs +132 -0
- package/parity/hold.mjs +54 -0
- package/parity/init.mjs +703 -0
- package/parity/map.mjs +640 -0
- package/parity/owner.mjs +170 -0
- package/parity/pairing.mjs +110 -0
- package/parity/pixels.mjs +33 -0
- package/parity/probe.mjs +1239 -0
- package/parity/report.mjs +236 -0
- package/parity/run.mjs +267 -0
- package/snippet/early-hold.js +64 -0
- package/snippet/early-hold.min.js +1 -0
- package/src/analytics/chain.ts +137 -0
- package/src/analytics/collect.ts +64 -0
- package/src/analytics/datalayer.ts +67 -0
- package/src/analytics/from-ga4.ts +68 -0
- package/src/analytics/from-luma.ts +21 -0
- package/src/analytics/index.ts +24 -0
- package/src/analytics/outbox.ts +118 -0
- package/src/analytics/page.ts +46 -0
- package/src/analytics/pageview.ts +82 -0
- package/src/analytics/ref.ts +19 -0
- package/src/analytics/setup.ts +41 -0
- package/src/analytics/start.ts +53 -0
- package/src/analytics/triggers.ts +239 -0
- package/src/analytics/user.ts +12 -0
- package/src/analytics/wire.ts +67 -0
- package/src/api/ai.ts +98 -0
- package/src/api/index.ts +87 -0
- package/src/api/token-key.ts +42 -0
- package/src/api/token.ts +312 -0
- package/src/core/a11y-keys.ts +60 -0
- package/src/core/a11y.ts +160 -0
- package/src/core/boot-state.ts +6 -0
- package/src/core/color.ts +42 -0
- package/src/core/context.ts +108 -0
- package/src/core/early-rules.ts +96 -0
- package/src/core/entries.ts +25 -0
- package/src/core/fold.ts +7 -0
- package/src/core/health.ts +26 -0
- package/src/core/hold.ts +74 -0
- package/src/core/i18n.ts +66 -0
- package/src/core/image-load.ts +102 -0
- package/src/core/image-tags.ts +26 -0
- package/src/core/images.ts +170 -0
- package/src/core/integration.ts +126 -0
- package/src/core/kept.ts +25 -0
- package/src/core/listings.ts +100 -0
- package/src/core/locales.ts +14 -0
- package/src/core/own.ts +49 -0
- package/src/core/page.ts +14 -0
- package/src/core/prefetch.ts +88 -0
- package/src/core/recommenders.ts +166 -0
- package/src/core/reconciler.ts +397 -0
- package/src/core/result.ts +73 -0
- package/src/core/routes.ts +115 -0
- package/src/core/scheduler.ts +83 -0
- package/src/core/sense.ts +107 -0
- package/src/core/speculation.ts +59 -0
- package/src/core/stash.ts +38 -0
- package/src/core/storage.ts +45 -0
- package/src/core/styles.ts +106 -0
- package/src/core/support.ts +12 -0
- package/src/core/takeover.ts +220 -0
- package/src/core/templates.ts +74 -0
- package/src/core/tiles.ts +137 -0
- package/src/core/turbo.ts +64 -0
- package/src/core/url.ts +164 -0
- package/src/core/widget-inits.ts +40 -0
- package/src/env.d.ts +3 -0
- package/src/lit.ts +11 -0
- package/src/locales/cs.json +94 -0
- package/src/locales/de.json +88 -0
- package/src/locales/en.json +88 -0
- package/src/main.ts +493 -0
- package/src/types.ts +778 -0
- package/src/widgets/autocomplete/index.ts +1387 -0
- package/src/widgets/autocomplete/style-autocomplete.css +266 -0
- package/src/widgets/autocomplete/template-autocomplete-box.ts +61 -0
- package/src/widgets/autocomplete/template-autocomplete-empty.ts +53 -0
- package/src/widgets/autocomplete/template-autocomplete-result-default.ts +82 -0
- package/src/widgets/autocomplete/template-autocomplete-sheet.ts +74 -0
- package/src/widgets/listing/element.ts +1772 -0
- package/src/widgets/listing/flow.ts +30 -0
- package/src/widgets/listing/instant.ts +133 -0
- package/src/widgets/listing/intent.ts +193 -0
- package/src/widgets/listing/query.ts +298 -0
- package/src/widgets/listing/stash.ts +97 -0
- package/src/widgets/listing/state.ts +131 -0
- package/src/widgets/listing/view.ts +257 -0
- package/src/widgets/plp/index.ts +11 -0
- package/src/widgets/plp/route.ts +263 -0
- package/src/widgets/recommender/index.ts +261 -0
- package/src/widgets/recommender/style-recommender.css +26 -0
- package/src/widgets/recommender/template-recommender-box-default.ts +11 -0
- package/src/widgets/recommender/template-recommender-popup.ts +12 -0
- package/src/widgets/recommender/template-recommender-result-default.ts +10 -0
- package/src/widgets/search/assets.ts +7 -0
- package/src/widgets/search/first.ts +15 -0
- package/src/widgets/search/form-url.ts +59 -0
- package/src/widgets/search/in-place.ts +226 -0
- package/src/widgets/search/index.ts +16 -0
- package/src/widgets/search/range.ts +260 -0
- package/src/widgets/search/route.ts +32 -0
- package/src/widgets/search/speculate.ts +202 -0
- package/src/widgets/search/style-search.css +65 -0
- package/src/widgets/search/template-search-box.ts +17 -0
- package/src/widgets/search/template-search-facets-default.ts +73 -0
- package/src/widgets/search/template-search-header.ts +14 -0
- package/src/widgets/search/template-search-pagination.ts +12 -0
- package/src/widgets/search/template-search-range-default.ts +48 -0
- package/src/widgets/search/template-search-result-default.ts +13 -0
- package/src/widgets/search/template-search-results-default.ts +8 -0
- package/tasks/model.mjs +116 -0
- package/tasks/run.mjs +305 -0
- package/tsconfig.json +14 -0
package/tasks/run.mjs
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// yarn luma tasks: which main-thread tasks of ours are over a frame's budget on the
|
|
3
|
+
// storefront, and what is in them. Run from the integration repository (Luma
|
|
4
|
+
// as the @luigisbox/luma package, or an older luma/ submodule):
|
|
5
|
+
//
|
|
6
|
+
// yarn luma tasks every scenario, 3 runs each, 4x CPU, budget 16 ms
|
|
7
|
+
// yarn luma tasks --only category,more some of them
|
|
8
|
+
// yarn luma tasks --headed show the browser
|
|
9
|
+
//
|
|
10
|
+
// (bin/luma.mjs; the storefront's host from luma.config.mjs `host`, or LBX_HOST.)
|
|
11
|
+
//
|
|
12
|
+
// Scenarios: the pages loaded (luma.config.mjs `perf.pages`, else home and
|
|
13
|
+
// its fixtures' category, search and product), then on the category page (or
|
|
14
|
+
// the search page) a change of the listing's state (`change`: its first
|
|
15
|
+
// visible link that leads to another state, `[data-lbx-href]`, which Luma's
|
|
16
|
+
// facets and sort controls carry), `more` (`[data-lbx-more]`), `back` (to the
|
|
17
|
+
// state before the change), and `typing` (fixtures.query typed into
|
|
18
|
+
// fixtures.input or `_global.inputSelector`, at 140 ms a key).
|
|
19
|
+
//
|
|
20
|
+
// Each run takes a CPU profile of the page's isolate and a trace of its tasks
|
|
21
|
+
// together (tasks/model.mjs): a task's time, ours (a frame of ours on the
|
|
22
|
+
// stack) and our own (the innermost frame with a script is ours: not the
|
|
23
|
+
// site's code under our calls, e.g. its pushState hooks). Functions are named
|
|
24
|
+
// by the integration's source (a build with source maps, into a temporary
|
|
25
|
+
// folder; dist/ is left as it is). The verdict per scenario is the median of
|
|
26
|
+
// each run's worst task by our own time; exit 1 when one is over the budget.
|
|
27
|
+
//
|
|
28
|
+
// The page is the live storefront (its host known) with Luma put in as a customer
|
|
29
|
+
// installs it (the early-hold snippet, then the build: parity/hold.mjs), or
|
|
30
|
+
// the integration's stub page (--origin stub, luma.config.mjs stubPage). A
|
|
31
|
+
// storefront that answers with a bot challenge is never worked around: the
|
|
32
|
+
// run says so and falls back to the stub page. Run it on the client's own site
|
|
33
|
+
// with their knowledge, or ask them to allowlist the runner. Nothing of the site is changed.
|
|
34
|
+
import { execFileSync } from 'node:child_process';
|
|
35
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { tmpdir } from 'node:os';
|
|
37
|
+
import { extname, join, normalize, resolve } from 'node:path';
|
|
38
|
+
import { inside } from '../bin/paths.mjs';
|
|
39
|
+
import { attribute, formatReport, mainThreadTasks, sampleTimes, summarize } from './model.mjs';
|
|
40
|
+
|
|
41
|
+
const USAGE = `usage: yarn luma tasks [options]
|
|
42
|
+
|
|
43
|
+
--only a,b scenarios: home, category, search, product (the pages), change, more, back, typing
|
|
44
|
+
--runs N runs per scenario (default 3)
|
|
45
|
+
--cpu N CPU throttling (default 4)
|
|
46
|
+
--budget MS our own time per task (default 16: a frame)
|
|
47
|
+
--origin stub|live the live storefront (default where its host is known: luma.config.mjs host, LBX_HOST) or the stub page
|
|
48
|
+
--headed show the browser
|
|
49
|
+
--no-build use dist/ as it is (no source maps: functions by their minified names)
|
|
50
|
+
--json FILE also write the results as JSON
|
|
51
|
+
--site-dir DIR the integration repository (default: the current directory, LBX_SITE_DIR)`;
|
|
52
|
+
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
const opt = { only: [], runs: 3, cpu: 4, budget: 16, origin: '', headed: false, build: true, json: '', siteDir: '' };
|
|
55
|
+
const die = (msg) => {
|
|
56
|
+
console.error(msg);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
};
|
|
59
|
+
for (let i = 0; i < args.length; i++) {
|
|
60
|
+
const a = args[i];
|
|
61
|
+
const next = () => {
|
|
62
|
+
const v = args[++i];
|
|
63
|
+
if (v === undefined || v.startsWith('--')) die(`${a} needs a value\n\n${USAGE}`);
|
|
64
|
+
return v;
|
|
65
|
+
};
|
|
66
|
+
if (a === '--help' || a === '-h') {
|
|
67
|
+
console.log(USAGE);
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} else if (a === '--only') opt.only = next().split(',').map((s) => s.trim()).filter(Boolean);
|
|
70
|
+
else if (a === '--runs') opt.runs = Number(next());
|
|
71
|
+
else if (a === '--cpu') opt.cpu = Number(next());
|
|
72
|
+
else if (a === '--budget') opt.budget = Number(next());
|
|
73
|
+
else if (a === '--origin') opt.origin = next();
|
|
74
|
+
else if (a === '--headed') opt.headed = true;
|
|
75
|
+
else if (a === '--no-build') opt.build = false;
|
|
76
|
+
else if (a === '--json') opt.json = next();
|
|
77
|
+
else if (a === '--site-dir') opt.siteDir = next();
|
|
78
|
+
else die(`unknown option ${a}\n\n${USAGE}`);
|
|
79
|
+
}
|
|
80
|
+
const SITE_DIR = resolve(opt.siteDir || process.env.LBX_SITE_DIR || process.cwd());
|
|
81
|
+
if (!existsSync(join(SITE_DIR, 'integration'))) die(`${SITE_DIR} has no integration/ folder: run it from the integration repository (or --site-dir).`);
|
|
82
|
+
process.env.LBX_SITE_DIR = SITE_DIR;
|
|
83
|
+
if (opt.origin && !/^(stub|live)$/.test(opt.origin)) die(`--origin must be stub or live, not "${opt.origin}"`);
|
|
84
|
+
|
|
85
|
+
for (const dep of ['playwright-core', '@jridgewell/trace-mapping']) {
|
|
86
|
+
try {
|
|
87
|
+
await import(dep);
|
|
88
|
+
} catch {
|
|
89
|
+
die(`Luma's dependencies are not installed (${dep} is missing): run \`yarn install\`.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const { chromium } = await import('playwright-core');
|
|
93
|
+
const { TraceMap, originalPositionFor } = await import('@jridgewell/trace-mapping');
|
|
94
|
+
const { loadConfig } = await import('../bin/config.mjs');
|
|
95
|
+
const config = await loadConfig(SITE_DIR);
|
|
96
|
+
const { host: HOST, stubPage } = config;
|
|
97
|
+
const { SNIPPET, holdInit } = await import('../parity/hold.mjs');
|
|
98
|
+
|
|
99
|
+
const F = config.fixtures;
|
|
100
|
+
const ORIGIN = `https://${HOST}`;
|
|
101
|
+
const PREFIX = '/__lbx/tasks/';
|
|
102
|
+
const BUNDLE = `${ORIGIN}${PREFIX}luma-integration.js`;
|
|
103
|
+
// The live storefront where its host is known (LBX_HOST, luma.config.mjs `host`), else the stub page.
|
|
104
|
+
let stub = (opt.origin || (config.hostKnown ? 'live' : 'stub')) === 'stub';
|
|
105
|
+
|
|
106
|
+
// ------------------------------------------------------------------ build (source maps, a folder of its own)
|
|
107
|
+
|
|
108
|
+
let DIST = join(SITE_DIR, 'dist');
|
|
109
|
+
let temp = '';
|
|
110
|
+
if (opt.build) {
|
|
111
|
+
temp = DIST = mkdtempSync(join(tmpdir(), 'lbx-tasks-'));
|
|
112
|
+
process.stderr.write('building with source maps… ');
|
|
113
|
+
try {
|
|
114
|
+
execFileSync('yarn', ['vite', 'build', '--sourcemap', '--outDir', DIST, '--emptyOutDir'], { cwd: SITE_DIR, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
115
|
+
process.stderr.write('done\n');
|
|
116
|
+
} catch (e) {
|
|
117
|
+
process.stderr.write('failed\n');
|
|
118
|
+
die(`the build failed:\n${e.stdout ?? ''}${e.stderr ?? ''}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!existsSync(join(DIST, 'luma-integration.js'))) die(`${DIST}/luma-integration.js is missing: run yarn build (or drop --no-build).`);
|
|
122
|
+
process.on('exit', () => temp && rmSync(temp, { recursive: true, force: true }));
|
|
123
|
+
|
|
124
|
+
// Frames of ours: the build's scripts. Named by their source where the build has maps.
|
|
125
|
+
const OURS = new RegExp(`${PREFIX.replace(/\//g, '\\/')}`);
|
|
126
|
+
const ours = (f) => OURS.test(f.url ?? '');
|
|
127
|
+
const maps = new Map();
|
|
128
|
+
const nameOf = (f) => {
|
|
129
|
+
const rel = (f.url ?? '').split(PREFIX)[1];
|
|
130
|
+
if (rel !== undefined && !maps.has(rel)) {
|
|
131
|
+
const file = join(DIST, `${rel}.map`);
|
|
132
|
+
maps.set(rel, existsSync(file) ? new TraceMap(readFileSync(file, 'utf8')) : null);
|
|
133
|
+
}
|
|
134
|
+
const m = rel === undefined ? null : maps.get(rel);
|
|
135
|
+
const p = m ? originalPositionFor(m, { line: f.lineNumber + 1, column: f.columnNumber }) : null;
|
|
136
|
+
if (p?.source) return `${p.name || f.functionName || '(anonymous)'} ${p.source.replace(/^.*?\b(src|integration|node_modules)\//, '$1/')}:${p.line}`;
|
|
137
|
+
return `${f.functionName || '(anonymous)'} ${rel ?? f.url}:${f.lineNumber + 1}`;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ------------------------------------------------------------------ browser
|
|
141
|
+
|
|
142
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.map': 'application/json' };
|
|
143
|
+
const launch = async () => {
|
|
144
|
+
const o = { headless: !opt.headed };
|
|
145
|
+
const tries = [...(process.env.LBX_CHROMIUM_PATH ? [{ ...o, executablePath: process.env.LBX_CHROMIUM_PATH }] : []), { ...o, channel: 'chromium' }, { ...o, channel: 'chrome' }];
|
|
146
|
+
let err;
|
|
147
|
+
for (const t of tries) {
|
|
148
|
+
try {
|
|
149
|
+
return await chromium.launch(t);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
err = e;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
die(`No Chromium to run: install Chrome, or \`yarn playwright-core install chromium\`, or set LBX_CHROMIUM_PATH.\n${err?.message?.split('\n')[0] ?? ''}`);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const newContext = async (browser) => {
|
|
158
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, serviceWorkers: 'block' });
|
|
159
|
+
await ctx.route(/^https:\/\/(cdn\.luigisbox\.(com|tech)|cdn\.eu1\.luigisbox\.ai|scripts\.luigisbox\.(com|tech))\//, (r) => r.abort('blockedbyclient'));
|
|
160
|
+
await ctx.route(`${ORIGIN}/**`, async (route) => {
|
|
161
|
+
const req = route.request();
|
|
162
|
+
const url = new URL(req.url());
|
|
163
|
+
if (url.pathname.startsWith(PREFIX)) {
|
|
164
|
+
const file = normalize(join(DIST, decodeURIComponent(url.pathname.slice(PREFIX.length))));
|
|
165
|
+
if (!inside(DIST, file) || !existsSync(file) || !statSync(file).isFile()) return route.fulfill({ status: 404, body: '' });
|
|
166
|
+
return route.fulfill({ status: 200, body: readFileSync(file), headers: { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream', 'cache-control': 'no-store' } });
|
|
167
|
+
}
|
|
168
|
+
if (!stub) return route.fallback();
|
|
169
|
+
if (req.resourceType() === 'document') return route.fulfill({ status: 200, contentType: 'text/html; charset=utf-8', body: await stubPage(url) });
|
|
170
|
+
return route.fulfill({ status: 404, body: '' });
|
|
171
|
+
});
|
|
172
|
+
await ctx.addInitScript(holdInit, { origin: ORIGIN, bundle: BUNDLE, snippet: SNIPPET, tag: 'tasks' });
|
|
173
|
+
return ctx;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const challenged = async (res, page) => {
|
|
177
|
+
if (!res) return 'no response';
|
|
178
|
+
if (res.headers()['cf-mitigated'] === 'challenge') return `a Cloudflare challenge (HTTP ${res.status()})`;
|
|
179
|
+
if (res.status() === 429) return 'HTTP 429 (rate limited)';
|
|
180
|
+
if (res.status() === 403 || res.status() === 503) {
|
|
181
|
+
const text = await page.content().catch(() => '');
|
|
182
|
+
return /challenge-platform|cf-chl|Just a moment|captcha|Attention Required/i.test(text) ? `a bot challenge (HTTP ${res.status()})` : `HTTP ${res.status()}`;
|
|
183
|
+
}
|
|
184
|
+
return '';
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ------------------------------------------------------------------ scenarios
|
|
188
|
+
|
|
189
|
+
const pages = config.perf?.pages ?? Object.fromEntries([['home', '/'], ['category', F.category], ['search', F.query ? `/search?q=${encodeURIComponent(F.query)}` : null], ['product', F.product]].filter(([, p]) => p));
|
|
190
|
+
const listingPath = pages.category ?? pages.search;
|
|
191
|
+
const scenarios = [
|
|
192
|
+
...Object.entries(pages).map(([name, path]) => ({ name, path, load: true })),
|
|
193
|
+
...(listingPath ? ['change', 'more', 'back'].map((name) => ({ name, path: listingPath })) : []),
|
|
194
|
+
...(F.query ? [{ name: 'typing', path: '/' }] : []),
|
|
195
|
+
].filter((s) => !opt.only.length || opt.only.includes(s.name));
|
|
196
|
+
if (!scenarios.length) die('no scenario to run (luma.config.mjs perf.pages or fixtures; --only)');
|
|
197
|
+
|
|
198
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
199
|
+
const LISTING = 'lbx-plp, lbx-search';
|
|
200
|
+
/** Ours has drawn and is not busy (a listing), or the page has had its time (any other). */
|
|
201
|
+
const settled = (page) =>
|
|
202
|
+
page.waitForFunction((sel) => {
|
|
203
|
+
const e = document.querySelector(sel);
|
|
204
|
+
return !e || (!!e.dataset.total && e.getAttribute('aria-busy') !== 'true');
|
|
205
|
+
}, LISTING, { timeout: 30000, polling: 100 }).catch(() => {});
|
|
206
|
+
/** A click as a pointer's (detail 1: Luma tells a keyboard's "show more" by detail 0), dispatched: a consent dialog over the page does not take it. */
|
|
207
|
+
const click = (page, sel) =>
|
|
208
|
+
page.evaluate(([sel, listing]) => {
|
|
209
|
+
const scope = document.querySelector(listing) ?? document;
|
|
210
|
+
const el = [...scope.querySelectorAll(sel)].find((x) => x.getBoundingClientRect().width > 0 && (!x.dataset.lbxHref || new URL(x.dataset.lbxHref, location.href).href !== location.href));
|
|
211
|
+
if (!el) return false;
|
|
212
|
+
el.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
|
|
213
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, detail: 1 }));
|
|
214
|
+
return true;
|
|
215
|
+
}, [sel, LISTING]);
|
|
216
|
+
|
|
217
|
+
const measure = async (browser, s) => {
|
|
218
|
+
const ctx = await newContext(browser);
|
|
219
|
+
const page = await ctx.newPage();
|
|
220
|
+
const cdp = await ctx.newCDPSession(page);
|
|
221
|
+
await cdp.send('Emulation.setCPUThrottlingRate', { rate: opt.cpu });
|
|
222
|
+
await cdp.send('Profiler.enable');
|
|
223
|
+
await cdp.send('Profiler.setSamplingInterval', { interval: 100 });
|
|
224
|
+
const trace = [];
|
|
225
|
+
cdp.on('Tracing.dataCollected', (d) => trace.push(...d.value));
|
|
226
|
+
const start = async () => {
|
|
227
|
+
await cdp.send('Tracing.start', { traceConfig: { includedCategories: ['toplevel'] }, transferMode: 'ReportEvents' });
|
|
228
|
+
await cdp.send('Profiler.start');
|
|
229
|
+
};
|
|
230
|
+
let note = '';
|
|
231
|
+
try {
|
|
232
|
+
if (s.load) await start();
|
|
233
|
+
const res = await page.goto(`${ORIGIN}${s.path}`, { waitUntil: 'load', timeout: 60000 });
|
|
234
|
+
if (!stub) {
|
|
235
|
+
const why = await challenged(res, page);
|
|
236
|
+
if (why) return { fallback: why };
|
|
237
|
+
}
|
|
238
|
+
await settled(page);
|
|
239
|
+
await sleep(2500);
|
|
240
|
+
if (!s.load) {
|
|
241
|
+
if (s.name === 'back' && !(await click(page, '[data-lbx-href]'))) note = 'no [data-lbx-href] to change the state before Back';
|
|
242
|
+
if (s.name === 'back') await settled(page), await sleep(1500);
|
|
243
|
+
if (s.name === 'typing') {
|
|
244
|
+
const sel = F.input || (await page.evaluate(() => window.__lbx?.config?._global?.inputSelector)) || 'input[name=q]';
|
|
245
|
+
await page.evaluate((sel) => document.querySelector(sel)?.focus(), sel);
|
|
246
|
+
await sleep(600);
|
|
247
|
+
}
|
|
248
|
+
const before = await page.evaluate(() => window.__lbx?.health?.timeline?.length ?? 0);
|
|
249
|
+
await start();
|
|
250
|
+
if (s.name === 'change' && !(await click(page, '[data-lbx-href]'))) note = 'no [data-lbx-href] in the listing';
|
|
251
|
+
if (s.name === 'more' && !(await click(page, '[data-lbx-more]'))) note = 'no [data-lbx-more] in the listing';
|
|
252
|
+
if (s.name === 'back') await page.goBack();
|
|
253
|
+
if (s.name === 'typing') await page.keyboard.type(F.query, { delay: 140 }), await sleep(2000);
|
|
254
|
+
await sleep(300);
|
|
255
|
+
await settled(page);
|
|
256
|
+
await sleep(1200);
|
|
257
|
+
// What the scenario did, from Luma's health timeline: a run that did nothing measures nothing.
|
|
258
|
+
const marks = await page.evaluate((n) => (window.__lbx?.health?.timeline ?? []).slice(n).map(([, w]) => w), before);
|
|
259
|
+
const want = s.name === 'typing' ? /^ac shown / : /^\S+ render \d+ hits$/;
|
|
260
|
+
if (!note && !marks.some((w) => want.test(w))) note = s.name === 'typing' ? 'no suggestions were shown (fixtures.input / _global.inputSelector, fixtures.query)' : 'the listing did not render';
|
|
261
|
+
}
|
|
262
|
+
const { profile } = await cdp.send('Profiler.stop');
|
|
263
|
+
const done = new Promise((r) => cdp.once('Tracing.tracingComplete', r));
|
|
264
|
+
await cdp.send('Tracing.end');
|
|
265
|
+
await done;
|
|
266
|
+
const errors = await page.evaluate(() => window.__lbx?.health?.errors ?? []).catch(() => []);
|
|
267
|
+
const tasks = mainThreadTasks(trace, sampleTimes(profile).at);
|
|
268
|
+
return { tasks: attribute(profile, tasks, { ours, name: nameOf }), note, errors };
|
|
269
|
+
} finally {
|
|
270
|
+
await ctx.close();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const browser = await launch();
|
|
275
|
+
const results = [];
|
|
276
|
+
const notes = [];
|
|
277
|
+
for (const s of scenarios) {
|
|
278
|
+
const runs = [];
|
|
279
|
+
for (let i = 0; i < opt.runs; i++) {
|
|
280
|
+
process.stderr.write(`${s.name}: run ${i + 1}/${opt.runs} `);
|
|
281
|
+
let r = await measure(browser, s);
|
|
282
|
+
if (r.fallback) {
|
|
283
|
+
const n = `The live storefront ${ORIGIN} answered with ${r.fallback}: not worked around; measured on the integration's stub page (luma.config.mjs stubPage) instead (run it with the client's knowledge, or ask them to allowlist the runner).`;
|
|
284
|
+
console.error(`\n${n}`);
|
|
285
|
+
notes.push(n);
|
|
286
|
+
stub = true;
|
|
287
|
+
r = await measure(browser, s);
|
|
288
|
+
}
|
|
289
|
+
if (r.note && !notes.includes(`${s.name}: ${r.note}`)) notes.push(`${s.name}: ${r.note}`);
|
|
290
|
+
for (const e of r.errors ?? []) if (!notes.includes(`${s.name}: health error ${e}`)) notes.push(`${s.name}: health error ${e}`);
|
|
291
|
+
runs.push(r.tasks ?? []);
|
|
292
|
+
}
|
|
293
|
+
results.push({ name: s.name, ...summarize(runs, opt.budget) });
|
|
294
|
+
}
|
|
295
|
+
await browser.close();
|
|
296
|
+
process.stderr.write('\n');
|
|
297
|
+
|
|
298
|
+
const meta = { integration: SITE_DIR, page: stub ? `${ORIGIN} (the integration's stub page)` : `${ORIGIN} (live)`, runs: String(opt.runs), build: opt.build ? 'yarn vite build --sourcemap (a temporary folder)' : 'dist/ as it is' };
|
|
299
|
+
console.log(formatReport(results, { budget: opt.budget, cpu: opt.cpu, meta }));
|
|
300
|
+
if (notes.length) console.log(`\nnotes:\n${notes.map((n) => ` ${n}`).join('\n')}`);
|
|
301
|
+
if (opt.json) {
|
|
302
|
+
writeFileSync(resolve(opt.json), JSON.stringify({ meta, budget: opt.budget, cpu: opt.cpu, results: results.map((r) => ({ ...r, worst: r.worst && { ...r.worst, fns: Object.fromEntries(r.worst.fns) } })), notes }, null, 1));
|
|
303
|
+
console.log(`JSON: ${resolve(opt.json)}`);
|
|
304
|
+
}
|
|
305
|
+
process.exit(results.some((r) => r.over) ? 1 : 0);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"types": ["vite/client"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"paths": { "@/*": ["./src/*"] }
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|