247 lines
7.7 KiB
JavaScript
247 lines
7.7 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const DEFAULT_TIMEOUT = 10000;
|
|
const DEFAULT_INTERVAL = 50;
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => {
|
|
window.setTimeout(resolve, ms);
|
|
});
|
|
}
|
|
|
|
async function waitFor(predicate, options = {}) {
|
|
const timeout = Number.isFinite(Number(options.timeout)) ? Number(options.timeout) : DEFAULT_TIMEOUT;
|
|
const interval = Number.isFinite(Number(options.interval)) ? Number(options.interval) : DEFAULT_INTERVAL;
|
|
const deadline = Date.now() + timeout;
|
|
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
if (await predicate()) {
|
|
return true;
|
|
}
|
|
} catch {
|
|
}
|
|
|
|
await sleep(interval);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function textById(id) {
|
|
const element = document.getElementById(id);
|
|
return element instanceof HTMLElement ? String(element.textContent || "").trim() : "";
|
|
}
|
|
|
|
function getSectionElement(selector) {
|
|
const element = document.querySelector(selector);
|
|
return element instanceof HTMLElement ? element : null;
|
|
}
|
|
|
|
function getLayoutState(selector) {
|
|
const layout = getSectionElement(selector);
|
|
const classes = layout ? Array.from(layout.classList) : [];
|
|
return {
|
|
classes,
|
|
detailOnly: classes.includes("layout-sidebar-collapsed") && !classes.includes("layout-detail-collapsed")
|
|
};
|
|
}
|
|
|
|
function getSelectedText(sectionSelector) {
|
|
const section = getSectionElement(sectionSelector);
|
|
if (!section) {
|
|
return "";
|
|
}
|
|
|
|
const selected = section.querySelector('[aria-selected="true"]');
|
|
return selected instanceof HTMLElement ? String(selected.textContent || "").trim() : "";
|
|
}
|
|
|
|
function dispatchNavigationEvent(eventName, detail) {
|
|
document.dispatchEvent(new CustomEvent(eventName, {
|
|
detail,
|
|
bubbles: true
|
|
}));
|
|
}
|
|
|
|
function createSnapshot(test) {
|
|
const section = getSectionElement(test.sectionSelector);
|
|
const layoutState = getLayoutState(test.layoutSelector);
|
|
|
|
return {
|
|
sectionHidden: section ? section.hidden : true,
|
|
layoutClasses: layoutState.classes,
|
|
detailOnly: layoutState.detailOnly,
|
|
detailName: textById(test.detailNameId),
|
|
detailSub: test.detailSubId ? textById(test.detailSubId) : "",
|
|
detailBody: test.detailBodyId ? textById(test.detailBodyId) : "",
|
|
selectedText: getSelectedText(test.sectionSelector)
|
|
};
|
|
}
|
|
|
|
async function waitForAppReady() {
|
|
return waitFor(() => {
|
|
if (document.readyState !== "complete") {
|
|
return false;
|
|
}
|
|
|
|
const referenceData = window.TarotAppRuntime?.getReferenceData?.();
|
|
const magickDataset = window.TarotAppRuntime?.getMagickDataset?.();
|
|
|
|
return Boolean(
|
|
typeof window.TarotChromeUi?.showDetailOnly === "function"
|
|
&& referenceData
|
|
&& magickDataset
|
|
);
|
|
});
|
|
}
|
|
|
|
const TESTS = {
|
|
tarotTrumpNav: {
|
|
eventName: "nav:tarot-trump",
|
|
detail: { trumpNumber: 0 },
|
|
sectionSelector: "#tarot-section",
|
|
layoutSelector: "#tarot-browse-view .tarot-layout",
|
|
detailNameId: "tarot-detail-name",
|
|
matches: (state) => state.detailName === "The Fool"
|
|
},
|
|
kabViewTrump: {
|
|
eventName: "kab:view-trump",
|
|
detail: { trumpNumber: 0 },
|
|
sectionSelector: "#tarot-section",
|
|
layoutSelector: "#tarot-browse-view .tarot-layout",
|
|
detailNameId: "tarot-detail-name",
|
|
matches: (state) => state.detailName === "The Fool"
|
|
},
|
|
ichingHexagramNav: {
|
|
eventName: "nav:iching",
|
|
detail: { hexagramNumber: 1 },
|
|
sectionSelector: "#iching-section",
|
|
layoutSelector: "#iching-section .planet-layout",
|
|
detailNameId: "iching-detail-name",
|
|
matches: (state) => /Creative Force/i.test(state.detailName) && /#1/i.test(state.selectedText)
|
|
},
|
|
zodiacSignNav: {
|
|
eventName: "nav:zodiac",
|
|
detail: { signId: "aries" },
|
|
sectionSelector: "#zodiac-section",
|
|
layoutSelector: "#zodiac-section .planet-layout",
|
|
detailNameId: "zodiac-detail-name",
|
|
detailSubId: "zodiac-detail-sub",
|
|
matches: (state) => /Aries/i.test(state.detailSub) && /aries/i.test(state.selectedText)
|
|
},
|
|
planetNav: {
|
|
eventName: "nav:planet",
|
|
detail: { planetId: "mars" },
|
|
sectionSelector: "#planet-section",
|
|
layoutSelector: "#planet-section .planet-layout",
|
|
detailNameId: "planet-detail-name",
|
|
matches: (state) => state.detailName !== "--" && /mars/i.test(state.selectedText)
|
|
},
|
|
numberNav: {
|
|
eventName: "nav:number",
|
|
detail: { value: 1 },
|
|
sectionSelector: "#numbers-section",
|
|
layoutSelector: "#numbers-section .numbers-main-layout",
|
|
detailNameId: "numbers-detail-name",
|
|
matches: (state) => /Number 1|One/i.test(state.detailName) && /One/i.test(state.selectedText)
|
|
},
|
|
elementNav: {
|
|
eventName: "nav:elements",
|
|
detail: { elementId: "fire" },
|
|
sectionSelector: "#elements-section",
|
|
layoutSelector: "#elements-section .planet-layout",
|
|
detailNameId: "elements-detail-name",
|
|
matches: (state) => state.detailName !== "--" && /fire/i.test(state.selectedText)
|
|
},
|
|
calendarMonthNav: {
|
|
eventName: "nav:calendar-month",
|
|
detail: { calendarId: "gregorian", monthId: "january" },
|
|
sectionSelector: "#calendar-section",
|
|
layoutSelector: "#calendar-section .planet-layout",
|
|
detailNameId: "calendar-detail-name",
|
|
matches: (state) => /january/i.test(state.detailName) && /january/i.test(state.selectedText)
|
|
},
|
|
tarotViewKabPath: {
|
|
eventName: "tarot:view-kab-path",
|
|
detail: { pathNumber: 11 },
|
|
sectionSelector: "#kabbalah-tree-section",
|
|
layoutSelector: "#kabbalah-tree-section .kab-layout",
|
|
detailNameId: "kab-detail-name",
|
|
detailSubId: "kab-detail-sub",
|
|
detailBodyId: "kab-detail-body",
|
|
matches: (state) => /Path 11/i.test(state.detailName) && /The Fool/i.test(state.detailSub)
|
|
}
|
|
};
|
|
|
|
async function runTest(testId, options = {}) {
|
|
const test = TESTS[testId];
|
|
if (!test) {
|
|
throw new Error(`Unknown navigation detail test: ${testId}`);
|
|
}
|
|
|
|
const ready = await waitForAppReady();
|
|
if (!ready) {
|
|
return {
|
|
id: testId,
|
|
pass: false,
|
|
error: "App data did not finish loading in time."
|
|
};
|
|
}
|
|
|
|
dispatchNavigationEvent(test.eventName, test.detail);
|
|
|
|
const timeout = Number.isFinite(Number(options.timeout)) ? Number(options.timeout) : DEFAULT_TIMEOUT;
|
|
const pass = await waitFor(() => {
|
|
const state = createSnapshot(test);
|
|
return !state.sectionHidden && state.detailOnly && test.matches(state);
|
|
}, { timeout });
|
|
|
|
const state = createSnapshot(test);
|
|
return {
|
|
id: testId,
|
|
eventName: test.eventName,
|
|
detail: test.detail,
|
|
pass,
|
|
...state,
|
|
error: pass ? "" : "Destination detail pane did not settle into detail-only mode."
|
|
};
|
|
}
|
|
|
|
async function runAll(testIds = Object.keys(TESTS), options = {}) {
|
|
const results = [];
|
|
for (const testId of testIds) {
|
|
results.push(await runTest(testId, options));
|
|
}
|
|
|
|
const summary = {
|
|
total: results.length,
|
|
passed: results.filter((result) => result.pass).length,
|
|
failed: results.filter((result) => !result.pass).length
|
|
};
|
|
|
|
const payload = { summary, results };
|
|
window.__TAROT_NAVIGATION_DETAIL_TEST_RESULTS__ = payload;
|
|
return payload;
|
|
}
|
|
|
|
window.TarotNavigationDetailTestHarness = {
|
|
runAll,
|
|
runTest,
|
|
listTests: () => Object.keys(TESTS)
|
|
};
|
|
|
|
if (new URLSearchParams(window.location.search).has("navtest")) {
|
|
waitForAppReady().then((ready) => {
|
|
if (!ready) {
|
|
return;
|
|
}
|
|
|
|
runAll().then((result) => {
|
|
window.__TAROT_NAVIGATION_DETAIL_TEST_LAST_RESULT__ = result;
|
|
console.info("Navigation detail test harness results", result);
|
|
});
|
|
});
|
|
}
|
|
})(); |