fix: refine heaven readings and heart breathing
This commit is contained in:
+11
-10
@@ -10,6 +10,9 @@ const {
|
|||||||
todayString,
|
todayString,
|
||||||
} = window.XiaobaiUI;
|
} = window.XiaobaiUI;
|
||||||
|
|
||||||
|
const HEART_BREATH_TOTAL_MS = 40_000;
|
||||||
|
const HEART_BREATH_PHASE_MS = 4_000;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
user: null,
|
user: null,
|
||||||
csrfToken: "",
|
csrfToken: "",
|
||||||
@@ -81,7 +84,7 @@ const state = {
|
|||||||
heavenReadingError: "",
|
heavenReadingError: "",
|
||||||
heartStage: "intro",
|
heartStage: "intro",
|
||||||
heartTimer: null,
|
heartTimer: null,
|
||||||
heartSeconds: 30,
|
heartSeconds: HEART_BREATH_TOTAL_MS / 1000,
|
||||||
heartBreathingEndsAt: 0,
|
heartBreathingEndsAt: 0,
|
||||||
heartLines: [],
|
heartLines: [],
|
||||||
heartThrows: [],
|
heartThrows: [],
|
||||||
@@ -3791,11 +3794,11 @@ function waitForHeartMotion(duration, token = state.heartStageToken) {
|
|||||||
|
|
||||||
async function startHeartBreathing() {
|
async function startHeartBreathing() {
|
||||||
if (state.heartTimer) clearInterval(state.heartTimer);
|
if (state.heartTimer) clearInterval(state.heartTimer);
|
||||||
state.heartSeconds = 30;
|
state.heartSeconds = HEART_BREATH_TOTAL_MS / 1000;
|
||||||
state.heartBreathingEndsAt = 0;
|
state.heartBreathingEndsAt = 0;
|
||||||
document.querySelector("#beginCastingButton")?.classList.remove("is-ready");
|
document.querySelector("#beginCastingButton")?.classList.remove("is-ready");
|
||||||
if (!await transitionHeartStage("breathing")) return;
|
if (!await transitionHeartStage("breathing")) return;
|
||||||
state.heartBreathingEndsAt = Date.now() + 30_000;
|
state.heartBreathingEndsAt = Date.now() + HEART_BREATH_TOTAL_MS;
|
||||||
const ember = document.querySelector("#heartIncenseEmber");
|
const ember = document.querySelector("#heartIncenseEmber");
|
||||||
ember?.classList.remove("is-burning");
|
ember?.classList.remove("is-burning");
|
||||||
if (ember) void ember.offsetWidth;
|
if (ember) void ember.offsetWidth;
|
||||||
@@ -3821,22 +3824,20 @@ function finishHeartBreathing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateBreathingDisplay() {
|
function updateBreathingDisplay() {
|
||||||
setText("breathingSeconds", Math.max(0, state.heartSeconds));
|
|
||||||
const remainingMs = state.heartBreathingEndsAt
|
const remainingMs = state.heartBreathingEndsAt
|
||||||
? Math.max(0, state.heartBreathingEndsAt - Date.now())
|
? Math.max(0, state.heartBreathingEndsAt - Date.now())
|
||||||
: Math.max(0, state.heartSeconds * 1000);
|
: Math.max(0, state.heartSeconds * 1000);
|
||||||
const elapsedMs = 30_000 - remainingMs;
|
const elapsedMs = HEART_BREATH_TOTAL_MS - remainingMs;
|
||||||
const inhaling = Math.floor(elapsedMs / 4000) % 2 === 0;
|
const inhaling = Math.floor(elapsedMs / HEART_BREATH_PHASE_MS) % 2 === 0;
|
||||||
const phase = state.heartSeconds <= 0 ? "settled" : inhaling ? "inhale" : "exhale";
|
const phase = state.heartSeconds <= 0 ? "settled" : inhaling ? "inhale" : "exhale";
|
||||||
const scene = document.querySelector("#breathingScene");
|
const scene = document.querySelector("#breathingScene");
|
||||||
scene.dataset.phase = phase;
|
scene.dataset.phase = phase;
|
||||||
setText("breathingPhase", phase === "settled" ? "已静" : inhaling ? "吸气" : "呼气");
|
setText("breathingPhase", phase === "settled" ? "已静" : inhaling ? "吸气" : "呼气");
|
||||||
document.querySelector("#breathingProgress").style.width = `${clamp(elapsedMs / 300, 0, 100)}%`;
|
|
||||||
const prompt = state.heartSeconds <= 0
|
const prompt = state.heartSeconds <= 0
|
||||||
? "静心已成,可以起卦"
|
? "静心已成,可以起卦"
|
||||||
: state.heartSeconds > 20
|
: elapsedMs < 16_000
|
||||||
? inhaling ? "缓慢吸气,放下对答案的预设" : "缓慢呼气,让预设随之松开"
|
? inhaling ? "缓慢吸气,放下对答案的预设" : "缓慢呼气,让预设随之松开"
|
||||||
: state.heartSeconds > 10
|
: elapsedMs < 32_000
|
||||||
? inhaling ? "吸气,只留下真正想问的事" : "呼气,不急着寻找答案"
|
? inhaling ? "吸气,只留下真正想问的事" : "呼气,不急着寻找答案"
|
||||||
: inhaling ? "吸气,让心停在此刻" : "呼气,不追逐经过的念头";
|
: inhaling ? "吸气,让心停在此刻" : "呼气,不追逐经过的念头";
|
||||||
setText("breathingPrompt", prompt);
|
setText("breathingPrompt", prompt);
|
||||||
@@ -4185,7 +4186,7 @@ function resetHeartCoins() {
|
|||||||
async function resetHeartRitual() {
|
async function resetHeartRitual() {
|
||||||
if (state.heartTimer) clearInterval(state.heartTimer);
|
if (state.heartTimer) clearInterval(state.heartTimer);
|
||||||
state.heartTimer = null;
|
state.heartTimer = null;
|
||||||
state.heartSeconds = 30;
|
state.heartSeconds = HEART_BREATH_TOTAL_MS / 1000;
|
||||||
state.heartBreathingEndsAt = 0;
|
state.heartBreathingEndsAt = 0;
|
||||||
state.heartLines = [];
|
state.heartLines = [];
|
||||||
state.heartThrows = [];
|
state.heartThrows = [];
|
||||||
|
|||||||
+2
-6
@@ -598,15 +598,11 @@
|
|||||||
<div class="heart-stage-inner breathing-stage">
|
<div class="heart-stage-inner breathing-stage">
|
||||||
<span class="heart-stage-index">观心 · 二</span>
|
<span class="heart-stage-index">观心 · 二</span>
|
||||||
<div id="breathingScene" class="breathing-scene" data-phase="inhale">
|
<div id="breathingScene" class="breathing-scene" data-phase="inhale">
|
||||||
<span class="breathing-ring ring-outer" aria-hidden="true"></span>
|
<div class="heart-breath-enso" aria-hidden="true"><i></i></div>
|
||||||
<span class="breathing-ring ring-inner" aria-hidden="true"></span>
|
<b id="breathingPhase" class="breathing-phase" role="status" aria-live="polite">吸气</b>
|
||||||
<div class="heart-breath-flame" aria-hidden="true"><i></i></div>
|
|
||||||
<div class="breathing-orbit"><strong id="breathingSeconds">30</strong><span>秒</span></div>
|
|
||||||
<b id="breathingPhase" class="breathing-phase">吸气</b>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 id="breathingPrompt">缓慢吸气,放下对答案的预设</h3>
|
<h3 id="breathingPrompt">缓慢吸气,放下对答案的预设</h3>
|
||||||
<div class="heart-incense" aria-hidden="true"><i id="heartIncenseEmber"></i></div>
|
<div class="heart-incense" aria-hidden="true"><i id="heartIncenseEmber"></i></div>
|
||||||
<div class="breathing-progress" aria-hidden="true"><i id="breathingProgress"></i></div>
|
|
||||||
<button id="beginCastingButton" class="button primary" type="button" disabled>静心完成,开始起卦</button>
|
<button id="beginCastingButton" class="button primary" type="button" disabled>静心完成,开始起卦</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+116
-6
@@ -10392,7 +10392,7 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
box-shadow: 0 0 14px 4px rgba(255, 180, 90, 0.35);
|
box-shadow: 0 0 14px 4px rgba(255, 180, 90, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heart-incense i.is-burning { animation: heart-incense-burn 30s linear forwards; }
|
.heart-incense i.is-burning { animation: heart-incense-burn 40s linear forwards; }
|
||||||
|
|
||||||
#heavenHeartPanel #beginCastingButton {
|
#heavenHeartPanel #beginCastingButton {
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
@@ -10787,6 +10787,9 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
animation-duration: 1ms !important;
|
animation-duration: 1ms !important;
|
||||||
transition-duration: 1ms !important;
|
transition-duration: 1ms !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .heart-breath-enso,
|
||||||
|
#heavenHeartPanel .breathing-phase { transition: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared paper mode: every heart stage uses the same surface as trend and fortune. */
|
/* Shared paper mode: every heart stage uses the same surface as trend and fortune. */
|
||||||
@@ -10979,6 +10982,92 @@ body.sidebar-collapsed .sidebar-collapse-button .lucide {
|
|||||||
opacity: 0.78;
|
opacity: 0.78;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fixed ink enso: breath is expressed through tone and light, never geometry. */
|
||||||
|
#heavenHeartPanel .heart-breath-enso {
|
||||||
|
width: 176px;
|
||||||
|
height: 176px;
|
||||||
|
position: absolute;
|
||||||
|
top: 43%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
color: rgba(111, 76, 54, 0.7);
|
||||||
|
opacity: 0.74;
|
||||||
|
filter: drop-shadow(0 0 5px rgba(154, 91, 69, 0.06));
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: color 3.8s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 3.8s cubic-bezier(0.22, 0.61, 0.36, 1), filter 3.8s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .heart-breath-enso::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 12px;
|
||||||
|
border: 7px solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-bottom-color: rgba(111, 76, 54, 0.28);
|
||||||
|
border-radius: 49% 51% 47% 53% / 52% 48% 54% 46%;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(253, 252, 248, 0.76);
|
||||||
|
transform: rotate(-26deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .heart-breath-enso::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 31px;
|
||||||
|
border: 1px solid rgba(139, 112, 77, 0.13);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .heart-breath-enso > i {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
right: 31px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
box-shadow: 0 0 11px rgba(154, 91, 69, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .breathing-scene[data-phase="inhale"] .heart-breath-enso {
|
||||||
|
color: rgba(126, 77, 52, 0.86);
|
||||||
|
opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 13px rgba(194, 159, 90, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .breathing-scene[data-phase="exhale"] .heart-breath-enso {
|
||||||
|
color: rgba(91, 99, 91, 0.48);
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: drop-shadow(0 0 3px rgba(91, 99, 91, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .breathing-scene[data-phase="settled"] .heart-breath-enso {
|
||||||
|
color: rgba(111, 76, 54, 0.64);
|
||||||
|
opacity: 0.72;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .breathing-phase {
|
||||||
|
top: 43%;
|
||||||
|
bottom: auto;
|
||||||
|
z-index: 3;
|
||||||
|
color: #76513d;
|
||||||
|
font-family: var(--heaven-serif);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: color 3.8s ease, opacity 3.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .breathing-scene[data-phase="exhale"] .breathing-phase {
|
||||||
|
color: #6f756e;
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
#heavenHeartPanel .breathing-scene[data-phase="settled"] .breathing-phase {
|
||||||
|
color: #76513d;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Account identity and membership access states */
|
/* Account identity and membership access states */
|
||||||
.account-menu-shell { position: relative; display: inline-flex; align-items: center; gap: 6px; }
|
.account-menu-shell { position: relative; display: inline-flex; align-items: center; gap: 6px; }
|
||||||
.account-menu-chevron { width: 13px !important; height: 13px !important; margin-left: 1px; transition: transform 180ms var(--ease-out); }
|
.account-menu-chevron { width: 13px !important; height: 13px !important; margin-left: 1px; transition: transform 180ms var(--ease-out); }
|
||||||
@@ -11666,10 +11755,13 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin
|
|||||||
|
|
||||||
.heaven-reading-dialog {
|
.heaven-reading-dialog {
|
||||||
width: min(920px, calc(100vw - 32px));
|
width: min(920px, calc(100vw - 32px));
|
||||||
|
height: min(820px, calc(100dvh - 32px));
|
||||||
max-height: min(820px, calc(100dvh - 32px));
|
max-height: min(820px, calc(100dvh - 32px));
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fdfcf8;
|
background: #fdfcf8;
|
||||||
}
|
}
|
||||||
|
.heaven-reading-dialog[open] { display: grid; }
|
||||||
.heaven-reading-tabs {
|
.heaven-reading-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -11689,7 +11781,25 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin
|
|||||||
}
|
}
|
||||||
.heaven-reading-tabs button.active { border-bottom-color: #9a5b45; color: #6f3f31; font-weight: 700; }
|
.heaven-reading-tabs button.active { border-bottom-color: #9a5b45; color: #6f3f31; font-weight: 700; }
|
||||||
.heaven-reading-tabs button:focus-visible { outline: 2px solid #9a5b45; outline-offset: 1px; }
|
.heaven-reading-tabs button:focus-visible { outline: 2px solid #9a5b45; outline-offset: 1px; }
|
||||||
.heaven-reading-current { min-height: 430px; max-height: calc(100dvh - 170px); overflow-y: auto; padding: 24px 28px 30px; }
|
.heaven-reading-current {
|
||||||
|
min-height: 0;
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px 28px 30px;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
.heaven-reading-current::-webkit-scrollbar,
|
||||||
|
.heaven-reading-history-detail::-webkit-scrollbar,
|
||||||
|
.heaven-reading-history-list-wrap::-webkit-scrollbar { width: 9px; }
|
||||||
|
.heaven-reading-current::-webkit-scrollbar-thumb,
|
||||||
|
.heaven-reading-history-detail::-webkit-scrollbar-thumb,
|
||||||
|
.heaven-reading-history-list-wrap::-webkit-scrollbar-thumb {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(112, 94, 72, .34);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
.heaven-reading-loading { min-height: 360px; display: grid; place-content: center; justify-items: center; gap: 10px; text-align: center; }
|
.heaven-reading-loading { min-height: 360px; display: grid; place-content: center; justify-items: center; gap: 10px; text-align: center; }
|
||||||
.heaven-reading-loading[hidden] { display: none; }
|
.heaven-reading-loading[hidden] { display: none; }
|
||||||
.heaven-reading-mark {
|
.heaven-reading-mark {
|
||||||
@@ -11719,7 +11829,7 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin
|
|||||||
.heaven-reading-history-detail > p { margin: 12px 0 0; color: var(--text-secondary); font-size: 12px; }
|
.heaven-reading-history-detail > p { margin: 12px 0 0; color: var(--text-secondary); font-size: 12px; }
|
||||||
.heaven-reading-answer { margin-top: 24px; color: #303833; font-family: var(--heaven-serif); font-size: 15px; line-height: 1.95; }
|
.heaven-reading-answer { margin-top: 24px; color: #303833; font-family: var(--heaven-serif); font-size: 15px; line-height: 1.95; }
|
||||||
.heaven-reading-answer .mentor-answer-heading { color: #713f31; }
|
.heaven-reading-answer .mentor-answer-heading { color: #713f31; }
|
||||||
.heaven-reading-history { min-height: 500px; max-height: calc(100dvh - 170px); display: grid; grid-template-columns: 270px minmax(0, 1fr); overflow: hidden; }
|
.heaven-reading-history { min-height: 0; max-height: none; display: grid; grid-template-columns: 270px minmax(0, 1fr); overflow: hidden; }
|
||||||
.heaven-reading-history[hidden] { display: none; }
|
.heaven-reading-history[hidden] { display: none; }
|
||||||
.heaven-reading-history-list-wrap { min-width: 0; overflow-y: auto; border-right: 1px solid var(--border); background: rgba(246, 244, 237, .72); }
|
.heaven-reading-history-list-wrap { min-width: 0; overflow-y: auto; border-right: 1px solid var(--border); background: rgba(246, 244, 237, .72); }
|
||||||
.heaven-reading-history-heading { display: flex; justify-content: space-between; gap: 10px; padding: 15px 16px 10px; color: var(--text-secondary); font-size: 11px; }
|
.heaven-reading-history-heading { display: flex; justify-content: space-between; gap: 10px; padding: 15px 16px 10px; color: var(--text-secondary); font-size: 11px; }
|
||||||
@@ -11741,9 +11851,9 @@ button.account-role-badge:focus-visible { outline: 2px solid var(--blue); outlin
|
|||||||
.heart-toolbar-controls .heart-history-button { width: 36px; min-width: 36px; min-height: 36px; padding: 0; }
|
.heart-toolbar-controls .heart-history-button { width: 36px; min-width: 36px; min-height: 36px; padding: 0; }
|
||||||
.heart-toolbar-controls span { display: none; }
|
.heart-toolbar-controls span { display: none; }
|
||||||
.heart-read-actions { flex-wrap: wrap; justify-content: flex-end; }
|
.heart-read-actions { flex-wrap: wrap; justify-content: flex-end; }
|
||||||
.heaven-reading-dialog { width: calc(100vw - 16px); max-height: calc(100dvh - 16px); margin: 8px auto; }
|
.heaven-reading-dialog { width: calc(100vw - 16px); height: calc(100dvh - 16px); max-height: calc(100dvh - 16px); margin: 8px auto; }
|
||||||
.heaven-reading-current { min-height: 360px; max-height: calc(100dvh - 150px); padding: 20px 17px 24px; }
|
.heaven-reading-current { min-height: 0; max-height: none; padding: 20px 17px 24px; }
|
||||||
.heaven-reading-history { min-height: 0; max-height: calc(100dvh - 150px); grid-template-columns: 1fr; overflow-y: auto; }
|
.heaven-reading-history { min-height: 0; max-height: none; grid-template-columns: 1fr; overflow-y: auto; }
|
||||||
.heaven-reading-history-list-wrap { max-height: 210px; border-right: 0; border-bottom: 1px solid var(--border); }
|
.heaven-reading-history-list-wrap { max-height: 210px; border-right: 0; border-bottom: 1px solid var(--border); }
|
||||||
.heaven-reading-history-detail { overflow: visible; padding: 20px 17px 24px; }
|
.heaven-reading-history-detail { overflow: visible; padding: 20px 17px 24px; }
|
||||||
.heaven-reading-result > header,
|
.heaven-reading-result > header,
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ test("saved daily fortune opens in the reading dialog without regenerating", asy
|
|||||||
context_date: "20260723",
|
context_date: "20260723",
|
||||||
subject: "2026-07-23 观气",
|
subject: "2026-07-23 观气",
|
||||||
subject_detail: "丙午年 · 乙未月 · 己丑日 · 土气偏显",
|
subject_detail: "丙午年 · 乙未月 · 己丑日 · 土气偏显",
|
||||||
answer: "三层气机已经合参,今日宜先定节奏,再看行动。",
|
answer: Array.from({ length: 36 }, (_, index) => `第${index + 1}节:三层气机已经合参,今日宜先定节奏,再看行动。`).join("\n\n"),
|
||||||
created_at: "2026-07-23T09:12:00+08:00",
|
created_at: "2026-07-23T09:12:00+08:00",
|
||||||
};
|
};
|
||||||
updateHeavenInterpretationControls();
|
updateHeavenInterpretationControls();
|
||||||
@@ -326,11 +326,51 @@ test("saved daily fortune opens in the reading dialog without regenerating", asy
|
|||||||
await page.locator("#interpretFortuneButton").click();
|
await page.locator("#interpretFortuneButton").click();
|
||||||
await expect(page.locator("#heavenReadingDialog")).toBeVisible();
|
await expect(page.locator("#heavenReadingDialog")).toBeVisible();
|
||||||
await expect(page.locator("#heavenReadingAnswer")).toContainText("今日宜先定节奏");
|
await expect(page.locator("#heavenReadingAnswer")).toContainText("今日宜先定节奏");
|
||||||
|
const readingScroll = await page.locator("#heavenReadingCurrent").evaluate((element) => ({
|
||||||
|
clientHeight: element.clientHeight,
|
||||||
|
scrollHeight: element.scrollHeight,
|
||||||
|
overflowY: getComputedStyle(element).overflowY,
|
||||||
|
}));
|
||||||
|
expect(readingScroll.scrollHeight).toBeGreaterThan(readingScroll.clientHeight);
|
||||||
|
expect(readingScroll.overflowY).toBe("auto");
|
||||||
|
await page.locator("#heavenReadingCurrent").evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||||
|
expect(await page.locator("#heavenReadingCurrent").evaluate((element) => element.scrollTop)).toBeGreaterThan(0);
|
||||||
expect(interpretRequests).toHaveLength(0);
|
expect(interpretRequests).toHaveLength(0);
|
||||||
await page.locator('[data-heaven-reading-tab="history"]').click();
|
await page.locator('[data-heaven-reading-tab="history"]').click();
|
||||||
await expect(page.locator("#heavenReadingHistoryList .heaven-reading-history-item")).toHaveCount(1);
|
await expect(page.locator("#heavenReadingHistoryList .heaven-reading-history-item")).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("heart breathing uses five silent eight-second cycles with a fixed enso", async ({ page }) => {
|
||||||
|
await mockApplication(page, session("user", true));
|
||||||
|
await page.goto("/index.html");
|
||||||
|
await page.locator('[data-view="heavenView"]').first().click();
|
||||||
|
await page.locator('[data-heaven-panel="heart"]').click();
|
||||||
|
await page.evaluate(() => startHeartBreathing());
|
||||||
|
|
||||||
|
const timing = await page.evaluate(() => ({
|
||||||
|
remaining: state.heartBreathingEndsAt - Date.now(),
|
||||||
|
incenseDuration: getComputedStyle(document.querySelector("#heartIncenseEmber")).animationDuration,
|
||||||
|
inhaleTransform: getComputedStyle(document.querySelector(".heart-breath-enso")).transform,
|
||||||
|
}));
|
||||||
|
expect(timing.remaining).toBeGreaterThan(39_000);
|
||||||
|
expect(timing.remaining).toBeLessThanOrEqual(40_000);
|
||||||
|
expect(timing.incenseDuration).toBe("40s");
|
||||||
|
await expect(page.locator("#breathingPhase")).toHaveText("吸气");
|
||||||
|
await expect(page.locator("#breathingSeconds, #breathingProgress, .breathing-orbit")).toHaveCount(0);
|
||||||
|
|
||||||
|
const exhaleTransform = await page.evaluate(() => {
|
||||||
|
clearInterval(state.heartTimer);
|
||||||
|
state.heartTimer = null;
|
||||||
|
state.heartSeconds = 35;
|
||||||
|
state.heartBreathingEndsAt = Date.now() + 35_000;
|
||||||
|
updateBreathingDisplay();
|
||||||
|
return getComputedStyle(document.querySelector(".heart-breath-enso")).transform;
|
||||||
|
});
|
||||||
|
await expect(page.locator("#breathingScene")).toHaveAttribute("data-phase", "exhale");
|
||||||
|
await expect(page.locator("#breathingPhase")).toHaveText("呼气");
|
||||||
|
expect(exhaleTransform).toBe(timing.inhaleTransform);
|
||||||
|
});
|
||||||
|
|
||||||
test("mobile shell stays within the viewport", async ({ page }) => {
|
test("mobile shell stays within the viewport", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 375, height: 812 });
|
await page.setViewportSize({ width: 375, height: 812 });
|
||||||
await mockApplication(page, session("user", true));
|
await mockApplication(page, session("user", true));
|
||||||
|
|||||||
Reference in New Issue
Block a user