page-analyzer 1.1.1 → 1.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/README.md +33 -0
- package/index.js +42 -38
- package/package.json +2 -1
- package/page-extractor.js +228 -49
- package/result-viewer.html +214 -29
- package/scripts/build-result-viewer.js +214 -29
- package/test/smoke.test.js +242 -1
package/result-viewer.html
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
6
7
|
<title>Page Analyzer Result Viewer</title>
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
@@ -108,12 +109,28 @@
|
|
|
108
109
|
color: var(--missing);
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
.url-loader,
|
|
111
113
|
.file-loader {
|
|
112
114
|
display: grid;
|
|
113
115
|
gap: 8px;
|
|
114
116
|
margin-top: 8px;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
.url-loader label,
|
|
120
|
+
.file-loader span {
|
|
121
|
+
color: var(--muted);
|
|
122
|
+
font-size: 11px;
|
|
123
|
+
font-weight: 800;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.url-row {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.url-loader input,
|
|
117
134
|
.file-loader input {
|
|
118
135
|
width: 100%;
|
|
119
136
|
border: 1px solid var(--line);
|
|
@@ -123,6 +140,16 @@
|
|
|
123
140
|
font-size: 12px;
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
.url-loader button {
|
|
144
|
+
min-width: 68px;
|
|
145
|
+
border: 1px solid var(--ink);
|
|
146
|
+
background: var(--ink);
|
|
147
|
+
color: white;
|
|
148
|
+
padding: 0 12px;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
.metrics {
|
|
127
154
|
display: grid;
|
|
128
155
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -522,7 +549,14 @@
|
|
|
522
549
|
<h1>Block Review</h1>
|
|
523
550
|
<div class="title" id="page-title"></div>
|
|
524
551
|
<div class="load-state" id="load-state">Loading result.json...</div>
|
|
525
|
-
<
|
|
552
|
+
<form class="url-loader" id="url-loader">
|
|
553
|
+
<label for="result-url">Result JSON URL</label>
|
|
554
|
+
<div class="url-row">
|
|
555
|
+
<input id="result-url" type="text" inputmode="url" autocomplete="url" placeholder="https://example.com/result.json">
|
|
556
|
+
<button type="submit">Load</button>
|
|
557
|
+
</div>
|
|
558
|
+
</form>
|
|
559
|
+
<label class="file-loader" id="file-loader">
|
|
526
560
|
<span>Choose result.json manually</span>
|
|
527
561
|
<input id="result-file" type="file" accept="application/json,.json">
|
|
528
562
|
</label>
|
|
@@ -598,6 +632,9 @@
|
|
|
598
632
|
let selectedIndex = 0;
|
|
599
633
|
let activeFilter = 'all';
|
|
600
634
|
let query = '';
|
|
635
|
+
let resultSourceUrl = '';
|
|
636
|
+
|
|
637
|
+
const DEFAULT_RESULT_URL = './result.json';
|
|
601
638
|
|
|
602
639
|
const els = {
|
|
603
640
|
pageTitle: document.getElementById('page-title'),
|
|
@@ -614,13 +651,36 @@
|
|
|
614
651
|
fullPageLink: document.getElementById('full-page-link'),
|
|
615
652
|
loadState: document.getElementById('load-state'),
|
|
616
653
|
fileLoader: document.getElementById('file-loader'),
|
|
617
|
-
resultFile: document.getElementById('result-file')
|
|
654
|
+
resultFile: document.getElementById('result-file'),
|
|
655
|
+
urlLoader: document.getElementById('url-loader'),
|
|
656
|
+
resultUrl: document.getElementById('result-url')
|
|
618
657
|
};
|
|
619
658
|
|
|
659
|
+
function asArray(value) {
|
|
660
|
+
if (Array.isArray(value)) return value;
|
|
661
|
+
if (value === undefined || value === null || value === '') return [];
|
|
662
|
+
return [value];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isLikelyLocalAbsolutePath(value) {
|
|
666
|
+
return /^\/(Users|Volumes|Applications|System|Library|private|tmp|var|home)\//.test(value);
|
|
667
|
+
}
|
|
668
|
+
|
|
620
669
|
function pathToUrl(value) {
|
|
621
|
-
const text = String(value || '');
|
|
670
|
+
const text = String(value || '').trim();
|
|
622
671
|
if (!text) return '';
|
|
623
|
-
if (/^(https?:|file:|data:)/i.test(text)) return text;
|
|
672
|
+
if (/^(https?:|file:|data:|blob:)/i.test(text)) return text;
|
|
673
|
+
if (resultSourceUrl && /^(https?:|file:)/i.test(resultSourceUrl)) {
|
|
674
|
+
const shouldResolveFromSource = !text.startsWith('/') ||
|
|
675
|
+
(/^https?:/i.test(resultSourceUrl) && !isLikelyLocalAbsolutePath(text));
|
|
676
|
+
if (shouldResolveFromSource) {
|
|
677
|
+
try {
|
|
678
|
+
return new URL(text, resultSourceUrl).href;
|
|
679
|
+
} catch {
|
|
680
|
+
// Fall through to local snapshot handling.
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
624
684
|
const snapshotIndex = text.lastIndexOf('/snapshots/');
|
|
625
685
|
if (snapshotIndex >= 0) {
|
|
626
686
|
return encodeURI('./snapshots/' + text.slice(snapshotIndex + '/snapshots/'.length));
|
|
@@ -630,7 +690,7 @@
|
|
|
630
690
|
}
|
|
631
691
|
|
|
632
692
|
function getShot(block, index) {
|
|
633
|
-
const direct =
|
|
693
|
+
const direct = asArray(block.blockScreenshotPaths)[0] || block.blockScreenshotPath || block.screenshotPath || '';
|
|
634
694
|
if (direct) return { path: direct };
|
|
635
695
|
return screenshotByBlockIdx.get(index) || null;
|
|
636
696
|
}
|
|
@@ -651,8 +711,9 @@
|
|
|
651
711
|
block.blockDescription,
|
|
652
712
|
block.blockCssPath,
|
|
653
713
|
block.blockIdxs,
|
|
654
|
-
...(block.blockSemantics
|
|
655
|
-
...(block.blockPossibleEvents
|
|
714
|
+
...asArray(block.blockSemantics),
|
|
715
|
+
...asArray(block.blockPossibleEvents),
|
|
716
|
+
...asArray(block.blockSemanticGroups).map((item) => JSON.stringify(item))
|
|
656
717
|
].join(' ').toLowerCase();
|
|
657
718
|
return haystack.includes(query.toLowerCase());
|
|
658
719
|
}
|
|
@@ -670,11 +731,24 @@
|
|
|
670
731
|
.filter(({ block, index }) => textMatches(block, index) && isVisibleByFilter(block, index));
|
|
671
732
|
}
|
|
672
733
|
|
|
734
|
+
function screenshotCount() {
|
|
735
|
+
const paths = new Set();
|
|
736
|
+
for (const item of screenshotRows) {
|
|
737
|
+
if (item?.path) paths.add(item.path);
|
|
738
|
+
}
|
|
739
|
+
for (const block of blocks) {
|
|
740
|
+
for (const path of asArray(block.blockScreenshotPaths)) {
|
|
741
|
+
if (path) paths.add(path);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return paths.size;
|
|
745
|
+
}
|
|
746
|
+
|
|
673
747
|
function renderMetrics() {
|
|
674
748
|
const stats = data.analysis?.block_analysis?.stats || {};
|
|
675
749
|
const metrics = [
|
|
676
750
|
['Blocks', blocks.length],
|
|
677
|
-
['Screenshots',
|
|
751
|
+
['Screenshots', screenshotCount()],
|
|
678
752
|
['Elements', data.parseMetrics?.elementsCount || 0],
|
|
679
753
|
['Parse ms', data.parseMetrics?.parseMs || 0]
|
|
680
754
|
];
|
|
@@ -690,6 +764,10 @@
|
|
|
690
764
|
|
|
691
765
|
function renderList() {
|
|
692
766
|
const rows = visibleBlocks();
|
|
767
|
+
if (!rows.length) {
|
|
768
|
+
els.list.innerHTML = '<div class="load-state">No blocks match the current search or filter.</div>';
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
693
771
|
els.list.innerHTML = rows.map(({ block, index }) => {
|
|
694
772
|
const hasShot = Boolean(getShot(block, index));
|
|
695
773
|
const status = hasShot ? 'shot' : 'no shot';
|
|
@@ -707,7 +785,12 @@
|
|
|
707
785
|
}
|
|
708
786
|
|
|
709
787
|
function renderAllBlocks() {
|
|
710
|
-
|
|
788
|
+
const rows = visibleBlocks();
|
|
789
|
+
if (!rows.length) {
|
|
790
|
+
els.allBlocks.innerHTML = '<div class="missing-shot">No blocks to show.</div>';
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
els.allBlocks.innerHTML = rows.map(({ block, index }) => {
|
|
711
794
|
const shot = getShot(block, index);
|
|
712
795
|
const image = shot
|
|
713
796
|
? '<img src="' + pathToUrl(shot.path) + '" alt="Screenshot for block ' + index + '">'
|
|
@@ -728,6 +811,18 @@
|
|
|
728
811
|
}
|
|
729
812
|
|
|
730
813
|
function renderSelected() {
|
|
814
|
+
if (!blocks.length) {
|
|
815
|
+
els.selectedTitle.textContent = 'No blocks found';
|
|
816
|
+
els.selectedDescription.textContent = 'Loaded JSON does not contain block analysis rows.';
|
|
817
|
+
els.copySelector.disabled = true;
|
|
818
|
+
els.fullPageLink.href = pathToUrl(data.screenshots?.fullPage || '');
|
|
819
|
+
els.fullPageLink.style.display = data.screenshots?.fullPage ? 'inline-flex' : 'none';
|
|
820
|
+
els.screenshot.innerHTML = '<div class="missing-shot">Load a Page Analyzer result with analysis.block_analysis.blocks.</div>';
|
|
821
|
+
els.info.innerHTML = '';
|
|
822
|
+
els.raw.textContent = JSON.stringify(data, null, 2);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!blocks[selectedIndex]) selectedIndex = 0;
|
|
731
826
|
const block = blocks[selectedIndex] || {};
|
|
732
827
|
const shot = getShot(block, selectedIndex);
|
|
733
828
|
els.selectedTitle.textContent = '#' + selectedIndex + ' ' + (block.blockName || 'Unnamed block');
|
|
@@ -745,9 +840,9 @@
|
|
|
745
840
|
'<div class="missing-shot">No screenshot was generated for this block.<br>Most likely the selector was empty, hidden, or not screenshotable.</div>';
|
|
746
841
|
}
|
|
747
842
|
|
|
748
|
-
const semantics = block.blockSemantics
|
|
749
|
-
const events = block.blockPossibleEvents
|
|
750
|
-
const groups = block.blockSemanticGroups
|
|
843
|
+
const semantics = asArray(block.blockSemantics);
|
|
844
|
+
const events = asArray(block.blockPossibleEvents);
|
|
845
|
+
const groups = asArray(block.blockSemanticGroups);
|
|
751
846
|
els.info.innerHTML = [
|
|
752
847
|
info('Block name', block.blockName || 'n/a'),
|
|
753
848
|
info('Block idxs', block.blockIdxs || String(selectedIndex)),
|
|
@@ -823,44 +918,132 @@
|
|
|
823
918
|
els.loadState.classList.toggle('error', isError);
|
|
824
919
|
}
|
|
825
920
|
|
|
826
|
-
function
|
|
921
|
+
function getBlocks(nextData) {
|
|
922
|
+
const candidates = [
|
|
923
|
+
nextData?.analysis?.block_analysis?.blocks,
|
|
924
|
+
nextData?.block_analysis?.blocks,
|
|
925
|
+
nextData?.analysis?.blocks,
|
|
926
|
+
nextData?.blocks
|
|
927
|
+
];
|
|
928
|
+
return candidates.find(Array.isArray) || [];
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function getScreenshotRows(nextData) {
|
|
932
|
+
const rows = nextData?.screenshots?.blocks || nextData?.blockScreenshots || [];
|
|
933
|
+
if (Array.isArray(rows)) {
|
|
934
|
+
return rows
|
|
935
|
+
.map((item, index) => typeof item === 'string' ? { blockIdx: index, path: item } : item)
|
|
936
|
+
.filter((item) => item && item.path);
|
|
937
|
+
}
|
|
938
|
+
if (rows && typeof rows === 'object') {
|
|
939
|
+
return Object.entries(rows).map(([blockIdx, path]) => ({ blockIdx, path }));
|
|
940
|
+
}
|
|
941
|
+
return [];
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function buildScreenshotMap(rows) {
|
|
945
|
+
return new Map(rows.map((item, index) => [
|
|
946
|
+
Number(item.blockIdx ?? item.blockIndex ?? item.index ?? index),
|
|
947
|
+
item
|
|
948
|
+
]));
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function sourceLabelForUrl(value) {
|
|
952
|
+
try {
|
|
953
|
+
const url = new URL(value, window.location.href);
|
|
954
|
+
return url.pathname.split('/').filter(Boolean).pop() || url.host || value;
|
|
955
|
+
} catch {
|
|
956
|
+
return value;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function normalizeResultUrl(value) {
|
|
961
|
+
const text = String(value || '').trim();
|
|
962
|
+
if (!text) {
|
|
963
|
+
throw new Error('Result JSON URL is empty');
|
|
964
|
+
}
|
|
965
|
+
return new URL(text, window.location.href).href;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function getInitialResultUrl() {
|
|
969
|
+
const params = new URLSearchParams(window.location.search);
|
|
970
|
+
return params.get('result') || params.get('url') || '';
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function rememberResultUrl(value) {
|
|
974
|
+
const url = new URL(window.location.href);
|
|
975
|
+
url.searchParams.set('result', value);
|
|
976
|
+
url.searchParams.delete('url');
|
|
977
|
+
window.history.replaceState(null, '', url);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function forgetResultUrl() {
|
|
981
|
+
const url = new URL(window.location.href);
|
|
982
|
+
url.searchParams.delete('result');
|
|
983
|
+
url.searchParams.delete('url');
|
|
984
|
+
window.history.replaceState(null, '', url);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function renderLoadError(error, title = 'result.json not loaded') {
|
|
988
|
+
if (data) return;
|
|
989
|
+
els.selectedTitle.textContent = title;
|
|
990
|
+
els.selectedDescription.textContent = error.message || 'Unknown load error';
|
|
991
|
+
els.screenshot.innerHTML = '<div class="missing-shot">Load result.json to inspect blocks.</div>';
|
|
992
|
+
els.info.innerHTML = '';
|
|
993
|
+
els.raw.textContent = '';
|
|
994
|
+
els.allBlocks.innerHTML = '';
|
|
995
|
+
els.list.innerHTML = '';
|
|
996
|
+
els.metrics.innerHTML = '';
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function initialize(nextData, sourceLabel, sourceUrl = '') {
|
|
827
1000
|
data = nextData || {};
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1001
|
+
resultSourceUrl = sourceUrl ? normalizeResultUrl(sourceUrl) : '';
|
|
1002
|
+
blocks = getBlocks(data);
|
|
1003
|
+
screenshotRows = getScreenshotRows(data);
|
|
1004
|
+
screenshotByBlockIdx = buildScreenshotMap(screenshotRows);
|
|
831
1005
|
selectedIndex = 0;
|
|
832
1006
|
query = '';
|
|
833
1007
|
els.search.value = '';
|
|
834
1008
|
els.pageTitle.textContent = data.title || 'Untitled page';
|
|
835
|
-
setLoadState(sourceLabel + ' loaded. ' + blocks.length + ' blocks, ' +
|
|
836
|
-
els.fileLoader.hidden = true;
|
|
1009
|
+
setLoadState(sourceLabel + ' loaded. ' + blocks.length + ' blocks, ' + screenshotCount() + ' screenshots.');
|
|
837
1010
|
renderMetrics();
|
|
838
1011
|
renderList();
|
|
839
1012
|
renderAllBlocks();
|
|
840
1013
|
renderSelected();
|
|
841
1014
|
}
|
|
842
1015
|
|
|
843
|
-
async function
|
|
1016
|
+
async function loadResultFromUrl(rawUrl, updateAddress = false) {
|
|
1017
|
+
const resolvedUrl = normalizeResultUrl(rawUrl);
|
|
1018
|
+
els.resultUrl.value = rawUrl;
|
|
1019
|
+
setLoadState('Loading ' + rawUrl + '...');
|
|
844
1020
|
try {
|
|
845
|
-
const response = await fetch(
|
|
1021
|
+
const response = await fetch(resolvedUrl, { cache: 'no-store' });
|
|
846
1022
|
if (!response.ok) {
|
|
847
|
-
throw new Error('HTTP ' + response.status + ' while loading result
|
|
1023
|
+
throw new Error('HTTP ' + response.status + ' while loading result JSON');
|
|
848
1024
|
}
|
|
849
|
-
initialize(await response.json(),
|
|
1025
|
+
initialize(await response.json(), sourceLabelForUrl(rawUrl), resolvedUrl);
|
|
1026
|
+
if (updateAddress) rememberResultUrl(rawUrl);
|
|
850
1027
|
} catch (error) {
|
|
851
1028
|
setLoadState(
|
|
852
|
-
'Could not load
|
|
1029
|
+
'Could not load ' + rawUrl + '. ' + (error.message || 'Unknown load error') + '. Remote URLs must allow browser CORS access.',
|
|
853
1030
|
true
|
|
854
1031
|
);
|
|
855
|
-
|
|
856
|
-
els.selectedTitle.textContent = 'result.json not loaded';
|
|
857
|
-
els.selectedDescription.textContent = error.message || 'Unknown load error';
|
|
858
|
-
els.screenshot.innerHTML = '<div class="missing-shot">Load result.json to inspect blocks.</div>';
|
|
859
|
-
els.info.innerHTML = '';
|
|
860
|
-
els.raw.textContent = '';
|
|
1032
|
+
renderLoadError(error);
|
|
861
1033
|
}
|
|
862
1034
|
}
|
|
863
1035
|
|
|
1036
|
+
async function loadResultJson() {
|
|
1037
|
+
const initialUrl = getInitialResultUrl() || DEFAULT_RESULT_URL;
|
|
1038
|
+
els.resultUrl.value = initialUrl;
|
|
1039
|
+
await loadResultFromUrl(initialUrl);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
els.urlLoader.addEventListener('submit', async (event) => {
|
|
1043
|
+
event.preventDefault();
|
|
1044
|
+
await loadResultFromUrl(els.resultUrl.value, true);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
864
1047
|
els.resultFile.addEventListener('change', async () => {
|
|
865
1048
|
const file = els.resultFile.files?.[0];
|
|
866
1049
|
if (!file) {
|
|
@@ -868,6 +1051,8 @@
|
|
|
868
1051
|
}
|
|
869
1052
|
try {
|
|
870
1053
|
initialize(JSON.parse(await file.text()), file.name);
|
|
1054
|
+
els.resultUrl.value = '';
|
|
1055
|
+
forgetResultUrl();
|
|
871
1056
|
} catch (error) {
|
|
872
1057
|
setLoadState('Could not parse selected JSON: ' + error.message, true);
|
|
873
1058
|
}
|