Skip to content

Commit 2efa723

Browse files
committed
chore(visual-testing): improvements to report
1 parent 4d1d1f2 commit 2efa723

2 files changed

Lines changed: 217 additions & 0 deletions

File tree

devtools/visual-testing/scripts/compare.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ export interface InteractionMetadata {
168168
export interface SourceDocMetadata {
169169
/** Corpus-relative path of the source document (e.g. "tables/basic.docx") */
170170
relativePath: string;
171+
/** Absolute local path to the original source document (corpus location/cache file) */
172+
originalLocalPath: string;
171173
/** Absolute local path to the (possibly staged) copy of the document */
172174
localPath: string;
173175
/** ms-word: protocol deep-link URL (macOS only) */
@@ -966,6 +968,7 @@ async function augmentReportWithSourceDocs(
966968

967969
sourceDocByKey.set(docKey, {
968970
relativePath: docInfo.relativePath,
971+
originalLocalPath: localPath,
969972
localPath: openPath,
970973
wordUrl: toWordDeepLink(openPath),
971974
});

devtools/visual-testing/scripts/report.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,37 @@ export function writeHtmlReport(
476476
border-color: rgba(82, 212, 166, 0.6);
477477
}
478478
479+
.reject-btn {
480+
border: 1px solid rgba(251, 146, 60, 0.55);
481+
background: rgba(251, 146, 60, 0.15);
482+
color: #9a3412;
483+
padding: 4px 10px;
484+
border-radius: 999px;
485+
font-size: 11px;
486+
font-weight: 600;
487+
cursor: pointer;
488+
text-transform: uppercase;
489+
letter-spacing: 0.5px;
490+
transition: all 0.15s ease;
491+
}
492+
493+
.reject-btn:hover {
494+
background: rgba(251, 146, 60, 0.28);
495+
border-color: rgba(251, 146, 60, 0.75);
496+
}
497+
498+
details.group.rejected .reject-btn {
499+
background: rgba(251, 146, 60, 0.32);
500+
border-color: rgba(194, 65, 12, 0.7);
501+
color: #7c2d12;
502+
}
503+
504+
details.group.rejected {
505+
border-color: rgba(251, 146, 60, 0.5);
506+
}
507+
479508
.word-btn,
509+
.copy-path-btn,
480510
.word-overlay-btn {
481511
border: 1px solid rgba(15, 25, 45, 0.18);
482512
background: rgba(15, 25, 45, 0.06);
@@ -491,6 +521,7 @@ export function writeHtmlReport(
491521
}
492522
493523
.word-btn:hover:not(:disabled),
524+
.copy-path-btn:hover:not(:disabled),
494525
.word-overlay-btn:hover:not(:disabled) {
495526
background: rgba(15, 25, 45, 0.12);
496527
border-color: rgba(15, 25, 45, 0.28);
@@ -787,6 +818,7 @@ export function writeHtmlReport(
787818
</div>
788819
<div class="actions">
789820
<button class="secondary" id="toggle-layout">Show side-by-side</button>
821+
<button class="secondary" id="export-rejected" hidden>Export rejected docs list</button>
790822
<button class="secondary" id="toggle-lens">Disable magnifier</button>
791823
<button class="secondary" id="toggle-unchanged">Hide unchanged</button>
792824
<button class="secondary" id="toggle-approved">Show approved</button>
@@ -811,6 +843,7 @@ export function writeHtmlReport(
811843
const zoomLens = document.getElementById('zoom-lens');
812844
const toggleLensButton = document.getElementById('toggle-lens');
813845
const toggleLayoutButton = document.getElementById('toggle-layout');
846+
const exportRejectedButton = document.getElementById('export-rejected');
814847
815848
const diffs = report.results.filter((item) => !item.passed);
816849
const showAll = ${JSON.stringify(showAll)};
@@ -819,6 +852,8 @@ export function writeHtmlReport(
819852
820853
// Approval state (session only, resets on refresh)
821854
const approved = new Set();
855+
const rejectedGroups = new Set();
856+
const buttonTimers = new WeakMap();
822857
823858
const resultsFolderName = (report.resultsFolder || '').replace(/\\\\/g, '/');
824859
const resultsPrefix = resultsFolderName ? resultsFolderName + '/' : '';
@@ -854,6 +889,79 @@ export function writeHtmlReport(
854889
return parsed;
855890
}
856891
892+
function setTemporaryButtonLabel(button, text, ms) {
893+
if (!(button instanceof HTMLButtonElement)) return;
894+
const defaultText = button.dataset.defaultText || button.textContent || '';
895+
const existingTimer = buttonTimers.get(button);
896+
if (existingTimer) {
897+
clearTimeout(existingTimer);
898+
}
899+
button.textContent = text;
900+
const timer = setTimeout(() => {
901+
button.textContent = defaultText;
902+
buttonTimers.delete(button);
903+
}, ms);
904+
buttonTimers.set(button, timer);
905+
}
906+
907+
/** @deprecated fallback for browsers without navigator.clipboard — remove when no longer needed */
908+
function copyTextWithFallback(value) {
909+
const textArea = document.createElement('textarea');
910+
textArea.value = value;
911+
textArea.setAttribute('readonly', 'true');
912+
textArea.style.position = 'fixed';
913+
textArea.style.opacity = '0';
914+
textArea.style.left = '-9999px';
915+
document.body.appendChild(textArea);
916+
textArea.focus();
917+
textArea.select();
918+
let copied = false;
919+
try {
920+
copied = typeof document.execCommand === 'function' && document.execCommand('copy');
921+
} catch (_error) {
922+
copied = false;
923+
}
924+
textArea.remove();
925+
return copied;
926+
}
927+
928+
async function copyToClipboard(value) {
929+
if (!value) return false;
930+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
931+
try {
932+
await navigator.clipboard.writeText(value);
933+
return true;
934+
} catch (_error) {
935+
return copyTextWithFallback(value);
936+
}
937+
}
938+
return copyTextWithFallback(value);
939+
}
940+
941+
function updateRejectedExportButton() {
942+
if (!exportRejectedButton) return;
943+
exportRejectedButton.hidden = rejectedGroups.size === 0;
944+
}
945+
946+
function exportRejectedDocsList() {
947+
if (rejectedGroups.size === 0) return;
948+
const rejectedDocs = Array.from(rejectedGroups)
949+
.sort((a, b) => a.localeCompare(b))
950+
.map((groupName) => (groupName === '.' ? '(root)' : groupName));
951+
const reportName = resultsFolderName || 'report';
952+
const fileName = reportName + '-rejected-docs.txt';
953+
const content = rejectedDocs.join('\\n') + '\\n';
954+
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
955+
const url = URL.createObjectURL(blob);
956+
const link = document.createElement('a');
957+
link.href = url;
958+
link.download = fileName;
959+
document.body.appendChild(link);
960+
link.click();
961+
link.remove();
962+
setTimeout(() => URL.revokeObjectURL(url), 0);
963+
}
964+
857965
function createWordOverlayController(sourceDoc) {
858966
const sourceLocalPath = typeof sourceDoc?.localPath === 'string' ? sourceDoc.localPath : '';
859967
const sourceRelativePath = typeof sourceDoc?.relativePath === 'string' ? sourceDoc.relativePath : '';
@@ -1360,6 +1468,7 @@ export function writeHtmlReport(
13601468
groupEntries.forEach(([dir, items]) => {
13611469
const details = document.createElement('details');
13621470
details.className = 'group';
1471+
details.dataset.group = dir;
13631472
details.open = false;
13641473
13651474
const summary = document.createElement('summary');
@@ -1413,6 +1522,22 @@ export function writeHtmlReport(
14131522
14141523
actionWrap.appendChild(wordBtn);
14151524
1525+
const copyPathBtn = document.createElement('button');
1526+
copyPathBtn.type = 'button';
1527+
copyPathBtn.className = 'copy-path-btn';
1528+
copyPathBtn.textContent = 'Copy doc path';
1529+
copyPathBtn.dataset.defaultText = 'Copy doc path';
1530+
const copyDocPath =
1531+
(sourceDoc && sourceDoc.originalLocalPath) || (sourceDoc && sourceDoc.localPath) || '';
1532+
if (copyDocPath) {
1533+
copyPathBtn.dataset.docPath = copyDocPath;
1534+
copyPathBtn.title = 'Copy full local path to clipboard';
1535+
} else {
1536+
copyPathBtn.disabled = true;
1537+
copyPathBtn.title = 'Doc path not available locally.';
1538+
}
1539+
actionWrap.appendChild(copyPathBtn);
1540+
14161541
wordOverlayController = createWordOverlayController(sourceDoc);
14171542
actionWrap.appendChild(wordOverlayController.button);
14181543
}
@@ -1424,6 +1549,13 @@ export function writeHtmlReport(
14241549
approveBtn.dataset.group = dir;
14251550
actionWrap.appendChild(approveBtn);
14261551
1552+
const rejectBtn = document.createElement('button');
1553+
rejectBtn.type = 'button';
1554+
rejectBtn.className = 'reject-btn';
1555+
rejectBtn.textContent = 'Reject doc';
1556+
rejectBtn.dataset.groupReject = dir;
1557+
actionWrap.appendChild(rejectBtn);
1558+
14271559
summary.appendChild(titleWrap);
14281560
summary.appendChild(count);
14291561
summary.appendChild(actionWrap);
@@ -1758,6 +1890,19 @@ export function writeHtmlReport(
17581890
}
17591891
17601892
groupsContainer.addEventListener('click', (event) => {
1893+
const copyPathBtn = event.target.closest('.copy-path-btn');
1894+
if (copyPathBtn) {
1895+
event.preventDefault();
1896+
event.stopPropagation();
1897+
if (copyPathBtn.disabled) return;
1898+
const docPath = copyPathBtn.dataset.docPath;
1899+
if (!docPath) return;
1900+
copyToClipboard(docPath).then((didCopy) => {
1901+
setTemporaryButtonLabel(copyPathBtn, didCopy ? 'Copied' : 'Copy failed', didCopy ? 1300 : 1800);
1902+
});
1903+
return;
1904+
}
1905+
17611906
const openWordBtn = event.target.closest('.word-btn');
17621907
if (openWordBtn) {
17631908
event.preventDefault();
@@ -1770,6 +1915,54 @@ export function writeHtmlReport(
17701915
return;
17711916
}
17721917
1918+
const rejectBtn = event.target.closest('.reject-btn');
1919+
if (rejectBtn) {
1920+
event.preventDefault();
1921+
event.stopPropagation();
1922+
1923+
const group = rejectBtn.closest('details.group');
1924+
if (!group) return;
1925+
const groupName = rejectBtn.dataset.groupReject;
1926+
if (!groupName) return;
1927+
const groupKey = 'group:' + groupName;
1928+
const isRejected = group.classList.contains('rejected');
1929+
const approveBtn = group.querySelector('.approve-btn[data-group]');
1930+
1931+
if (isRejected) {
1932+
group.classList.remove('rejected');
1933+
rejectBtn.textContent = 'Reject doc';
1934+
rejectedGroups.delete(groupName);
1935+
} else {
1936+
const cards = group.querySelectorAll('.diff-card');
1937+
cards.forEach((card) => {
1938+
if (!card.classList.contains('approved')) return;
1939+
card.classList.remove('approved');
1940+
const cardBtn = card.querySelector('.approve-btn[data-card]');
1941+
if (cardBtn) {
1942+
cardBtn.textContent = 'Approve';
1943+
}
1944+
const cardKey = cardBtn ? 'card:' + cardBtn.dataset.card : '';
1945+
if (cardKey) {
1946+
approved.delete(cardKey);
1947+
}
1948+
});
1949+
1950+
group.classList.remove('approved');
1951+
if (approveBtn) {
1952+
approveBtn.textContent = 'Approve doc';
1953+
}
1954+
approved.delete(groupKey);
1955+
1956+
group.classList.add('rejected');
1957+
rejectBtn.textContent = 'Unreject doc';
1958+
rejectedGroups.add(groupName);
1959+
}
1960+
1961+
updateRejectedExportButton();
1962+
updateGroupCounts();
1963+
return;
1964+
}
1965+
17731966
const btn = event.target.closest('.approve-btn');
17741967
if (!btn) return;
17751968
@@ -1783,6 +1976,7 @@ export function writeHtmlReport(
17831976
17841977
const groupKey = 'group:' + btn.dataset.group;
17851978
const isApproved = group.classList.contains('approved');
1979+
const rejectBtn = group.querySelector('.reject-btn[data-group-reject]');
17861980
17871981
// Approve/unapprove all cards inside this group
17881982
const cards = group.querySelectorAll('.diff-card');
@@ -1805,6 +1999,17 @@ export function writeHtmlReport(
18051999
btn.textContent = 'Approve doc';
18062000
approved.delete(groupKey);
18072001
} else {
2002+
if (group.classList.contains('rejected')) {
2003+
group.classList.remove('rejected');
2004+
const groupName = btn.dataset.group;
2005+
if (groupName) {
2006+
rejectedGroups.delete(groupName);
2007+
}
2008+
if (rejectBtn) {
2009+
rejectBtn.textContent = 'Reject doc';
2010+
}
2011+
updateRejectedExportButton();
2012+
}
18082013
group.classList.add('approved');
18092014
btn.textContent = 'Unapprove doc';
18102015
approved.add(groupKey);
@@ -1854,6 +2059,15 @@ export function writeHtmlReport(
18542059
});
18552060
}
18562061
2062+
if (exportRejectedButton) {
2063+
exportRejectedButton.dataset.defaultText = 'Export rejected docs list';
2064+
exportRejectedButton.addEventListener('click', () => {
2065+
exportRejectedDocsList();
2066+
setTemporaryButtonLabel(exportRejectedButton, 'Exported', 1400);
2067+
});
2068+
}
2069+
updateRejectedExportButton();
2070+
18572071
// Initial count update
18582072
updateGroupCounts();
18592073
</script>

0 commit comments

Comments
 (0)