1857 lines
89 KiB
JavaScript
1857 lines
89 KiB
JavaScript
import { chromium } from "playwright-core";
|
|
|
|
const executablePath =
|
|
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE ??
|
|
"/root/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome";
|
|
const baseUrl = process.env.PIXHELF_URL ?? "http://127.0.0.1:3002";
|
|
const requestedTarget = process.env.PIXHELF_VISUAL_TARGET;
|
|
const viewerReturnOnly = process.env.PIXHELF_VIEWER_RETURN_ONLY === "1";
|
|
|
|
const browser = await chromium.launch({
|
|
executablePath,
|
|
headless: true,
|
|
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
});
|
|
|
|
const results = [];
|
|
|
|
try {
|
|
const targets = [
|
|
{ name: "desktop", width: 1440, height: 1000 },
|
|
{ name: "mobile", width: 390, height: 844 },
|
|
].filter((target) => !requestedTarget || target.name === requestedTarget);
|
|
for (const target of targets) {
|
|
const page = await browser.newPage({
|
|
viewport: { width: target.width, height: target.height },
|
|
deviceScaleFactor: target.name === "mobile" ? 3 : 1,
|
|
hasTouch: target.name === "mobile",
|
|
});
|
|
const browserErrors = [];
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") browserErrors.push(message.text());
|
|
});
|
|
page.on("pageerror", (error) => browserErrors.push(error.message));
|
|
|
|
await page.goto(baseUrl, { waitUntil: "domcontentloaded", timeout: 60_000 });
|
|
await page.waitForSelector("[data-image-id]", { timeout: 60_000 });
|
|
const minimumVisibleImages = target.name === "mobile" ? 4 : 8;
|
|
await page.waitForFunction((minimumVisible) => {
|
|
const visible = [...document.querySelectorAll("[data-image-id] img")].filter((image) => {
|
|
const rect = image.getBoundingClientRect();
|
|
return rect.bottom > 0 && rect.top < innerHeight;
|
|
});
|
|
return visible.length >= minimumVisible &&
|
|
visible.every((image) => image.complete && image.naturalWidth > 0);
|
|
}, minimumVisibleImages, { timeout: 60_000 });
|
|
await page.waitForTimeout(250);
|
|
await page.screenshot({ path: `/tmp/pixhelf-${target.name}.png` });
|
|
|
|
const layout = await page.evaluate(() => {
|
|
const path = document.querySelector(".desktop-sidebar .sidebar-gallery-path");
|
|
const count = document.querySelector(".desktop-sidebar .sidebar-gallery-count");
|
|
const search = document.querySelector(".topbar-search");
|
|
const topbar = document.querySelector(".topbar");
|
|
const sidebarStatus = document.querySelector(".desktop-sidebar .sidebar-status");
|
|
const sidebarMeta = document.querySelector(".desktop-sidebar .sidebar-gallery-meta");
|
|
const pathRect = path?.getBoundingClientRect();
|
|
const countRect = count?.getBoundingClientRect();
|
|
const searchRect = search?.getBoundingClientRect();
|
|
const exploreRect = document.querySelector(".explore-toggle")?.getBoundingClientRect();
|
|
const topbarRect = topbar?.getBoundingClientRect();
|
|
const statusRect = sidebarStatus?.getBoundingClientRect();
|
|
const metaRect = sidebarMeta?.getBoundingClientRect();
|
|
const galleryToggle = document.querySelector(".gallery-sidebar-toggle");
|
|
return {
|
|
viewport: [innerWidth, innerHeight],
|
|
bodyWidth: document.documentElement.scrollWidth,
|
|
cards: document.querySelectorAll("[data-image-id]").length,
|
|
cardTags: [...document.querySelectorAll("[data-image-id]")]
|
|
.map((element) => element.tagName),
|
|
masonryColumns: Number(
|
|
document.querySelector(".masonry")?.getAttribute("data-columns") ?? 0,
|
|
),
|
|
loadedCards: document.querySelectorAll("[data-image-id] img.loaded").length,
|
|
brokenVisibleImages: [...document.images].filter((image) => {
|
|
const rect = image.getBoundingClientRect();
|
|
return rect.bottom > 0 && rect.top < innerHeight && image.naturalWidth === 0;
|
|
}).length,
|
|
horizontalOverflow: document.documentElement.scrollWidth > innerWidth + 1,
|
|
dialogs: document.querySelectorAll('[role="dialog"]').length,
|
|
topbarPosition: topbar ? getComputedStyle(topbar).position : "",
|
|
topbarTop: topbarRect?.top ?? -1,
|
|
topbarSearches: document.querySelectorAll(
|
|
".topbar-actions > .topbar-search",
|
|
).length,
|
|
brandHomeLinks: document.querySelectorAll('a.brand[href="/"]').length,
|
|
gallerySidebarToggles: document.querySelectorAll(".gallery-sidebar-toggle").length,
|
|
galleryToggleOpacity: galleryToggle
|
|
? Number.parseFloat(getComputedStyle(galleryToggle).opacity)
|
|
: -1,
|
|
topbarSidebarToggles: document.querySelectorAll(
|
|
".topbar .sidebar-toggle-button",
|
|
).length,
|
|
contentSearches: document.querySelectorAll(".content .search-field").length,
|
|
topbarSearchRightGap: searchRect ? innerWidth - searchRect.right : -1,
|
|
exploreButtons: document.querySelectorAll(".topbar-actions > .explore-toggle").length,
|
|
exploreSearchGap: exploreRect && searchRect ? searchRect.left - exploreRect.right : -1,
|
|
sidebarStatuses: document.querySelectorAll(
|
|
".desktop-sidebar > .sidebar-status",
|
|
).length,
|
|
sidebarGalleryMetas: document.querySelectorAll(".sidebar-gallery-meta").length,
|
|
contentInfoRows: document.querySelectorAll(
|
|
".content-heading, .content .sidebar-gallery-meta",
|
|
).length,
|
|
sortControls: document.querySelectorAll(".sort-control").length,
|
|
cardNameDisplay: getComputedStyle(document.querySelector(".image-name")).display,
|
|
cardNameOpacity: Number.parseFloat(
|
|
getComputedStyle(document.querySelector(".image-name")).opacity,
|
|
),
|
|
cardNameWhiteSpace: getComputedStyle(
|
|
document.querySelector(".image-name"),
|
|
).whiteSpace,
|
|
sidebarMetaBottomGap: metaRect ? innerHeight - metaRect.bottom : -1,
|
|
sidebarMetaStatusGap: statusRect && metaRect ? metaRect.top - statusRect.bottom : -1,
|
|
galleryPath: path?.textContent ?? "",
|
|
galleryCount: count?.textContent ?? "",
|
|
galleryMetaRowDelta: pathRect && countRect
|
|
? Math.abs(pathRect.top - countRect.top)
|
|
: -1,
|
|
};
|
|
});
|
|
|
|
let resizeStability = null;
|
|
if (target.name === "desktop") {
|
|
await page.evaluate(() => {
|
|
globalThis.__pixhelfResizeLoadedIds = [
|
|
...document.querySelectorAll('[data-image-id][data-loaded="true"]'),
|
|
].map((card) => card.getAttribute("data-image-id"));
|
|
for (const card of document.querySelectorAll("[data-image-id]")) {
|
|
card.setAttribute(
|
|
"data-resize-probe",
|
|
card.getAttribute("data-image-id") ?? "",
|
|
);
|
|
}
|
|
globalThis.__pixhelfResizeSkeletonSeen = false;
|
|
globalThis.__pixhelfResizeObserver = new MutationObserver(() => {
|
|
if (document.querySelector(".skeleton-grid")) {
|
|
globalThis.__pixhelfResizeSkeletonSeen = true;
|
|
}
|
|
});
|
|
globalThis.__pixhelfResizeObserver.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
globalThis.__pixhelfImageRequestsBeforeResize = performance
|
|
.getEntriesByType("resource")
|
|
.filter((entry) => new URL(entry.name).pathname === "/api/images")
|
|
.length;
|
|
});
|
|
|
|
const resizeAnchor = await page.evaluate(() => {
|
|
const cards = [...document.querySelectorAll(".masonry .image-card")];
|
|
const card = cards[Math.min(24, cards.length - 1)];
|
|
if (!(card instanceof HTMLElement)) return null;
|
|
card.scrollIntoView({ block: "center", behavior: "instant" });
|
|
card.focus({ preventScroll: true });
|
|
const rect = card.getBoundingClientRect();
|
|
const viewportTop = visualViewport?.offsetTop ?? 0;
|
|
const viewportHeight = visualViewport?.height ?? innerHeight;
|
|
const viewportBottom = viewportTop + viewportHeight;
|
|
const topbarBottom = document.querySelector(".topbar")?.getBoundingClientRect().bottom
|
|
?? viewportTop;
|
|
const safeTop = Math.min(viewportBottom, Math.max(viewportTop, topbarBottom + 8));
|
|
const safeBottom = Math.max(safeTop, viewportBottom - 8);
|
|
const visibleTop = Math.max(safeTop, rect.top);
|
|
const visibleBottom = Math.min(safeBottom, rect.bottom);
|
|
const anchorY = (visibleTop + visibleBottom) / 2;
|
|
return {
|
|
imageId: card.dataset.imageId ?? "",
|
|
cardRatio: (anchorY - rect.top) / Math.max(1, rect.height),
|
|
viewportRatio: (anchorY - viewportTop) / Math.max(1, viewportHeight),
|
|
};
|
|
});
|
|
if (!resizeAnchor) throw new Error("masonry resize anchor could not be captured");
|
|
const resizeAnchorDeltas = [];
|
|
|
|
for (const size of [
|
|
{ width: 680, columns: 3 },
|
|
{ width: 520, columns: 2 },
|
|
{ width: 1000, columns: 3 },
|
|
{ width: target.width, columns: 5 },
|
|
]) {
|
|
await page.setViewportSize({ width: size.width, height: target.height });
|
|
await page.waitForFunction((columns) =>
|
|
document.querySelector(".masonry")?.getAttribute("data-columns") === String(columns)
|
|
, size.columns);
|
|
try {
|
|
await page.waitForFunction((anchor) => {
|
|
const card = document.querySelector(
|
|
`.masonry .image-card[data-image-id="${CSS.escape(anchor.imageId)}"]`,
|
|
);
|
|
if (!(card instanceof HTMLElement)) return false;
|
|
const rect = card.getBoundingClientRect();
|
|
const viewportTop = visualViewport?.offsetTop ?? 0;
|
|
const viewportHeight = visualViewport?.height ?? innerHeight;
|
|
const viewportBottom = viewportTop + viewportHeight;
|
|
const topbarBottom = document.querySelector(".topbar")?.getBoundingClientRect().bottom
|
|
?? viewportTop;
|
|
const safeTop = Math.min(viewportBottom, Math.max(viewportTop, topbarBottom + 8));
|
|
const safeBottom = Math.max(safeTop, viewportBottom - 8);
|
|
const expectedY = Math.min(
|
|
safeBottom,
|
|
Math.max(safeTop, viewportTop + anchor.viewportRatio * viewportHeight),
|
|
);
|
|
const actualY = rect.top + rect.height * anchor.cardRatio;
|
|
return Math.abs(actualY - expectedY) <= 2;
|
|
}, resizeAnchor, { timeout: 4000 });
|
|
} catch (error) {
|
|
const actual = await page.evaluate((anchor) => {
|
|
const card = document.querySelector(
|
|
`.masonry .image-card[data-image-id="${CSS.escape(anchor.imageId)}"]`,
|
|
);
|
|
if (!(card instanceof HTMLElement)) return { missing: true };
|
|
const rect = card.getBoundingClientRect();
|
|
const viewportTop = visualViewport?.offsetTop ?? 0;
|
|
const viewportHeight = visualViewport?.height ?? innerHeight;
|
|
const viewportBottom = viewportTop + viewportHeight;
|
|
const topbarBottom = document.querySelector(".topbar")?.getBoundingClientRect().bottom
|
|
?? viewportTop;
|
|
const safeTop = Math.min(viewportBottom, Math.max(viewportTop, topbarBottom + 8));
|
|
const safeBottom = Math.max(safeTop, viewportBottom - 8);
|
|
const expectedY = Math.min(
|
|
safeBottom,
|
|
Math.max(safeTop, viewportTop + anchor.viewportRatio * viewportHeight),
|
|
);
|
|
const actualY = rect.top + rect.height * anchor.cardRatio;
|
|
return {
|
|
actualY,
|
|
columns: document.querySelector(".masonry")?.getAttribute("data-columns"),
|
|
delta: Math.abs(actualY - expectedY),
|
|
expectedY,
|
|
scrollY,
|
|
};
|
|
}, resizeAnchor);
|
|
throw new Error(
|
|
`masonry resize anchor drifted at ${size.width}px: ${JSON.stringify(actual)}`,
|
|
{ cause: error },
|
|
);
|
|
}
|
|
resizeAnchorDeltas.push(await page.evaluate((anchor) => {
|
|
const card = document.querySelector(
|
|
`.masonry .image-card[data-image-id="${CSS.escape(anchor.imageId)}"]`,
|
|
);
|
|
if (!(card instanceof HTMLElement)) return Infinity;
|
|
const rect = card.getBoundingClientRect();
|
|
const viewportTop = visualViewport?.offsetTop ?? 0;
|
|
const viewportHeight = visualViewport?.height ?? innerHeight;
|
|
const viewportBottom = viewportTop + viewportHeight;
|
|
const topbarBottom = document.querySelector(".topbar")?.getBoundingClientRect().bottom
|
|
?? viewportTop;
|
|
const safeTop = Math.min(viewportBottom, Math.max(viewportTop, topbarBottom + 8));
|
|
const safeBottom = Math.max(safeTop, viewportBottom - 8);
|
|
const expectedY = Math.min(
|
|
safeBottom,
|
|
Math.max(safeTop, viewportTop + anchor.viewportRatio * viewportHeight),
|
|
);
|
|
return Math.abs(rect.top + rect.height * anchor.cardRatio - expectedY);
|
|
}, resizeAnchor));
|
|
}
|
|
|
|
await page.waitForFunction(() => {
|
|
const loadedIds = globalThis.__pixhelfResizeLoadedIds ?? [];
|
|
return loadedIds.every((id) => {
|
|
const card = document.querySelector(`[data-image-id="${CSS.escape(id)}"]`);
|
|
const image = card?.querySelector("img");
|
|
return card?.getAttribute("data-loaded") === "true"
|
|
&& image?.complete
|
|
&& image.naturalWidth > 0;
|
|
});
|
|
});
|
|
resizeStability = await page.evaluate(() => {
|
|
globalThis.__pixhelfResizeObserver?.disconnect();
|
|
const cards = [...document.querySelectorAll("[data-image-id]")];
|
|
const imageRequestsAfter = performance
|
|
.getEntriesByType("resource")
|
|
.filter((entry) => new URL(entry.name).pathname === "/api/images")
|
|
.length;
|
|
const visibleImages = cards
|
|
.map((card) => card.querySelector("img"))
|
|
.filter((image) => {
|
|
if (!image) return false;
|
|
const rect = image.getBoundingClientRect();
|
|
return rect.bottom > 0 && rect.top < innerHeight;
|
|
});
|
|
return {
|
|
cardsPreserved: cards.every((card) =>
|
|
card.getAttribute("data-resize-probe") === card.getAttribute("data-image-id")
|
|
),
|
|
loadedCardsPreserved: (globalThis.__pixhelfResizeLoadedIds ?? []).every((id) =>
|
|
document.querySelector(`[data-image-id="${CSS.escape(id)}"]`)
|
|
?.getAttribute("data-loaded") === "true"
|
|
),
|
|
skeletonSeen: globalThis.__pixhelfResizeSkeletonSeen,
|
|
imagePageRequests: imageRequestsAfter
|
|
- globalThis.__pixhelfImageRequestsBeforeResize,
|
|
brokenVisibleImages: visibleImages.filter((image) =>
|
|
!image.complete || image.naturalWidth <= 0
|
|
).length,
|
|
columns: Number(
|
|
document.querySelector(".masonry")?.getAttribute("data-columns") ?? 0,
|
|
),
|
|
};
|
|
});
|
|
Object.assign(resizeStability, {
|
|
anchorId: resizeAnchor.imageId,
|
|
anchorDeltas: resizeAnchorDeltas,
|
|
anchorFocused: await page.evaluate((imageId) =>
|
|
document.activeElement?.getAttribute("data-image-id") === imageId
|
|
, resizeAnchor.imageId),
|
|
});
|
|
}
|
|
|
|
const firstViewerTitle = await page.locator("[data-image-id]").first().getAttribute("title");
|
|
await page.locator("[data-image-id]").first().click();
|
|
await page.waitForSelector(".image-viewer");
|
|
const viewerChrome = await page.evaluate(() => ({
|
|
fullscreenButtons: document.querySelectorAll(".viewer-fullscreen").length,
|
|
detailsPages: document.querySelectorAll(".viewer-details-page").length,
|
|
scrollCues: document.querySelectorAll(".viewer-scroll-cue").length,
|
|
zoomControls: document.querySelectorAll(
|
|
".viewer-zoom-controls, .viewer-zoom-value",
|
|
).length,
|
|
scrollCueArrowOnly: (() => {
|
|
const cue = document.querySelector(".viewer-scroll-cue");
|
|
return cue?.textContent?.trim() === ""
|
|
&& cue.children.length === 1
|
|
&& cue.firstElementChild?.tagName === "svg"
|
|
&& cue.querySelectorAll("path").length === 2;
|
|
})(),
|
|
scrollCuePresentation: (() => {
|
|
const cue = document.querySelector(".viewer-scroll-cue");
|
|
const media = document.querySelector(".viewer-media");
|
|
if (!cue || !media) return null;
|
|
const style = getComputedStyle(cue);
|
|
const cueBounds = cue.getBoundingClientRect();
|
|
const mediaBounds = media.getBoundingClientRect();
|
|
return {
|
|
borderless: Number.parseFloat(style.borderTopWidth) === 0,
|
|
transparent: style.backgroundColor === "rgba(0, 0, 0, 0)",
|
|
width: cueBounds.width,
|
|
clearsMedia: cueBounds.top >= mediaBounds.bottom - 1,
|
|
};
|
|
})(),
|
|
headingModules: document.querySelectorAll(".viewer-heading").length,
|
|
unifiedControls: document.querySelector(".image-viewer")
|
|
?.getAttribute("data-control-system") === "unified",
|
|
drawerArtifacts: document.querySelectorAll(
|
|
".viewer-details-sheet, .viewer-details-scrim",
|
|
).length,
|
|
gestureHelp: document.querySelectorAll(".viewer-gesture-help").length,
|
|
loadingStatus: document.querySelectorAll(".viewer-load-status").length,
|
|
loadingCopyVisible: document.body.textContent?.includes("正在加载原图") ?? false,
|
|
}));
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-full-loaded") === "true"
|
|
, undefined, { timeout: 60_000 });
|
|
await page.waitForFunction(() => {
|
|
const originalPaths = performance.getEntriesByType("resource")
|
|
.map((entry) => new URL(entry.name).pathname)
|
|
.filter((path) => path.endsWith("/original"));
|
|
return new Set(originalPaths).size >= 2;
|
|
}, undefined, { timeout: 60_000 });
|
|
await page.waitForTimeout(260);
|
|
const dialogsAfterImageClick = await page.locator('[role="dialog"]').count();
|
|
await page.screenshot({ path: `/tmp/pixhelf-viewer-${target.name}.png` });
|
|
const viewer = await page.evaluate(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const thumbnail = document.querySelector(".viewer-thumbnail");
|
|
const original = document.querySelector(".viewer-original");
|
|
const nativeOriginal = document.querySelector(".viewer-native-original");
|
|
const mediaElement = document.querySelector(".viewer-media");
|
|
const media = mediaElement?.getBoundingClientRect();
|
|
const stage = document.querySelector(".viewer-stage")?.getBoundingClientRect();
|
|
const details = document.querySelector(".viewer-details-page")?.getBoundingClientRect();
|
|
const thumbnailStyle = thumbnail ? getComputedStyle(thumbnail) : null;
|
|
const originalStyle = original ? getComputedStyle(original) : null;
|
|
const resourcePaths = performance.getEntriesByType("resource")
|
|
.map((entry) => new URL(entry.name).pathname);
|
|
const originalSource = original instanceof HTMLImageElement
|
|
? original.currentSrc || original.src
|
|
: original?.getAttribute("data-original-url") ?? "";
|
|
return {
|
|
title: overlay?.getAttribute("data-image-name") ?? "",
|
|
fullLoaded: overlay?.getAttribute("data-full-loaded"),
|
|
displaySource: overlay?.getAttribute("data-display-source") ?? "",
|
|
nativeOriginalLoaded: overlay?.getAttribute("data-native-original-loaded") ?? "false",
|
|
nativeOriginalActive: overlay?.getAttribute("data-native-original-active") ?? "false",
|
|
nativeOriginalCount: document.querySelectorAll(".viewer-native-original").length,
|
|
nativeOriginalPath: nativeOriginal instanceof HTMLImageElement
|
|
? new URL(nativeOriginal.currentSrc || nativeOriginal.src, location.href).pathname
|
|
: "",
|
|
nativeOriginalPixels: nativeOriginal instanceof HTMLImageElement
|
|
? [nativeOriginal.naturalWidth, nativeOriginal.naturalHeight]
|
|
: [0, 0],
|
|
nativeOriginalVisible: nativeOriginal
|
|
? Number.parseFloat(getComputedStyle(nativeOriginal).opacity)
|
|
: 0,
|
|
mediaWillChange: mediaElement ? getComputedStyle(mediaElement).willChange : "",
|
|
originalPath: originalSource ? new URL(originalSource, location.href).pathname : "",
|
|
renderer: overlay?.getAttribute("data-renderer"),
|
|
sourceStrategy: overlay?.getAttribute("data-source-strategy") ?? "",
|
|
sourcePresentation: overlay?.getAttribute("data-source-presentation") ?? "",
|
|
thumbnailFilter: thumbnailStyle?.filter ?? "",
|
|
thumbnailTransform: thumbnailStyle?.transform ?? "",
|
|
thumbnailTransitionDelay: thumbnailStyle?.transitionDelay ?? "",
|
|
originalTransitionDuration: originalStyle?.transitionDuration ?? "",
|
|
gestureRenderer: overlay?.getAttribute("data-mobile-gesture-renderer") ?? "",
|
|
originalPolicy: overlay?.getAttribute("data-mobile-original-policy") ?? "",
|
|
scrollMode: overlay?.getAttribute("data-scroll-mode"),
|
|
renderedPixels: original instanceof HTMLCanvasElement
|
|
? [original.width, original.height]
|
|
: [original?.clientWidth ?? 0, original?.clientHeight ?? 0],
|
|
prefetchedOriginals: new Set(
|
|
resourcePaths.filter((path) => path.endsWith("/original")),
|
|
).size,
|
|
previewRequests: resourcePaths.filter((path) => path.endsWith("/preview")).length,
|
|
rootInert: document.querySelector("#root")?.inert ?? false,
|
|
closeFocused: document.activeElement?.classList.contains("viewer-close") ?? false,
|
|
mediaContained: media
|
|
? media.left >= -1 && media.top >= -1 && media.right <= innerWidth + 1 &&
|
|
media.bottom <= innerHeight + 1
|
|
: false,
|
|
continuousLayout: overlay && stage && details
|
|
? Math.abs(stage.top) < 1 && Math.abs(stage.height - innerHeight) < 1 &&
|
|
Math.abs(details.top - stage.bottom) < 1 &&
|
|
details.height >= innerHeight - 1 &&
|
|
overlay.scrollHeight >= innerHeight * 2 - 1
|
|
: false,
|
|
position: document.querySelector(".viewer-details-heading span")?.textContent ?? "",
|
|
};
|
|
});
|
|
Object.assign(viewer, viewerChrome);
|
|
const sideButtonStart = await page.evaluate(() => {
|
|
const activeId = document.querySelector(".image-viewer")?.getAttribute("data-image-id") ?? "";
|
|
const ids = [...document.querySelectorAll(".masonry .image-card")]
|
|
.map((card) => card.getAttribute("data-image-id") ?? "");
|
|
const index = ids.indexOf(activeId);
|
|
return {
|
|
activeId,
|
|
nextId: ids[index + 1] ?? "",
|
|
historyLength: history.length,
|
|
href: location.href,
|
|
};
|
|
});
|
|
const dispatchViewerSideButton = (button) => page.locator(".image-viewer").evaluate(
|
|
(viewerElement, sideButton) => {
|
|
const mask = sideButton === 3 ? 8 : 16;
|
|
const downPrevented = !viewerElement.dispatchEvent(new PointerEvent("pointerdown", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
button: sideButton,
|
|
buttons: mask,
|
|
pointerId: 71,
|
|
pointerType: "mouse",
|
|
}));
|
|
const upPrevented = !viewerElement.dispatchEvent(new PointerEvent("pointerup", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
button: sideButton,
|
|
buttons: 0,
|
|
pointerId: 71,
|
|
pointerType: "mouse",
|
|
}));
|
|
const auxiliaryPrevented = !viewerElement.dispatchEvent(new MouseEvent("auxclick", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
button: sideButton,
|
|
}));
|
|
return { downPrevented, upPrevented, auxiliaryPrevented };
|
|
},
|
|
button,
|
|
);
|
|
const sideForwardEvents = await dispatchViewerSideButton(4);
|
|
await page.waitForFunction((imageId) =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-image-id") === imageId
|
|
, sideButtonStart.nextId);
|
|
const sideBackEvents = await dispatchViewerSideButton(3);
|
|
await page.waitForFunction((imageId) =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-image-id") === imageId
|
|
, sideButtonStart.activeId);
|
|
await page.evaluate(() => new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve))
|
|
));
|
|
const sideButtonNavigation = await page.evaluate(({ start, forward, back }) => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
return {
|
|
forward,
|
|
back,
|
|
returnedId: overlay?.getAttribute("data-image-id") ?? "",
|
|
returnedSource: overlay?.getAttribute("data-display-source") ?? "",
|
|
returnedPresentation: overlay?.getAttribute("data-source-presentation") ?? "",
|
|
returnedLoaded: overlay?.getAttribute("data-full-loaded") ?? "false",
|
|
enabled: overlay?.getAttribute("data-mouse-side-navigation") === "true",
|
|
historyPreserved: history.length === start.historyLength && location.href === start.href,
|
|
};
|
|
}, { start: sideButtonStart, forward: sideForwardEvents, back: sideBackEvents });
|
|
if (
|
|
!sideButtonStart.nextId ||
|
|
sideButtonNavigation.returnedId !== sideButtonStart.activeId ||
|
|
sideButtonNavigation.returnedPresentation !== "direct" ||
|
|
sideButtonNavigation.returnedLoaded !== "true" ||
|
|
!["original", "viewport-bitmap"].includes(sideButtonNavigation.returnedSource) ||
|
|
!sideButtonNavigation.enabled ||
|
|
!sideButtonNavigation.historyPreserved ||
|
|
!Object.values(sideButtonNavigation.forward).every(Boolean) ||
|
|
!Object.values(sideButtonNavigation.back).every(Boolean)
|
|
) {
|
|
throw new Error(`viewer side-button navigation failed: ${JSON.stringify(sideButtonNavigation)}`);
|
|
}
|
|
Object.assign(viewer, { sideButtonNavigation });
|
|
if (target.name === "desktop") {
|
|
await page.locator(".viewer-next").click();
|
|
} else {
|
|
await page.keyboard.press("ArrowRight");
|
|
}
|
|
await page.waitForFunction((imageId) =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-image-id") === imageId
|
|
, sideButtonStart.nextId);
|
|
const switchPerformance = await page.evaluate(async (expectedId) => {
|
|
const start = performance.now();
|
|
window.dispatchEvent(new KeyboardEvent("keydown", {
|
|
key: "ArrowLeft",
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}));
|
|
const dispatchDuration = performance.now() - start;
|
|
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
const firstFrameDuration = performance.now() - start;
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const media = document.querySelector(".viewer-media");
|
|
return {
|
|
currentId: overlay?.getAttribute("data-image-id") ?? "",
|
|
dispatchDuration,
|
|
firstFrameDuration,
|
|
firstFrameSource: overlay?.getAttribute("data-display-source") ?? "",
|
|
focusedOnViewer: document.activeElement === overlay,
|
|
visibleControlFocusRings: document.querySelectorAll(
|
|
".image-viewer .viewer-control:focus-visible, "
|
|
+ ".image-viewer .viewer-nav:focus-visible, "
|
|
+ ".image-viewer .viewer-scroll-cue:focus-visible",
|
|
).length,
|
|
mediaAnimations: media?.getAnimations().length ?? -1,
|
|
expectedId,
|
|
};
|
|
}, sideButtonStart.activeId);
|
|
if (
|
|
switchPerformance.currentId !== switchPerformance.expectedId ||
|
|
switchPerformance.dispatchDuration > 80 ||
|
|
switchPerformance.firstFrameDuration > 100 ||
|
|
(target.name === "mobile"
|
|
? !["thumbnail", "viewport-bitmap"].includes(switchPerformance.firstFrameSource)
|
|
: switchPerformance.firstFrameSource !== "original") ||
|
|
!switchPerformance.focusedOnViewer ||
|
|
switchPerformance.visibleControlFocusRings !== 0 ||
|
|
switchPerformance.mediaAnimations !== 0
|
|
) {
|
|
throw new Error(`viewer switch responsiveness failed: ${JSON.stringify(switchPerformance)}`);
|
|
}
|
|
Object.assign(viewer, { switchPerformance });
|
|
const rapidSwitch = await page.evaluate(async () => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const ids = [...document.querySelectorAll(".masonry .image-card")]
|
|
.map((card) => card.getAttribute("data-image-id") ?? "");
|
|
const startId = overlay?.getAttribute("data-image-id") ?? "";
|
|
const startIndex = ids.indexOf(startId);
|
|
const steps = Math.min(6, Math.max(0, ids.length - startIndex - 1));
|
|
const expectedId = ids[startIndex + steps] ?? startId;
|
|
const previousMedia = document.querySelector(".viewer-media");
|
|
for (let index = 0; index < steps; index += 1) {
|
|
window.dispatchEvent(new KeyboardEvent("keydown", {
|
|
key: "ArrowRight",
|
|
bubbles: true,
|
|
cancelable: true,
|
|
}));
|
|
}
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
const currentOverlay = document.querySelector(".image-viewer");
|
|
const currentId = currentOverlay?.getAttribute("data-image-id") ?? "";
|
|
const thumbnail = document.querySelector(".viewer-thumbnail");
|
|
const original = document.querySelector(".viewer-original");
|
|
const thumbnailPath = thumbnail instanceof HTMLImageElement
|
|
? new URL(thumbnail.currentSrc || thumbnail.src, location.href).pathname
|
|
: "";
|
|
const originalSource = original instanceof HTMLImageElement
|
|
? original.currentSrc || original.src
|
|
: original?.getAttribute("data-original-url") ?? "";
|
|
const originalPath = originalSource
|
|
? new URL(originalSource, location.href).pathname
|
|
: "";
|
|
return {
|
|
steps,
|
|
startId,
|
|
expectedId,
|
|
currentId,
|
|
mediaReplaced: previousMedia !== document.querySelector(".viewer-media"),
|
|
mediaCount: document.querySelectorAll(".viewer-media").length,
|
|
thumbnailMatches: thumbnailPath.includes(`/api/images/${encodeURIComponent(currentId)}/thumbnail`),
|
|
originalMatches: originalPath.includes(`/api/images/${encodeURIComponent(currentId)}/original`),
|
|
immediateSource: currentOverlay?.getAttribute("data-display-source") ?? "",
|
|
mediaAnimations: document.querySelector(".viewer-media")?.getAnimations().length ?? -1,
|
|
};
|
|
});
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-display-source") !== "placeholder"
|
|
, undefined, { timeout: 10_000 });
|
|
rapidSwitch.readySource = await page.locator(".image-viewer").getAttribute("data-display-source");
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-full-loaded") === "true"
|
|
, undefined, { timeout: 60_000 });
|
|
rapidSwitch.upgradedSource = await page.locator(".image-viewer").getAttribute("data-display-source");
|
|
Object.assign(viewer, { rapidSwitch });
|
|
if (
|
|
rapidSwitch.steps !== 6 ||
|
|
rapidSwitch.currentId !== rapidSwitch.expectedId ||
|
|
!rapidSwitch.mediaReplaced ||
|
|
rapidSwitch.mediaCount !== 1 ||
|
|
!rapidSwitch.thumbnailMatches ||
|
|
!rapidSwitch.originalMatches ||
|
|
rapidSwitch.mediaAnimations !== 0 ||
|
|
rapidSwitch.readySource === "placeholder" ||
|
|
rapidSwitch.upgradedSource !== (
|
|
target.name === "mobile" ? "viewport-bitmap" : "original"
|
|
)
|
|
) {
|
|
throw new Error(`viewer rapid-switch pipeline failed: ${JSON.stringify(rapidSwitch)}`);
|
|
}
|
|
await page.locator(".viewer-fullscreen").click();
|
|
await page.waitForFunction(() =>
|
|
document.fullscreenElement?.classList.contains("image-viewer")
|
|
);
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".viewer-fullscreen")?.getAttribute("aria-label") === "退出全屏"
|
|
);
|
|
viewer.fullscreenEntered = true;
|
|
viewer.fullscreenLabel = await page.locator(".viewer-fullscreen").getAttribute("aria-label");
|
|
await page.locator(".viewer-fullscreen").click();
|
|
await page.waitForFunction(() => document.fullscreenElement === null);
|
|
viewer.fullscreenExited = true;
|
|
await page.keyboard.press("=");
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-zoomed") === "true"
|
|
);
|
|
viewer.zoomed = await page.locator(".image-viewer").getAttribute("data-zoomed");
|
|
await page.waitForTimeout(260);
|
|
viewer.zoomScale = await page.locator(".viewer-media").evaluate((media) =>
|
|
new DOMMatrix(getComputedStyle(media).transform).a
|
|
);
|
|
Object.assign(viewer, await page.locator(".image-viewer").evaluate((overlay) => ({
|
|
zoomKeptImagePage: overlay.scrollTop < 1 && overlay.dataset.page === "image",
|
|
zoomLockedPageScroll: getComputedStyle(overlay).overflowY === "hidden",
|
|
})));
|
|
if (target.name === "desktop") {
|
|
await page.mouse.move(target.width / 2, target.height / 2);
|
|
await page.mouse.wheel(0, 1_000);
|
|
await page.mouse.wheel(0, 1_000);
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-zoomed") === "false"
|
|
);
|
|
await page.mouse.wheel(0, 1_000);
|
|
await page.waitForTimeout(80);
|
|
viewer.zoomWheelHandoffLocked = await page.locator(".image-viewer").evaluate((overlay) =>
|
|
overlay.scrollTop < 1 && overlay.dataset.page === "image"
|
|
);
|
|
}
|
|
const keyboardStartName = await page.locator(".image-viewer").getAttribute("data-image-name");
|
|
await page.keyboard.press("ArrowRight");
|
|
await page.waitForFunction((title) =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-image-name") !== title
|
|
, keyboardStartName);
|
|
viewer.keyboardNavigation = await page.locator(".image-viewer").getAttribute("data-image-name");
|
|
const keyboardFocus = await page.evaluate(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
return {
|
|
parkedOnViewer: document.activeElement === overlay,
|
|
visibleControlFocusRings: document.querySelectorAll(
|
|
".image-viewer .viewer-control:focus-visible, "
|
|
+ ".image-viewer .viewer-nav:focus-visible, "
|
|
+ ".image-viewer .viewer-scroll-cue:focus-visible",
|
|
).length,
|
|
};
|
|
});
|
|
if (!keyboardFocus.parkedOnViewer || keyboardFocus.visibleControlFocusRings !== 0) {
|
|
throw new Error(`viewer keyboard focus was not parked: ${JSON.stringify(keyboardFocus)}`);
|
|
}
|
|
Object.assign(viewer, { keyboardFocus });
|
|
if (target.name === "desktop") {
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-zoomed") === "false"
|
|
);
|
|
await page.mouse.move(target.width / 2, target.height / 2);
|
|
await page.mouse.wheel(0, target.height * 1.15);
|
|
await page.waitForFunction(() =>
|
|
(document.querySelector(".image-viewer")?.scrollTop ?? 0) > innerHeight * 0.5
|
|
);
|
|
await page.waitForTimeout(320);
|
|
await page.screenshot({ path: "/tmp/pixhelf-viewer-details-desktop.png" });
|
|
Object.assign(viewer, await page.evaluate(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const stage = document.querySelector(".viewer-stage")?.getBoundingClientRect();
|
|
const details = document.querySelector(".viewer-details-page");
|
|
const detailsRect = details?.getBoundingClientRect();
|
|
return {
|
|
detailsReachedByWheel: (overlay?.scrollTop ?? 0) > innerHeight * 0.5,
|
|
pageAfterDetails: overlay?.getAttribute("data-page") ?? "",
|
|
detailsName: details?.getAttribute("data-details-image-name") ?? "",
|
|
detailsHeading: document.querySelector(".viewer-details-heading h2")?.textContent ?? "",
|
|
stageScrolledAway: stage ? stage.top < -innerHeight * 0.5 : false,
|
|
detailsVisible: detailsRect
|
|
? detailsRect.top < innerHeight * 0.5 && detailsRect.bottom > innerHeight * 0.5
|
|
: false,
|
|
drawerArtifactsAfterScroll: document.querySelectorAll(
|
|
".viewer-details-sheet, .viewer-details-scrim",
|
|
).length,
|
|
};
|
|
}));
|
|
await page.locator(".viewer-details-return").click();
|
|
await page.waitForFunction(() =>
|
|
(document.querySelector(".image-viewer")?.scrollTop ?? 1) < 1
|
|
);
|
|
viewer.returnedToImage = true;
|
|
await page.mouse.wheel(0, -target.height);
|
|
await page.waitForTimeout(80);
|
|
Object.assign(viewer, await page.locator(".image-viewer").evaluate((overlay) => ({
|
|
pageAfterReturn: overlay.dataset.page ?? "",
|
|
returnWheelHandoffLocked:
|
|
overlay.scrollTop < 1 && overlay.getAttribute("data-zoomed") === "false",
|
|
})));
|
|
}
|
|
if (target.name === "mobile") {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
const dispatchTouch = (type, touchPoints) => cdp.send("Input.dispatchTouchEvent", {
|
|
type,
|
|
touchPoints: touchPoints.map((point) => ({
|
|
...point,
|
|
radiusX: 2,
|
|
radiusY: 2,
|
|
force: 1,
|
|
})),
|
|
});
|
|
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-zoomed") === "false"
|
|
);
|
|
await dispatchTouch("touchStart", [
|
|
{ id: 11, x: 145, y: 420 },
|
|
{ id: 12, x: 245, y: 420 },
|
|
]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchMove", [
|
|
{ id: 11, x: 95, y: 420 },
|
|
{ id: 12, x: 295, y: 420 },
|
|
]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchEnd", []);
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-zoomed") === "true"
|
|
);
|
|
viewer.pinchZoomed = true;
|
|
await page.waitForFunction(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const original = document.querySelector(".viewer-native-original");
|
|
return overlay?.getAttribute("data-native-original-active") === "true"
|
|
&& overlay.getAttribute("data-display-source") === "original"
|
|
&& original
|
|
&& Number.parseFloat(getComputedStyle(original).opacity) >= .99;
|
|
}, undefined, { timeout: 60_000 });
|
|
await page.waitForTimeout(40);
|
|
viewer.pinchScale = await page.locator(".viewer-media").evaluate((media) =>
|
|
new DOMMatrix(getComputedStyle(media).transform).a
|
|
);
|
|
viewer.pinchNativeOriginal = await page.evaluate(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const media = document.querySelector(".viewer-media");
|
|
const original = document.querySelector(".viewer-native-original");
|
|
return {
|
|
active: overlay?.getAttribute("data-native-original-active") ?? "false",
|
|
displaySource: overlay?.getAttribute("data-display-source") ?? "",
|
|
visible: original ? Number.parseFloat(getComputedStyle(original).opacity) : 0,
|
|
settledWillChange: media ? getComputedStyle(media).willChange : "",
|
|
};
|
|
});
|
|
await page.screenshot({ path: "/tmp/pixhelf-viewer-zoom-mobile.png" });
|
|
|
|
await page.touchscreen.tap(195, 420);
|
|
await page.waitForTimeout(70);
|
|
await page.touchscreen.tap(195, 420);
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-zoomed") === "false"
|
|
);
|
|
viewer.doubleTapReset = true;
|
|
await page.waitForTimeout(260);
|
|
viewer.doubleTapScale = await page.locator(".viewer-media").evaluate((media) =>
|
|
new DOMMatrix(getComputedStyle(media).transform).a
|
|
);
|
|
viewer.nativeOriginalAfterReset = await page.evaluate(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const media = document.querySelector(".viewer-media");
|
|
const original = document.querySelector(".viewer-native-original");
|
|
return {
|
|
active: overlay?.getAttribute("data-native-original-active") ?? "false",
|
|
displaySource: overlay?.getAttribute("data-display-source") ?? "",
|
|
visible: original ? Number.parseFloat(getComputedStyle(original).opacity) : 0,
|
|
settledWillChange: media ? getComputedStyle(media).willChange : "",
|
|
};
|
|
});
|
|
|
|
const titleBeforeSwipe = await page.locator(".image-viewer").getAttribute("data-image-name");
|
|
await dispatchTouch("touchStart", [{ id: 31, x: 320, y: 420 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchMove", [{ id: 31, x: 90, y: 420 }]);
|
|
await page.waitForTimeout(32);
|
|
viewer.swipeDragTracking = await page.locator(".viewer-media").evaluate((media) =>
|
|
new DOMMatrix(getComputedStyle(media).transform).e
|
|
);
|
|
viewer.swipeDragWillChange = await page.locator(".viewer-media").evaluate((media) =>
|
|
getComputedStyle(media).willChange
|
|
);
|
|
await dispatchTouch("touchEnd", []);
|
|
viewer.swipeMotion = await page.evaluate(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const media = document.querySelector(".viewer-media");
|
|
const outgoing = document.querySelector(".viewer-swipe-outgoing");
|
|
return {
|
|
mode: overlay?.getAttribute("data-mobile-swipe-motion") ?? "",
|
|
direction: overlay?.getAttribute("data-swipe-direction") ?? "",
|
|
snapshot: overlay?.getAttribute("data-swipe-snapshot") ?? "",
|
|
releaseCostMs: Number.parseFloat(
|
|
overlay?.getAttribute("data-swipe-release-cost-ms") ?? "Infinity",
|
|
),
|
|
incomingAnimations: media?.getAnimations().length ?? 0,
|
|
outgoingAnimations: outgoing?.getAnimations().length ?? 0,
|
|
outgoingIgnoresInput: outgoing
|
|
? getComputedStyle(outgoing).pointerEvents === "none"
|
|
: false,
|
|
};
|
|
});
|
|
await page.waitForFunction((title) =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-image-name") !== title
|
|
, titleBeforeSwipe);
|
|
viewer.swipeNavigation = await page.locator(".image-viewer").getAttribute("data-image-name");
|
|
await page.waitForFunction(() =>
|
|
!document.querySelector(".viewer-swipe-outgoing")
|
|
&& !document.querySelector(".image-viewer")?.hasAttribute("data-swipe-direction")
|
|
);
|
|
viewer.swipeMotionCleaned = true;
|
|
|
|
const shortSwipeStartName = await page.locator(".image-viewer")
|
|
.getAttribute("data-image-name");
|
|
await dispatchTouch("touchStart", [{ id: 32, x: 240, y: 420 }]);
|
|
await page.waitForTimeout(100);
|
|
await dispatchTouch("touchMove", [{ id: 32, x: 200, y: 420 }]);
|
|
await page.waitForTimeout(100);
|
|
await dispatchTouch("touchEnd", []);
|
|
await page.waitForFunction((title) =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-image-name") !== title
|
|
, shortSwipeStartName);
|
|
const shortSwipeEndName = await page.locator(".image-viewer")
|
|
.getAttribute("data-image-name");
|
|
viewer.shortSwipeNavigation = {
|
|
distance: 40,
|
|
changed: shortSwipeEndName !== shortSwipeStartName,
|
|
imageName: shortSwipeEndName,
|
|
};
|
|
await page.waitForFunction(() =>
|
|
!document.querySelector(".viewer-swipe-outgoing")
|
|
&& !document.querySelector(".image-viewer")?.hasAttribute("data-swipe-direction")
|
|
);
|
|
|
|
await dispatchTouch("touchStart", [{ id: 35, x: 195, y: 500 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchMove", [{ id: 35, x: 195, y: 350 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchMove", [{ id: 35, x: 195, y: 630 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchEnd", []);
|
|
await page.waitForFunction(() =>
|
|
(document.querySelector(".image-viewer")?.scrollTop ?? 1) < 1
|
|
);
|
|
viewer.reversedPageGestureStayedOpen = await page.locator(".image-viewer").evaluate(
|
|
(overlay) => overlay.isConnected && overlay.dataset.page === "image",
|
|
);
|
|
|
|
await dispatchTouch("touchStart", [{ id: 36, x: 195, y: 690 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchMove", [{ id: 36, x: 195, y: 510 }]);
|
|
await page.waitForTimeout(32);
|
|
viewer.detailsFollowedTouch = await page.locator(".image-viewer").evaluate((overlay) =>
|
|
overlay.scrollTop > 100 &&
|
|
overlay.scrollTop < overlay.clientHeight * 0.5 &&
|
|
overlay.dataset.page === "transition"
|
|
);
|
|
const titleDuringPageGesture = await page.locator(".image-viewer")
|
|
.getAttribute("data-image-name");
|
|
await dispatchTouch("touchMove", [{ id: 36, x: 60, y: 510 }]);
|
|
await page.waitForTimeout(32);
|
|
viewer.pageGestureIgnoredHorizontal =
|
|
await page.locator(".image-viewer").getAttribute("data-image-name")
|
|
=== titleDuringPageGesture;
|
|
await dispatchTouch("touchMove", [{ id: 36, x: 195, y: 360 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchEnd", []);
|
|
await page.waitForFunction(() =>
|
|
(document.querySelector(".image-viewer")?.scrollTop ?? 0) > innerHeight * 0.5
|
|
);
|
|
await page.waitForTimeout(320);
|
|
await page.screenshot({ path: "/tmp/pixhelf-viewer-details-mobile.png" });
|
|
Object.assign(viewer, await page.evaluate(() => {
|
|
const overlay = document.querySelector(".image-viewer");
|
|
const stage = document.querySelector(".viewer-stage")?.getBoundingClientRect();
|
|
const details = document.querySelector(".viewer-details-page");
|
|
const detailsRect = details?.getBoundingClientRect();
|
|
return {
|
|
detailsReachedBySwipe: (overlay?.scrollTop ?? 0) > innerHeight * 0.5,
|
|
pageAfterDetails: overlay?.getAttribute("data-page") ?? "",
|
|
detailsName: details?.getAttribute("data-details-image-name") ?? "",
|
|
detailsHeading: document.querySelector(".viewer-details-heading h2")?.textContent ?? "",
|
|
stageScrolledAway: stage ? stage.top < -innerHeight * 0.5 : false,
|
|
detailsVisible: detailsRect
|
|
? detailsRect.top < innerHeight * 0.5 && detailsRect.bottom > innerHeight * 0.5
|
|
: false,
|
|
drawerArtifactsAfterScroll: document.querySelectorAll(
|
|
".viewer-details-sheet, .viewer-details-scrim",
|
|
).length,
|
|
};
|
|
}));
|
|
await page.locator(".viewer-details-return").click();
|
|
await page.waitForFunction(() =>
|
|
(document.querySelector(".image-viewer")?.scrollTop ?? 1) < 1
|
|
);
|
|
viewer.returnedToImage = true;
|
|
viewer.pageAfterReturn = await page.locator(".image-viewer").getAttribute("data-page");
|
|
|
|
await dispatchTouch("touchStart", [{ id: 41, x: 195, y: 210 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchMove", [{ id: 41, x: 195, y: 510 }]);
|
|
await page.waitForTimeout(32);
|
|
await dispatchTouch("touchEnd", []);
|
|
viewer.closedByGesture = true;
|
|
} else {
|
|
await page.keyboard.press("Escape");
|
|
}
|
|
await page.waitForSelector(".viewer-return-layer", { timeout: 5_000 });
|
|
viewer.returnAnimationCreated = await page.locator(".viewer-return-layer").evaluate((layer) => ({
|
|
imageId: layer.getAttribute("data-image-id") ?? "",
|
|
phase: layer.getAttribute("data-phase") ?? "",
|
|
mediaCount: layer.querySelectorAll(".viewer-return-media").length,
|
|
canvasHasPixels: [...layer.querySelectorAll("canvas")]
|
|
.every((canvas) => canvas.width > 0 && canvas.height > 0),
|
|
maxCanvasEdge: Math.max(0, ...[...layer.querySelectorAll("canvas")]
|
|
.flatMap((canvas) => [canvas.width, canvas.height])),
|
|
visualReady: [...layer.querySelectorAll(".viewer-return-visual")]
|
|
.some((visual) => visual.complete && visual.naturalWidth > 0),
|
|
}));
|
|
await page.waitForSelector(".image-viewer", { state: "detached" });
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".viewer-return-layer")?.getAttribute("data-phase") === "flying"
|
|
, undefined, { timeout: 5_000 });
|
|
viewer.returnAnimationFlying = await page.locator(".viewer-return-layer").evaluate((layer) => {
|
|
const media = layer.querySelector(".viewer-return-media");
|
|
const target = document.querySelector('[data-viewer-return-target="true"]');
|
|
const animation = media?.getAnimations()[0];
|
|
const keyframes = animation?.effect instanceof KeyframeEffect
|
|
? animation.effect.getKeyframes()
|
|
: [];
|
|
const animatedProperties = [...new Set(keyframes.flatMap((keyframe) =>
|
|
Object.keys(keyframe).filter((property) =>
|
|
!["offset", "computedOffset", "easing", "composite"].includes(property)
|
|
)
|
|
))].sort();
|
|
return {
|
|
animations: media?.getAnimations().length ?? 0,
|
|
targetId: target?.getAttribute("data-image-id") ?? "",
|
|
startLatency: Number(layer.getAttribute("data-flying-at") ?? 0)
|
|
- Number(layer.getAttribute("data-created-at") ?? 0),
|
|
duration: Number(layer.getAttribute("data-duration") ?? 0),
|
|
animatedProperties,
|
|
};
|
|
});
|
|
const animationViewport = page.viewportSize();
|
|
if (animationViewport) {
|
|
await page.setViewportSize({
|
|
width: animationViewport.width + (target.name === "desktop" ? -18 : 10),
|
|
height: animationViewport.height,
|
|
});
|
|
await page.waitForFunction(() =>
|
|
Number(document.querySelector(".viewer-return-layer")?.getAttribute("data-retargets") ?? 0) > 0
|
|
);
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".viewer-return-layer")?.getAttribute("data-phase") === "flying"
|
|
);
|
|
viewer.returnAnimationRetargeted = await page.locator(".viewer-return-layer").evaluate(
|
|
(layer) => ({
|
|
count: Number(layer.getAttribute("data-retargets") ?? 0),
|
|
animations: layer.querySelector(".viewer-return-media")?.getAnimations().length ?? 0,
|
|
}),
|
|
);
|
|
}
|
|
await page.waitForTimeout(90);
|
|
await page.screenshot({ path: `/tmp/pixhelf-viewer-return-${target.name}.png` });
|
|
viewer.closed = await page.locator(".image-viewer").count();
|
|
viewer.rootRestored = await page.locator("#root").evaluate((root) => !root.inert);
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".app-shell")?.getAttribute("data-viewer-returning") === "false"
|
|
);
|
|
viewer.returnAnchorSettled = true;
|
|
viewer.returnAnimationCleared = await page.locator(".viewer-return-layer").count() === 0;
|
|
if (
|
|
viewer.returnAnimationCreated.mediaCount !== 1 ||
|
|
!viewer.returnAnimationCreated.canvasHasPixels ||
|
|
viewer.returnAnimationCreated.maxCanvasEdge > 1_440 ||
|
|
!viewer.returnAnimationCreated.visualReady ||
|
|
viewer.returnAnimationFlying.animations < 1 ||
|
|
viewer.returnAnimationFlying.targetId !== viewer.returnAnimationCreated.imageId ||
|
|
viewer.returnAnimationFlying.startLatency > 180 ||
|
|
viewer.returnAnimationFlying.duration > 280 ||
|
|
viewer.returnAnimationFlying.animatedProperties.join(",") !== "transform" ||
|
|
viewer.returnAnimationRetargeted?.count < 1 ||
|
|
viewer.returnAnimationRetargeted?.animations < 1 ||
|
|
!viewer.returnAnimationCleared
|
|
) {
|
|
throw new Error(`viewer return animation failed: ${JSON.stringify({
|
|
created: viewer.returnAnimationCreated,
|
|
flying: viewer.returnAnimationFlying,
|
|
cleared: viewer.returnAnimationCleared,
|
|
})}`);
|
|
}
|
|
|
|
const returnProbeStart = await page.evaluate(() => {
|
|
const cards = [...document.querySelectorAll(".masonry .image-card")];
|
|
const card = cards.at(-1);
|
|
if (!(card instanceof HTMLElement)) return null;
|
|
card.scrollIntoView({ block: "center", behavior: "instant" });
|
|
const rect = card.getBoundingClientRect();
|
|
const viewportTop = visualViewport?.offsetTop ?? 0;
|
|
const viewportHeight = visualViewport?.height ?? innerHeight;
|
|
const viewportBottom = viewportTop + viewportHeight;
|
|
const topbarBottom = document.querySelector(".topbar")?.getBoundingClientRect().bottom
|
|
?? viewportTop;
|
|
const visibleTop = Math.max(rect.top, viewportTop, topbarBottom);
|
|
const visibleBottom = Math.min(rect.bottom, viewportBottom);
|
|
const anchorY = visibleBottom > visibleTop
|
|
? (visibleTop + visibleBottom) / 2
|
|
: Math.min(viewportBottom, Math.max(viewportTop, rect.top + rect.height / 2));
|
|
const result = {
|
|
imageId: card.dataset.imageId ?? "",
|
|
beforeCount: cards.length,
|
|
beforeMasonryWidth: document.querySelector(".masonry")?.getBoundingClientRect().width ?? 0,
|
|
cardRatio: rect.height > 0 ? (anchorY - rect.top) / rect.height : 0.5,
|
|
viewportRatio: (anchorY - viewportTop) / Math.max(1, viewportHeight),
|
|
};
|
|
card.dispatchEvent(new MouseEvent("click", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
button: 0,
|
|
clientX: rect.left + rect.width / 2,
|
|
clientY: anchorY,
|
|
view: window,
|
|
}));
|
|
return result;
|
|
});
|
|
if (!returnProbeStart) throw new Error("viewer return probe could not find a card");
|
|
await page.waitForSelector(".image-viewer");
|
|
const returnViewerStartId = await page.locator(".image-viewer").getAttribute("data-image-id");
|
|
await page.keyboard.press("ArrowRight");
|
|
const resizedWidth = target.name === "desktop" ? 920 : 430;
|
|
await page.setViewportSize({ width: resizedWidth, height: target.height });
|
|
await page.waitForFunction((previousWidth) => {
|
|
const width = document.querySelector(".masonry")?.getBoundingClientRect().width ?? 0;
|
|
return Math.abs(width - previousWidth) > 20;
|
|
}, returnProbeStart.beforeMasonryWidth);
|
|
await page.waitForFunction(([startId, beforeCount]) =>
|
|
document.querySelector(".image-viewer")?.getAttribute("data-image-id") !== startId &&
|
|
document.querySelectorAll(".masonry .image-card").length > beforeCount
|
|
, [returnViewerStartId, returnProbeStart.beforeCount], { timeout: 60_000 });
|
|
const returnTargetId = await page.locator(".image-viewer").getAttribute("data-image-id");
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForSelector(".image-viewer", { state: "detached" });
|
|
await page.waitForFunction((imageId) => {
|
|
const shell = document.querySelector(".app-shell");
|
|
const active = document.activeElement;
|
|
return shell?.getAttribute("data-viewer-returning") === "false" &&
|
|
active instanceof HTMLElement && active.dataset.imageId === imageId;
|
|
}, returnTargetId);
|
|
const returnRestoration = await page.evaluate(({ start, targetId }) => {
|
|
const card = document.querySelector(
|
|
`.masonry .image-card[data-image-id="${CSS.escape(targetId)}"]`,
|
|
);
|
|
if (!(card instanceof HTMLElement)) return null;
|
|
const rect = card.getBoundingClientRect();
|
|
const viewportTop = visualViewport?.offsetTop ?? 0;
|
|
const viewportHeight = visualViewport?.height ?? innerHeight;
|
|
const viewportBottom = viewportTop + viewportHeight;
|
|
const topbarBottom = document.querySelector(".topbar")?.getBoundingClientRect().bottom
|
|
?? viewportTop;
|
|
const safeTop = Math.min(viewportBottom, Math.max(viewportTop, topbarBottom + 8));
|
|
const safeBottom = Math.max(safeTop, viewportBottom - 8);
|
|
const expectedAnchorY = Math.min(
|
|
safeBottom,
|
|
Math.max(safeTop, viewportTop + start.viewportRatio * viewportHeight),
|
|
);
|
|
const actualAnchorY = rect.top + rect.height * start.cardRatio;
|
|
return {
|
|
openedId: start.imageId,
|
|
targetId,
|
|
cardsBefore: start.beforeCount,
|
|
cardsAfter: document.querySelectorAll(".masonry .image-card").length,
|
|
currentId: document.activeElement?.getAttribute("data-image-id") ?? "",
|
|
visible: rect.bottom > topbarBottom && rect.top < viewportBottom,
|
|
anchorDelta: Math.abs(actualAnchorY - expectedAnchorY),
|
|
returning: document.querySelector(".app-shell")
|
|
?.getAttribute("data-viewer-returning"),
|
|
};
|
|
}, { start: returnProbeStart, targetId: returnTargetId });
|
|
if (
|
|
!returnRestoration ||
|
|
returnRestoration.openedId === returnRestoration.targetId ||
|
|
returnRestoration.targetId !== returnRestoration.currentId ||
|
|
returnRestoration.cardsAfter <= returnRestoration.cardsBefore ||
|
|
!returnRestoration.visible ||
|
|
returnRestoration.anchorDelta > 2 ||
|
|
returnRestoration.returning !== "false"
|
|
) {
|
|
throw new Error(`viewer return restoration failed: ${JSON.stringify(returnRestoration)}`);
|
|
}
|
|
if (viewerReturnOnly) {
|
|
const result = {
|
|
name: target.name,
|
|
resizeStability,
|
|
sideButtonNavigation: viewer.sideButtonNavigation,
|
|
switchPerformance: viewer.switchPerformance,
|
|
rapidSwitch: viewer.rapidSwitch,
|
|
returnAnimation: {
|
|
created: viewer.returnAnimationCreated,
|
|
flying: viewer.returnAnimationFlying,
|
|
retargeted: viewer.returnAnimationRetargeted,
|
|
cleared: viewer.returnAnimationCleared,
|
|
},
|
|
returnRestoration,
|
|
browserErrors,
|
|
};
|
|
if (browserErrors.length) {
|
|
throw new Error(`viewer return browser errors: ${JSON.stringify(browserErrors)}`);
|
|
}
|
|
results.push(result);
|
|
console.log(JSON.stringify(result));
|
|
await page.close();
|
|
continue;
|
|
}
|
|
await page.setViewportSize({ width: target.width, height: target.height });
|
|
await page.waitForTimeout(120);
|
|
await page.evaluate(() => window.scrollTo({ top: 0, left: 0, behavior: "instant" }));
|
|
|
|
const searchToggleBefore = await page.locator(".search-toggle").boundingBox();
|
|
await page.locator(".search-toggle").click();
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".search-toggle")?.getAttribute("aria-expanded") === "true"
|
|
);
|
|
await page.waitForTimeout(220);
|
|
const searchToggleOpen = await page.locator(".search-toggle").boundingBox();
|
|
const searchPanelOpen = await page.locator("#gallery-search-field").boundingBox();
|
|
const searchDisclosure = await page.evaluate(() => {
|
|
const leading = document.querySelector(".topbar-leading")?.getBoundingClientRect();
|
|
const panel = document.querySelector("#gallery-search-field")?.getBoundingClientRect();
|
|
const topbar = document.querySelector(".topbar")?.getBoundingClientRect();
|
|
return {
|
|
expanded: document.querySelector(".search-toggle")?.getAttribute("aria-expanded"),
|
|
focused: document.activeElement?.matches("#gallery-search-field input") ?? false,
|
|
panelRole: document.querySelector("#gallery-search-field")?.getAttribute("role"),
|
|
panelInert: document.querySelector("#gallery-search-field")?.inert ?? true,
|
|
leadingGap: leading && panel ? panel.left - leading.right : -1,
|
|
panelTop: panel?.top ?? -1,
|
|
topbarTop: topbar?.top ?? -1,
|
|
topbarBottom: topbar?.bottom ?? -1,
|
|
};
|
|
});
|
|
searchDisclosure.panelWidth = searchPanelOpen?.width ?? -1;
|
|
searchDisclosure.stationaryToggleDelta = searchToggleBefore && searchToggleOpen
|
|
? Math.hypot(
|
|
searchToggleBefore.x - searchToggleOpen.x,
|
|
searchToggleBefore.y - searchToggleOpen.y,
|
|
)
|
|
: -1;
|
|
await page.locator("#gallery-search-field input").fill("x");
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".topbar-search")?.classList.contains("has-query")
|
|
);
|
|
searchDisclosure.hasQueryClass = true;
|
|
await page.locator("#gallery-search-field input").fill("");
|
|
await page.waitForFunction(() =>
|
|
!document.querySelector(".topbar-search")?.classList.contains("has-query")
|
|
);
|
|
searchDisclosure.queryClassCleared = true;
|
|
if (target.name === "mobile") {
|
|
await page.evaluate(() => {
|
|
document.documentElement.style.setProperty("--visual-viewport-top", "24px");
|
|
});
|
|
await page.waitForTimeout(50);
|
|
const compensatedTopbar = await page.locator(".topbar").boundingBox();
|
|
const compensatedPanel = await page.locator("#gallery-search-field").boundingBox();
|
|
searchDisclosure.compensatedTopbarDelta = compensatedTopbar
|
|
? compensatedTopbar.y - searchDisclosure.topbarTop
|
|
: -1;
|
|
searchDisclosure.compensatedPanelDelta = compensatedPanel
|
|
? compensatedPanel.y - searchDisclosure.panelTop
|
|
: -1;
|
|
await page.evaluate(() => {
|
|
document.documentElement.style.setProperty("--visual-viewport-top", "0px");
|
|
});
|
|
await page.waitForTimeout(50);
|
|
}
|
|
await page.locator(".search-toggle").click();
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".search-toggle")?.getAttribute("aria-expanded") === "false"
|
|
);
|
|
searchDisclosure.restored = await page.locator(".search-toggle")
|
|
.getAttribute("aria-expanded");
|
|
searchDisclosure.panelInertClosed = await page.locator("#gallery-search-field")
|
|
.evaluate((panel) => panel.inert);
|
|
|
|
const firstExploreRequest = page.waitForRequest((request) => {
|
|
const url = new URL(request.url());
|
|
return url.pathname === "/api/images" &&
|
|
url.searchParams.get("sort") === "explore" &&
|
|
url.searchParams.get("offset") === "0";
|
|
});
|
|
await page.locator(".explore-toggle").click();
|
|
const firstExplore = await firstExploreRequest;
|
|
const firstExploreUrl = new URL(firstExplore.url());
|
|
const firstExploreResponse = await firstExplore.response();
|
|
await firstExploreResponse?.finished();
|
|
await page.waitForFunction(() => !document.querySelector(".skeleton-grid"));
|
|
|
|
const secondExploreRequest = page.waitForRequest((request) => {
|
|
const url = new URL(request.url());
|
|
return url.pathname === "/api/images" &&
|
|
url.searchParams.get("sort") === "explore" &&
|
|
url.searchParams.get("offset") === "0";
|
|
});
|
|
await page.locator(".explore-toggle").click();
|
|
const secondExplore = await secondExploreRequest;
|
|
const secondExploreUrl = new URL(secondExplore.url());
|
|
const secondExploreResponse = await secondExplore.response();
|
|
await secondExploreResponse?.finished();
|
|
await page.waitForFunction(() => !document.querySelector(".skeleton-grid"));
|
|
|
|
const exploration = await page.evaluate(([firstSeed, secondSeed]) => ({
|
|
firstSeed,
|
|
secondSeed,
|
|
exploreMarker: getComputedStyle(
|
|
document.querySelector(".explore-toggle"),
|
|
"::after",
|
|
).content,
|
|
searchMarker: getComputedStyle(
|
|
document.querySelector(".search-toggle"),
|
|
"::after",
|
|
).content,
|
|
}), [
|
|
firstExploreUrl.searchParams.get("seed") ?? "",
|
|
secondExploreUrl.searchParams.get("seed") ?? "",
|
|
]);
|
|
|
|
const homeRequest = page.waitForRequest((request) => {
|
|
const url = new URL(request.url());
|
|
return url.pathname === "/api/images" &&
|
|
url.searchParams.get("sort") === "name-asc" &&
|
|
url.searchParams.get("offset") === "0" &&
|
|
!url.searchParams.has("album") &&
|
|
!url.searchParams.has("search");
|
|
});
|
|
await page.locator("a.brand").click();
|
|
const homeResponse = await (await homeRequest).response();
|
|
await homeResponse?.finished();
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".explore-toggle")?.getAttribute("aria-pressed") === "false" &&
|
|
document.querySelector(".desktop-sidebar .album-link")?.classList.contains("active") &&
|
|
document.querySelector("#gallery-search-field input")?.value === ""
|
|
, undefined, { timeout: 60_000 });
|
|
const homeNavigation = await page.evaluate(() => ({
|
|
href: document.querySelector("a.brand")?.getAttribute("href"),
|
|
galleryPath: document.querySelector(
|
|
".desktop-sidebar .sidebar-gallery-path",
|
|
)?.textContent,
|
|
allImagesActive: document.querySelector(".desktop-sidebar .album-link")
|
|
?.classList.contains("active") ?? false,
|
|
searchValue: document.querySelector("#gallery-search-field input")?.value,
|
|
}));
|
|
|
|
let mobileCardNames = null;
|
|
if (target.name === "mobile") {
|
|
const firstCard = page.locator("[data-image-id]").first();
|
|
const secondCard = page.locator("[data-image-id]").nth(1);
|
|
await firstCard.dispatchEvent("pointerdown", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
pointerId: 1,
|
|
pointerType: "touch",
|
|
isPrimary: true,
|
|
});
|
|
await page.waitForFunction(() =>
|
|
document.querySelector("[data-image-id]")?.getAttribute("data-name-visible") === "true"
|
|
);
|
|
const shownOnContact = await firstCard.getAttribute("data-name-visible");
|
|
await firstCard.dispatchEvent("pointerup", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
pointerId: 1,
|
|
pointerType: "touch",
|
|
isPrimary: true,
|
|
});
|
|
const retainedOnRelease = await firstCard.getAttribute("data-name-visible");
|
|
await page.waitForTimeout(1_900);
|
|
const retainedWithoutTimeout = await firstCard.getAttribute("data-name-visible");
|
|
|
|
await secondCard.dispatchEvent("pointerdown", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
pointerId: 2,
|
|
pointerType: "touch",
|
|
isPrimary: true,
|
|
});
|
|
await page.waitForFunction(() =>
|
|
document.querySelectorAll('[data-name-visible="true"]').length === 1 &&
|
|
document.querySelectorAll("[data-image-id]")[1]
|
|
?.getAttribute("data-name-visible") === "true"
|
|
);
|
|
await page.waitForTimeout(180);
|
|
const switchedToSecond = await secondCard.getAttribute("data-name-visible");
|
|
const firstHidden = await firstCard.getAttribute("data-name-visible");
|
|
const activeCount = await page.locator('[data-name-visible="true"]').count();
|
|
const displayedCount = await page.locator('[data-name-visible="true"] .image-name')
|
|
.evaluateAll((names) => names.filter((name) =>
|
|
Number.parseFloat(getComputedStyle(name).opacity) > 0.99
|
|
).length);
|
|
|
|
await secondCard.dispatchEvent("pointerup", {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
pointerId: 2,
|
|
pointerType: "touch",
|
|
isPrimary: true,
|
|
});
|
|
await secondCard.evaluate((card) => {
|
|
globalThis.__pixhelfCardClickCount = 0;
|
|
card.addEventListener("click", () => {
|
|
globalThis.__pixhelfCardClickCount += 1;
|
|
}, { once: true });
|
|
});
|
|
const secondCardBox = await secondCard.boundingBox();
|
|
if (secondCardBox) {
|
|
await page.touchscreen.tap(
|
|
secondCardBox.x + secondCardBox.width / 2,
|
|
secondCardBox.y + secondCardBox.height / 2,
|
|
);
|
|
}
|
|
const clickCount = await page.evaluate(() => globalThis.__pixhelfCardClickCount ?? 0);
|
|
await page.waitForSelector(".image-viewer");
|
|
const viewerOpenedFromTouch = await page.locator(".image-viewer").count();
|
|
await page.locator(".viewer-close").click();
|
|
await page.waitForSelector(".image-viewer", { state: "detached" });
|
|
mobileCardNames = {
|
|
shownOnContact,
|
|
retainedOnRelease,
|
|
retainedWithoutTimeout,
|
|
switchedToSecond,
|
|
firstHidden,
|
|
activeCount,
|
|
displayedCount,
|
|
clickCount,
|
|
viewerOpenedFromTouch,
|
|
};
|
|
}
|
|
|
|
let desktopSidebar = null;
|
|
let mobileNavigation = null;
|
|
if (target.name === "desktop") {
|
|
const contentBefore = await page.locator(".content").boundingBox();
|
|
const toggleBefore = await page.locator(".gallery-sidebar-toggle").boundingBox();
|
|
const firstAlbumBefore = await page.locator(".desktop-sidebar .album-link").first()
|
|
.boundingBox();
|
|
const controlSlotBefore = await page.locator(".desktop-sidebar .sidebar-control-slot")
|
|
.boundingBox();
|
|
const metaBefore = await page.locator(".desktop-sidebar .sidebar-gallery-meta")
|
|
.boundingBox();
|
|
const galleryBefore = await page.locator(".masonry").boundingBox();
|
|
await page.locator(".gallery-sidebar-toggle").click();
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".app-shell")?.getAttribute("data-sidebar-collapsed") === "true"
|
|
);
|
|
await page.waitForTimeout(260);
|
|
const contentCollapsed = await page.locator(".content").boundingBox();
|
|
const toggleCollapsed = await page.locator(".gallery-sidebar-toggle").boundingBox();
|
|
const sidebarCollapsedBox = await page.locator(".desktop-sidebar").boundingBox();
|
|
const galleryCollapsed = await page.locator(".masonry").boundingBox();
|
|
desktopSidebar = await page.evaluate(() => ({
|
|
expanded: document.querySelector(".gallery-sidebar-toggle")?.getAttribute("aria-expanded"),
|
|
visibility: getComputedStyle(document.querySelector(".desktop-sidebar")).visibility,
|
|
navigationVisibility: getComputedStyle(
|
|
document.querySelector(".desktop-sidebar .album-nav"),
|
|
).visibility,
|
|
statusVisibility: getComputedStyle(
|
|
document.querySelector(".desktop-sidebar .sidebar-status"),
|
|
).visibility,
|
|
metaVisibility: getComputedStyle(
|
|
document.querySelector(".desktop-sidebar .sidebar-gallery-meta"),
|
|
).visibility,
|
|
ariaHidden: document.querySelector(".desktop-sidebar")?.getAttribute("aria-hidden"),
|
|
stored: localStorage.getItem("pixhelf.sidebar-collapsed"),
|
|
columns: Number(
|
|
document.querySelector(".masonry")?.getAttribute("data-columns") ?? 0,
|
|
),
|
|
redundantHeadings: document.querySelectorAll(".sidebar-heading").length,
|
|
pathDetails: document.querySelectorAll(".album-copy small").length,
|
|
progressPanels: document.querySelectorAll(".sidebar-progress").length,
|
|
inlineToggles: document.querySelectorAll(".desktop-sidebar-inline-toggle").length,
|
|
galleryToggles: document.querySelectorAll(".gallery-sidebar-toggle").length,
|
|
}));
|
|
desktopSidebar.contentBefore = contentBefore?.x ?? -1;
|
|
desktopSidebar.contentCollapsed = contentCollapsed?.x ?? -1;
|
|
desktopSidebar.expandedToggleWidth = toggleBefore?.width ?? -1;
|
|
desktopSidebar.expandedToggleHeight = toggleBefore?.height ?? -1;
|
|
desktopSidebar.collapsedToggleWidth = toggleCollapsed?.width ?? -1;
|
|
desktopSidebar.collapsedToggleHeight = toggleCollapsed?.height ?? -1;
|
|
desktopSidebar.albumClearance = controlSlotBefore && firstAlbumBefore
|
|
? firstAlbumBefore.y - (controlSlotBefore.y + controlSlotBefore.height)
|
|
: -1;
|
|
desktopSidebar.toggleControlInsetX = toggleBefore && controlSlotBefore
|
|
? toggleBefore.x - controlSlotBefore.x
|
|
: -1;
|
|
desktopSidebar.toggleControlInsetY = toggleBefore && controlSlotBefore
|
|
? toggleBefore.y - controlSlotBefore.y
|
|
: -1;
|
|
desktopSidebar.stationaryToggleDelta = toggleBefore && toggleCollapsed
|
|
? Math.hypot(toggleBefore.x - toggleCollapsed.x, toggleBefore.y - toggleCollapsed.y)
|
|
: -1;
|
|
desktopSidebar.metaIsBelowAlbums = metaBefore && firstAlbumBefore
|
|
? metaBefore.y > firstAlbumBefore.y + firstAlbumBefore.height
|
|
: false;
|
|
desktopSidebar.expandedGalleryInsetX = toggleBefore && galleryBefore
|
|
? toggleBefore.x - galleryBefore.x
|
|
: -1;
|
|
desktopSidebar.expandedGalleryInsetY = toggleBefore && galleryBefore
|
|
? toggleBefore.y - galleryBefore.y
|
|
: -1;
|
|
desktopSidebar.collapsedGalleryInsetX = toggleCollapsed && galleryCollapsed
|
|
? toggleCollapsed.x - galleryCollapsed.x
|
|
: -1;
|
|
desktopSidebar.collapsedGalleryInsetY = toggleCollapsed && galleryCollapsed
|
|
? toggleCollapsed.y - galleryCollapsed.y
|
|
: -1;
|
|
desktopSidebar.expandedGalleryOverlap = toggleBefore && galleryBefore
|
|
? !(
|
|
toggleBefore.x + toggleBefore.width <= galleryBefore.x ||
|
|
galleryBefore.x + galleryBefore.width <= toggleBefore.x ||
|
|
toggleBefore.y + toggleBefore.height <= galleryBefore.y ||
|
|
galleryBefore.y + galleryBefore.height <= toggleBefore.y
|
|
)
|
|
: false;
|
|
desktopSidebar.galleryOverlap = toggleCollapsed && galleryCollapsed
|
|
? !(
|
|
toggleCollapsed.x + toggleCollapsed.width <= galleryCollapsed.x ||
|
|
galleryCollapsed.x + galleryCollapsed.width <= toggleCollapsed.x ||
|
|
toggleCollapsed.y + toggleCollapsed.height <= galleryCollapsed.y ||
|
|
galleryCollapsed.y + galleryCollapsed.height <= toggleCollapsed.y
|
|
)
|
|
: true;
|
|
desktopSidebar.sidebarRight = sidebarCollapsedBox
|
|
? sidebarCollapsedBox.x + sidebarCollapsedBox.width
|
|
: -1;
|
|
|
|
await page.locator(".gallery-sidebar-toggle").click();
|
|
await page.waitForFunction(() =>
|
|
document.querySelector(".app-shell")?.getAttribute("data-sidebar-collapsed") === "false"
|
|
);
|
|
await page.waitForTimeout(260);
|
|
const toggleRestored = await page.locator(".gallery-sidebar-toggle").boundingBox();
|
|
desktopSidebar.restoredToggleDelta = toggleBefore && toggleRestored
|
|
? Math.hypot(toggleBefore.x - toggleRestored.x, toggleBefore.y - toggleRestored.y)
|
|
: -1;
|
|
desktopSidebar.restored = await page.locator(".gallery-sidebar-toggle")
|
|
.getAttribute("aria-expanded");
|
|
} else {
|
|
await page.evaluate(() => window.scrollTo({ top: 320, behavior: "auto" }));
|
|
await page.waitForTimeout(100);
|
|
const fixedTop = await page.locator(".topbar").boundingBox();
|
|
await page.evaluate(() => window.scrollTo({ top: 0, behavior: "auto" }));
|
|
const navigationToggleBefore = await page.locator(".gallery-sidebar-toggle").boundingBox();
|
|
const galleryBefore = await page.locator(".masonry").boundingBox();
|
|
await page.locator(".gallery-sidebar-toggle").click();
|
|
await page.waitForSelector(".mobile-nav-layer");
|
|
await page.waitForTimeout(240);
|
|
const navigationToggleOpen = await page.locator(".gallery-sidebar-toggle").boundingBox();
|
|
const navigationLayer = await page.locator(".mobile-nav-layer").boundingBox();
|
|
const navigationDrawer = await page.locator(".mobile-sidebar").boundingBox();
|
|
const controlSlot = await page.locator(".mobile-sidebar .sidebar-control-slot").boundingBox();
|
|
const mobileMeta = await page.locator(".mobile-sidebar .sidebar-gallery-meta").boundingBox();
|
|
const mobileStatus = await page.locator(".mobile-sidebar .sidebar-status").boundingBox();
|
|
const topbar = await page.locator(".topbar").boundingBox();
|
|
mobileNavigation = {
|
|
opened: await page.locator(".mobile-nav-layer").count(),
|
|
fixedTop: fixedTop?.y ?? -1,
|
|
expanded: await page.locator(".gallery-sidebar-toggle").getAttribute("aria-expanded"),
|
|
internalToggles: await page.locator(".mobile-sidebar .sidebar-toggle-button").count(),
|
|
metaRows: await page.locator(".mobile-sidebar .sidebar-gallery-meta").count(),
|
|
galleryPath: await page.locator(".mobile-sidebar .sidebar-gallery-path").textContent(),
|
|
galleryCount: await page.locator(".mobile-sidebar .sidebar-gallery-count").textContent(),
|
|
layerTopGap: navigationLayer && topbar
|
|
? navigationLayer.y - (topbar.y + topbar.height)
|
|
: -1,
|
|
toggleWidth: navigationToggleBefore?.width ?? -1,
|
|
toggleHeight: navigationToggleBefore?.height ?? -1,
|
|
expandedToggleWidth: navigationToggleOpen?.width ?? -1,
|
|
galleryInsetX: navigationToggleBefore && galleryBefore
|
|
? navigationToggleBefore.x - galleryBefore.x
|
|
: -1,
|
|
galleryInsetY: navigationToggleBefore && galleryBefore
|
|
? navigationToggleBefore.y - galleryBefore.y
|
|
: -1,
|
|
galleryOverlap: navigationToggleBefore && galleryBefore
|
|
? !(
|
|
navigationToggleBefore.x + navigationToggleBefore.width <= galleryBefore.x ||
|
|
galleryBefore.x + galleryBefore.width <= navigationToggleBefore.x ||
|
|
navigationToggleBefore.y + navigationToggleBefore.height <= galleryBefore.y ||
|
|
galleryBefore.y + galleryBefore.height <= navigationToggleBefore.y
|
|
)
|
|
: -1,
|
|
stationaryToggleDelta: navigationToggleBefore && navigationToggleOpen
|
|
? Math.hypot(
|
|
navigationToggleBefore.x - navigationToggleOpen.x,
|
|
navigationToggleBefore.y - navigationToggleOpen.y,
|
|
)
|
|
: -1,
|
|
toggleControlInsetX: navigationToggleOpen && controlSlot
|
|
? navigationToggleOpen.x - controlSlot.x
|
|
: -1,
|
|
toggleControlInsetY: navigationToggleOpen && controlSlot
|
|
? navigationToggleOpen.y - controlSlot.y
|
|
: -1,
|
|
metaBottomGap: mobileMeta ? target.height - (mobileMeta.y + mobileMeta.height) : -1,
|
|
metaStatusGap: mobileMeta && mobileStatus
|
|
? mobileMeta.y - (mobileStatus.y + mobileStatus.height)
|
|
: -1,
|
|
drawerWidth: navigationDrawer?.width ?? -1,
|
|
};
|
|
await page.locator(".gallery-sidebar-toggle").click();
|
|
await page.waitForSelector(".mobile-nav-layer", { state: "detached" });
|
|
mobileNavigation.closed = await page.locator(".mobile-nav-layer").count();
|
|
mobileNavigation.restored = await page.locator(".gallery-sidebar-toggle")
|
|
.getAttribute("aria-expanded");
|
|
}
|
|
|
|
const result = {
|
|
name: target.name,
|
|
...layout,
|
|
resizeStability,
|
|
dialogsAfterImageClick,
|
|
viewer,
|
|
returnRestoration,
|
|
searchDisclosure,
|
|
exploration,
|
|
homeNavigation,
|
|
mobileCardNames,
|
|
desktopSidebar,
|
|
mobileNavigation,
|
|
browserErrors,
|
|
};
|
|
results.push(result);
|
|
console.log(JSON.stringify(result));
|
|
|
|
const expectedColumns = target.name === "desktop" ? 5 : 2;
|
|
if (
|
|
layout.horizontalOverflow ||
|
|
layout.brokenVisibleImages ||
|
|
layout.masonryColumns !== expectedColumns ||
|
|
layout.cardTags.some((tag) => tag !== "FIGURE") ||
|
|
layout.dialogs !== 0 ||
|
|
dialogsAfterImageClick !== 1 ||
|
|
viewer.title !== firstViewerTitle ||
|
|
viewer.fullLoaded !== "true" ||
|
|
!viewer.originalPath.endsWith("/original") ||
|
|
!["native", "safe-canvas", "viewport-bitmap"].includes(viewer.renderer) ||
|
|
viewer.sourceStrategy !== (
|
|
target.name === "mobile" ? "viewport-upgrade" : "direct-original"
|
|
) ||
|
|
viewer.thumbnailFilter !== "none" ||
|
|
viewer.thumbnailTransform !== "none" ||
|
|
(viewer.sourcePresentation === "upgrade" && (
|
|
Number.parseFloat(viewer.thumbnailTransitionDelay) < .15 ||
|
|
Number.parseFloat(viewer.originalTransitionDuration) < .15
|
|
)) ||
|
|
viewer.renderedPixels.some((size) => size <= 0) ||
|
|
(viewer.renderer === "safe-canvas" && viewer.renderedPixels.some((size) => size > 4096)) ||
|
|
(viewer.renderer === "viewport-bitmap" && viewer.renderedPixels.some((size) => size > 2048)) ||
|
|
(target.name === "mobile" && viewer.renderer !== "viewport-bitmap") ||
|
|
viewer.prefetchedOriginals < 2 ||
|
|
viewer.previewRequests !== 0 ||
|
|
viewer.fullscreenButtons !== 1 ||
|
|
viewer.detailsPages !== 1 ||
|
|
viewer.scrollCues !== 1 ||
|
|
viewer.headingModules !== 0 ||
|
|
!viewer.unifiedControls ||
|
|
viewer.zoomControls !== 0 ||
|
|
!viewer.scrollCueArrowOnly ||
|
|
viewer.drawerArtifacts !== 0 ||
|
|
viewer.scrollMode !== "continuous" ||
|
|
!viewer.continuousLayout ||
|
|
viewer.gestureHelp !== 0 ||
|
|
viewer.loadingStatus !== 0 ||
|
|
viewer.loadingCopyVisible ||
|
|
!viewer.fullscreenEntered ||
|
|
viewer.fullscreenLabel !== "退出全屏" ||
|
|
!viewer.fullscreenExited ||
|
|
!viewer.rootInert ||
|
|
!viewer.closeFocused ||
|
|
!viewer.mediaContained ||
|
|
!viewer.position.includes("/") ||
|
|
viewer.zoomed !== "true" ||
|
|
Math.abs(viewer.zoomScale - 1.5) > 0.02 ||
|
|
!viewer.zoomKeptImagePage ||
|
|
!viewer.zoomLockedPageScroll ||
|
|
!viewer.keyboardNavigation ||
|
|
viewer.keyboardNavigation === firstViewerTitle ||
|
|
!viewer.keyboardFocus?.parkedOnViewer ||
|
|
viewer.keyboardFocus.visibleControlFocusRings !== 0 ||
|
|
!viewer.switchPerformance?.focusedOnViewer ||
|
|
viewer.switchPerformance.visibleControlFocusRings !== 0 ||
|
|
(target.name === "mobile"
|
|
? !["thumbnail", "viewport-bitmap"].includes(viewer.switchPerformance.firstFrameSource)
|
|
: viewer.switchPerformance.firstFrameSource !== "original") ||
|
|
viewer.switchPerformance.mediaAnimations !== 0 ||
|
|
viewer.switchPerformance.dispatchDuration > 80 ||
|
|
viewer.switchPerformance.firstFrameDuration > 100 ||
|
|
!viewerChrome.scrollCuePresentation?.borderless ||
|
|
!viewerChrome.scrollCuePresentation?.transparent ||
|
|
viewerChrome.scrollCuePresentation.width < 88 ||
|
|
!viewerChrome.scrollCuePresentation?.clearsMedia ||
|
|
(target.name === "desktop" && (
|
|
!viewer.detailsReachedByWheel ||
|
|
viewer.detailsName !== viewer.keyboardNavigation ||
|
|
!viewer.zoomWheelHandoffLocked ||
|
|
viewer.pageAfterDetails !== "details" ||
|
|
viewer.detailsHeading !== "图片详情" ||
|
|
!viewer.stageScrolledAway ||
|
|
!viewer.detailsVisible ||
|
|
viewer.drawerArtifactsAfterScroll !== 0 ||
|
|
!viewer.returnedToImage ||
|
|
viewer.pageAfterReturn !== "image" ||
|
|
!viewer.returnWheelHandoffLocked
|
|
)) ||
|
|
(target.name === "mobile" && (
|
|
viewer.nativeOriginalLoaded !== "false" ||
|
|
viewer.nativeOriginalActive !== "false" ||
|
|
viewer.nativeOriginalCount !== 0 ||
|
|
viewer.nativeOriginalPath !== "" ||
|
|
viewer.nativeOriginalVisible > .01 ||
|
|
viewer.nativeOriginalPixels.some((size) => size !== 0) ||
|
|
viewer.displaySource !== "viewport-bitmap" ||
|
|
viewer.gestureRenderer !== "raf-dom" ||
|
|
viewer.originalPolicy !== "zoom-only" ||
|
|
viewer.mediaWillChange !== "auto" ||
|
|
!viewer.pinchZoomed ||
|
|
viewer.pinchScale < 1.5 ||
|
|
viewer.pinchNativeOriginal?.active !== "true" ||
|
|
viewer.pinchNativeOriginal.displaySource !== "original" ||
|
|
viewer.pinchNativeOriginal.visible < .99 ||
|
|
viewer.pinchNativeOriginal.settledWillChange !== "auto" ||
|
|
!viewer.doubleTapReset ||
|
|
Math.abs(viewer.doubleTapScale - 1) > 0.02 ||
|
|
viewer.nativeOriginalAfterReset?.active !== "false" ||
|
|
viewer.nativeOriginalAfterReset.displaySource !== "viewport-bitmap" ||
|
|
viewer.nativeOriginalAfterReset.visible > .01 ||
|
|
viewer.nativeOriginalAfterReset.settledWillChange !== "auto" ||
|
|
!viewer.swipeNavigation ||
|
|
viewer.swipeNavigation === viewer.keyboardNavigation ||
|
|
!viewer.shortSwipeNavigation?.changed ||
|
|
viewer.shortSwipeNavigation.distance !== 40 ||
|
|
viewer.swipeDragTracking > -190 ||
|
|
viewer.swipeDragTracking < -225 ||
|
|
viewer.swipeDragWillChange !== "transform" ||
|
|
viewer.swipeMotion?.mode !== "interruptible" ||
|
|
viewer.swipeMotion.direction !== "next" ||
|
|
viewer.swipeMotion.snapshot !== "prepared" ||
|
|
viewer.swipeMotion.releaseCostMs > 12 ||
|
|
viewer.swipeMotion.incomingAnimations < 1 ||
|
|
viewer.swipeMotion.outgoingAnimations < 1 ||
|
|
!viewer.swipeMotion.outgoingIgnoresInput ||
|
|
!viewer.swipeMotionCleaned ||
|
|
!viewer.reversedPageGestureStayedOpen ||
|
|
!viewer.pageGestureIgnoredHorizontal ||
|
|
!viewer.detailsFollowedTouch ||
|
|
!viewer.detailsReachedBySwipe ||
|
|
viewer.pageAfterDetails !== "details" ||
|
|
viewer.detailsName !== viewer.shortSwipeNavigation.imageName ||
|
|
viewer.detailsHeading !== "图片详情" ||
|
|
!viewer.stageScrolledAway ||
|
|
!viewer.detailsVisible ||
|
|
viewer.drawerArtifactsAfterScroll !== 0 ||
|
|
!viewer.returnedToImage ||
|
|
viewer.pageAfterReturn !== "image" ||
|
|
!viewer.closedByGesture
|
|
)) ||
|
|
viewer.closed !== 0 ||
|
|
!viewer.rootRestored ||
|
|
!viewer.returnAnchorSettled ||
|
|
!returnRestoration ||
|
|
returnRestoration.openedId === returnRestoration.targetId ||
|
|
returnRestoration.targetId !== returnRestoration.currentId ||
|
|
returnRestoration.cardsAfter <= returnRestoration.cardsBefore ||
|
|
!returnRestoration.visible ||
|
|
returnRestoration.anchorDelta > 2 ||
|
|
returnRestoration.returning !== "false" ||
|
|
searchDisclosure.expanded !== "true" ||
|
|
!searchDisclosure.focused ||
|
|
searchDisclosure.panelRole !== "group" ||
|
|
searchDisclosure.panelInert ||
|
|
(target.name === "desktop" && searchDisclosure.leadingGap < 6) ||
|
|
searchDisclosure.panelWidth < (target.name === "desktop" ? 250 : 200) ||
|
|
searchDisclosure.stationaryToggleDelta < 0 ||
|
|
searchDisclosure.stationaryToggleDelta > 0.5 ||
|
|
searchDisclosure.restored !== "false" ||
|
|
!searchDisclosure.panelInertClosed ||
|
|
!searchDisclosure.hasQueryClass ||
|
|
!searchDisclosure.queryClassCleared ||
|
|
searchDisclosure.panelTop - searchDisclosure.topbarBottom < 6 ||
|
|
exploration.firstSeed.length !== 32 ||
|
|
exploration.secondSeed.length !== 32 ||
|
|
exploration.firstSeed === exploration.secondSeed ||
|
|
exploration.exploreMarker !== "none" ||
|
|
exploration.searchMarker !== "none" ||
|
|
homeNavigation.href !== "/" ||
|
|
homeNavigation.galleryPath !== "/" ||
|
|
!homeNavigation.allImagesActive ||
|
|
homeNavigation.searchValue !== "" ||
|
|
layout.topbarSearches !== 1 ||
|
|
layout.brandHomeLinks !== 1 ||
|
|
layout.gallerySidebarToggles !== 1 ||
|
|
layout.galleryToggleOpacity < 0.4 ||
|
|
layout.galleryToggleOpacity > 0.75 ||
|
|
layout.topbarSidebarToggles !== 0 ||
|
|
layout.contentSearches !== 0 ||
|
|
layout.contentInfoRows !== 0 ||
|
|
layout.sortControls !== 0 ||
|
|
layout.topbarSearchRightGap < 0 ||
|
|
layout.topbarSearchRightGap > 20 ||
|
|
layout.exploreButtons !== 1 ||
|
|
layout.exploreSearchGap < 4 ||
|
|
layout.exploreSearchGap > 8 ||
|
|
layout.sidebarStatuses !== 1 ||
|
|
layout.sidebarGalleryMetas !== 1 ||
|
|
(target.name === "desktop" && (
|
|
!resizeStability ||
|
|
!resizeStability.cardsPreserved ||
|
|
!resizeStability.loadedCardsPreserved ||
|
|
resizeStability.skeletonSeen ||
|
|
resizeStability.imagePageRequests !== 0 ||
|
|
resizeStability.brokenVisibleImages !== 0 ||
|
|
resizeStability.columns !== 5 ||
|
|
!resizeStability.anchorFocused ||
|
|
resizeStability.anchorDeltas.length !== 4 ||
|
|
resizeStability.anchorDeltas.some((delta) => !Number.isFinite(delta) || delta > 2)
|
|
)) ||
|
|
!layout.galleryPath.startsWith("/") ||
|
|
!layout.galleryCount.endsWith("张图片") ||
|
|
layout.galleryMetaRowDelta < 0 ||
|
|
layout.galleryMetaRowDelta > 2 ||
|
|
(target.name === "desktop" && (
|
|
!desktopSidebar ||
|
|
desktopSidebar.expanded !== "false" ||
|
|
desktopSidebar.visibility !== "visible" ||
|
|
desktopSidebar.navigationVisibility !== "hidden" ||
|
|
desktopSidebar.statusVisibility !== "hidden" ||
|
|
desktopSidebar.metaVisibility !== "hidden" ||
|
|
desktopSidebar.ariaHidden !== "true" ||
|
|
desktopSidebar.stored !== "true" ||
|
|
desktopSidebar.columns !== 6 ||
|
|
desktopSidebar.redundantHeadings !== 0 ||
|
|
desktopSidebar.pathDetails !== 0 ||
|
|
desktopSidebar.progressPanels !== 0 ||
|
|
desktopSidebar.inlineToggles !== 0 ||
|
|
desktopSidebar.galleryToggles !== 1 ||
|
|
Math.abs(desktopSidebar.expandedToggleWidth - (desktopSidebar.contentBefore - 20)) > 1 ||
|
|
Math.abs(desktopSidebar.expandedToggleHeight - 36) > 1 ||
|
|
Math.abs(desktopSidebar.collapsedToggleWidth - 40) > 1 ||
|
|
Math.abs(desktopSidebar.collapsedToggleHeight - 36) > 1 ||
|
|
desktopSidebar.albumClearance < 8 ||
|
|
desktopSidebar.toggleControlInsetX < 8 ||
|
|
desktopSidebar.toggleControlInsetX > 12 ||
|
|
desktopSidebar.toggleControlInsetY < 4 ||
|
|
desktopSidebar.toggleControlInsetY > 8 ||
|
|
desktopSidebar.stationaryToggleDelta < 0 ||
|
|
desktopSidebar.stationaryToggleDelta > 1 ||
|
|
!desktopSidebar.metaIsBelowAlbums ||
|
|
desktopSidebar.expandedGalleryOverlap ||
|
|
!desktopSidebar.galleryOverlap ||
|
|
Math.abs(desktopSidebar.sidebarRight) > 1 ||
|
|
desktopSidebar.contentBefore < 200 ||
|
|
Math.abs(desktopSidebar.contentCollapsed) > 1 ||
|
|
desktopSidebar.restoredToggleDelta < 0 ||
|
|
desktopSidebar.restoredToggleDelta > 1 ||
|
|
layout.sidebarMetaBottomGap < 0 ||
|
|
layout.sidebarMetaBottomGap > 1 ||
|
|
Math.abs(layout.sidebarMetaStatusGap) > 1 ||
|
|
desktopSidebar.restored !== "true"
|
|
)) ||
|
|
(target.name === "mobile" && (
|
|
layout.topbarPosition !== "fixed" ||
|
|
Math.abs(layout.topbarTop) > 1 ||
|
|
Math.abs(searchDisclosure.compensatedTopbarDelta - 24) > 1 ||
|
|
Math.abs(searchDisclosure.compensatedPanelDelta - 24) > 1 ||
|
|
mobileNavigation?.opened !== 1 ||
|
|
Math.abs(mobileNavigation.fixedTop) > 1 ||
|
|
mobileNavigation.expanded !== "true" ||
|
|
mobileNavigation.internalToggles !== 0 ||
|
|
mobileNavigation.metaRows !== 1 ||
|
|
!mobileNavigation.galleryPath?.startsWith("/") ||
|
|
!mobileNavigation.galleryCount?.endsWith("张图片") ||
|
|
Math.abs(mobileNavigation.layerTopGap) > 1 ||
|
|
Math.abs(mobileNavigation.toggleWidth - 40) > 1 ||
|
|
Math.abs(mobileNavigation.toggleHeight - 36) > 1 ||
|
|
Math.abs(mobileNavigation.expandedToggleWidth - (mobileNavigation.drawerWidth - 20)) > 1 ||
|
|
!mobileNavigation.galleryOverlap ||
|
|
mobileNavigation.stationaryToggleDelta < 0 ||
|
|
mobileNavigation.stationaryToggleDelta > 1 ||
|
|
mobileNavigation.toggleControlInsetX < 8 ||
|
|
mobileNavigation.toggleControlInsetX > 12 ||
|
|
mobileNavigation.toggleControlInsetY < 4 ||
|
|
mobileNavigation.toggleControlInsetY > 8 ||
|
|
mobileNavigation.metaBottomGap < 0 ||
|
|
mobileNavigation.metaBottomGap > 1 ||
|
|
Math.abs(mobileNavigation.metaStatusGap) > 1 ||
|
|
mobileNavigation.drawerWidth < 200 ||
|
|
mobileNavigation.closed !== 0 ||
|
|
mobileNavigation.restored !== "false" ||
|
|
mobileCardNames?.shownOnContact !== "true" ||
|
|
mobileCardNames.retainedOnRelease !== "true" ||
|
|
mobileCardNames.retainedWithoutTimeout !== "true" ||
|
|
mobileCardNames.switchedToSecond !== "true" ||
|
|
mobileCardNames.firstHidden !== "false" ||
|
|
mobileCardNames.activeCount !== 1 ||
|
|
mobileCardNames.displayedCount !== 1 ||
|
|
mobileCardNames.clickCount !== 1 ||
|
|
mobileCardNames.viewerOpenedFromTouch !== 1 ||
|
|
layout.cardNameDisplay === "none" ||
|
|
layout.cardNameOpacity > 0.01 ||
|
|
layout.cardNameWhiteSpace !== "nowrap"
|
|
)) ||
|
|
(target.name === "desktop" && layout.topbarPosition !== "fixed") ||
|
|
browserErrors.length
|
|
) {
|
|
throw new Error(`visual check failed: ${JSON.stringify(result)}`);
|
|
}
|
|
await page.close();
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
console.log(JSON.stringify(results, null, 2));
|