启程动效只在启程里播,慢网时不再把内容藏起来(BUG-1447)。
清单已关不播。动画代码没到手或超过 300 毫秒,内容保持可见。 检查:onboarding-motion-gate、onboarding-motion-20261022、onboarding-hint-observer。 结果:10 通过,0 失败,退出码 0。 Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
846ed6cca6
commit
7fe335c7e0
@@ -19674,3 +19674,19 @@
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/ui-kit-onboarding-fix-20261026`。
|
||||
|
||||
## BUG-1447 | 启程动效对所有人播放,慢网时先把内容藏起来
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-10-26
|
||||
- 最近更新:2026-10-26
|
||||
- 影响面:已用过的账户也会看到追问进场、档案卡进场、星盘和大运画出、合盘头像靠拢。动效代码下载慢时,这些内容会先消失。
|
||||
- 用户现象:老用户打开页面也会播一遍新用户才有的动画。网络慢时,合盘报告或追问胶囊会一直空白,直到动画代码下载完。
|
||||
- 触发条件:动效监听挂在每个已登录会话上。播放前先把元素透明度设成 0,再去加载动画代码。
|
||||
- 根因:没有区分是不是还在启程里。藏内容发生在拿到动画函数之前。
|
||||
- 修复:清单已关就不播。清单还开着且有未完成项,或账户还没完成第一问,才播。先拿到动画函数再隐藏;加载失败或超过 300 毫秒还没到,就直接显示、不播。
|
||||
- 验证:`frontend/tests/onboarding-motion-gate.test.tsx`、`onboarding-motion-20261022.test.ts`、`onboarding-hint-observer.test.tsx` 共 10 通过、0 失败、退出码 0。清单已关时四处动画都没被调用。加载失败或超过 300 毫秒时,元素透明度不是 0。
|
||||
- 防复发:上面两组测试。
|
||||
- 相关记录:issue #111 验收 U4。#99。红线 4。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/ui-kit-onboarding-fix-20261026`。
|
||||
|
||||
|
||||
@@ -31,3 +31,11 @@
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 提示监听 | `node node_modules/tsx/dist/cli.mjs --test tests/onboarding-hint-observer.test.tsx tests/onboarding-checklist-20261022.test.ts` | 14 通过,0 失败,退出码 0 |
|
||||
|
||||
## U4 · 启程动效只在启程里播(BUG-1447)
|
||||
|
||||
清单已关不播。还在启程里才播。动画代码没到手或超过 300 毫秒,内容保持可见。
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 动效范围 | `node node_modules/tsx/dist/cli.mjs --test tests/onboarding-motion-gate.test.tsx tests/onboarding-motion-20261022.test.ts tests/onboarding-hint-observer.test.tsx` | 10 通过,0 失败,退出码 0 |
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
| `TASK-rectification-explicit-open-20261023.md` | `PROGRESS-rectification-explicit-open-20261023.md` | **明说要校正只回一句话(issue #103)**:找会话直接读一张对服务角色撤权的表、必然失败被吞(真正进行中的也续不上);`candidate_accepted` 被算作进行中挡住入口和提议卡;兜底只给一句话。产品选 C;只改入口口径、经 RPC 找会话、删兜底,不改共享状态集合、不动表。BUG-1423 | 已合入 staging | 分支 `codex/rectification-explicit-open-20261023` |
|
||||
| `TASK-chart-table-span-20261023.md` | `PROGRESS-chart-table-span-20261023.md` | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发。BUG-1422 | 已合入 staging | 分支 `codex/chart-table-span-20261023` |
|
||||
| `TASK-ui-kit-migration-20261023.md` | `PROGRESS-ui-kit-0-base-20261023.md`;`PROGRESS-ui-kit-1-shell-20261023.md`;`PROGRESS-ui-kit-2-chat-20261023.md`;`PROGRESS-ui-kit-3-chart-20261023.md`;`PROGRESS-ui-kit-4-people-20261023.md`;`PROGRESS-ui-kit-5-pages-20261023.md`;`PROGRESS-ui-kit-6-rectification-20261023.md`;`PROGRESS-ui-kit-7-cleanup-20261023.md` | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | K0–K7 已在测试环境,待验收 | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG |
|
||||
| `TASK-ui-kit-onboarding-fix-20261026.md` | `PROGRESS-ui-kit-onboarding-fix-20261026.md` | **组件库 K0~K7 与启程(二)~(四)事后验收**:桌面首屏后整页重挂、中英切换看不出选中、提示层每次 DOM 变化全页扫描、启程动效对所有人生效且会藏内容、清单数据只拉一次、拖动排序 N 个请求与部分失败、缺真库重读测试、首屏超 2%、依赖卫生、补回削弱的测试、启程(二)三处未做 | 执行中(#111,U1–U3 已提交) | `codex/ui-kit-onboarding-fix-20261026` |
|
||||
| `TASK-ui-kit-onboarding-fix-20261026.md` | `PROGRESS-ui-kit-onboarding-fix-20261026.md` | **组件库 K0~K7 与启程(二)~(四)事后验收**:桌面首屏后整页重挂、中英切换看不出选中、提示层每次 DOM 变化全页扫描、启程动效对所有人生效且会藏内容、清单数据只拉一次、拖动排序 N 个请求与部分失败、缺真库重读测试、首屏超 2%、依赖卫生、补回削弱的测试、启程(二)三处未做 | 执行中(#111,U1–U4 已提交) | `codex/ui-kit-onboarding-fix-20261026` |
|
||||
| —(门禁修复,无新任务书) | `PROGRESS-birth-journey-contract-20261025.md` | **测试分支过不了门禁,网站停在完整数据版(二)**:BUG-1439 出生时间合同;BUG-1440 测试画布;BUG-1441 侧栏流式检查等第一次补画结束再数帧。页面不改 | 已合入 staging,等门禁 | `codex/gate-birth-journey-contract-20261025` |
|
||||
| `TASK-onboarding-journey-20261022.md` | `PROGRESS-onboarding-1-data-20261022.md`;`PROGRESS-onboarding-2-intake-20261022.md`;`PROGRESS-onboarding-4-motion-20261022.md` | **新用户启程**:建档加「先看谁的盘」(自己 / 帮别人看,本人资料可后补)、人物关系字段、首问免费(服务端原子)、起步三问、启程清单(七项从数据推导,收费项标价,老用户可见)、八条一次性提示(含星盘四页签)、启程流程专用动效。产品 2026-10-22 拍板 D1~D6;动表经产品同意进循环,拆四个 issue 串行。BUG-1420 已被文档去重占用,本轮首问免费记 BUG-1421。(一)T1+T2 在 `codex/onboarding-1-data-20261022`。(二)T3+T4 记 BUG-1431,在 `codex/onboarding-2-intake-20261022`。(四)T7 在 `codex/onboarding-4-motion-20261022` | (一)(二)(三)已在 staging;(四)本分支待验收 | 原型 https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U |
|
||||
| `TASK-site-motion-20261022.md` | — | **全站动效**:换页按侧栏方向滑入、选中块跟随、页签、盘型切换星体移宫、换人、翻日期、列表补位、弹窗从触发处长出、抽屉、顶部提示、点数滚动、中英交叉淡入;重写 DESIGN §6(推翻 180 ms 上限与 Spatial Instant),不引动画库,先验证 View Transitions。产品 2026-10-22 拍板;三个 issue,排在启程(三)之后、启程(四)之前;**2026-10-23 被 ui-kit-migration 取代** | 已取代 | 原型 https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd |
|
||||
|
||||
+1
-1
@@ -1283,7 +1283,7 @@ Yogini 还在取时,右栏用和 Chara 相同的轨道等待(`OrbitWaiting`
|
||||
|
||||
一次性提示用现有弹出层,宽 `min(18rem, calc(100vw - 2rem))`,不抢焦点。侧栏两项从锚点右侧伸出,其余从上方伸出,间距 8px。按钮是「知道了」。同一时刻最多一条。天空过场、兑换窗、任何弹窗或抽屉、启程清单正打开、回答还在生成时,提示不出;挡着的那一条等条件过去再出。侧栏「星盘」、手机菜单和星盘四个页签上的小圆点是 8px,贴在右上角 4px,不是字符。
|
||||
|
||||
启程专用动效只在元素第一次出现时播放。重绘、来回切换不重播。除回答流式中的呼吸点外,没有无限循环。只动 `opacity`、`transform`、颜色、`clip-path`,不触发布局。Motion 用到才加载,不进首屏。系统开启「减少动态效果」时,星点和圆点闪烁去掉,其余只淡入,没有位移、缩放和擦除。
|
||||
启程专用动效只在启程里播放:清单还开着且有未完成项,或账户还没完成第一问。清单已经关掉的账户不播追问进场、档案卡进场、星盘和大运画出、合盘头像靠拢。动效代码还没到手、或 300ms 内没到手时,内容保持可见,不先藏起来。只在元素第一次出现时播放。重绘、来回切换不重播。除回答流式中的呼吸点外,没有无限循环。只动 `opacity`、`transform`、颜色、`clip-path`,不触发布局。Motion 用到才加载,不进首屏。系统开启「减少动态效果」时,星点和圆点闪烁去掉,其余只淡入,没有位移、缩放和擦除。
|
||||
|
||||
| 类别 | 用在哪 | 时长 | 曲线 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useOnboardingJourneyOptional } from "@/lib/onboarding-journey";
|
||||
import { mountOnboardingMotion } from "@/lib/motion/onboarding-run";
|
||||
|
||||
/** Listens for first-appearance beats. Motion itself loads on the first one. */
|
||||
/** Listens for first-appearance beats. Only during onboarding. Motion loads on the first one. */
|
||||
export function OnboardingMotionHost() {
|
||||
useEffect(() => mountOnboardingMotion(document), []);
|
||||
const enabled = useOnboardingJourneyOptional()?.motionEnabled === true;
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
return mountOnboardingMotion(document);
|
||||
}, [enabled]);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,38 @@ async function animator(): Promise<AnimateFn> {
|
||||
return animateFn;
|
||||
}
|
||||
|
||||
const MOTION_WAIT_MS = 300;
|
||||
let motionLoader: () => Promise<unknown> = () => animator();
|
||||
|
||||
/** Tests replace the loader. Production uses the real Motion import. */
|
||||
export function setOnboardingMotionLoader(next: (() => Promise<unknown>) | null): void {
|
||||
motionLoader = next ?? (() => animator());
|
||||
}
|
||||
|
||||
async function motionReady(): Promise<boolean> {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const attempt = motionLoader().then(() => true).catch(() => false);
|
||||
const expired = new Promise<boolean>((resolve) => {
|
||||
timer = setTimeout(() => resolve(false), MOTION_WAIT_MS);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([attempt, expired]);
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/** Hide only after Motion is in hand. A failed or slow load leaves the element visible. */
|
||||
async function conceal(node: HTMLElement): Promise<boolean> {
|
||||
const ready = await motionReady();
|
||||
if (!ready) {
|
||||
node.style.opacity = "";
|
||||
return false;
|
||||
}
|
||||
node.style.opacity = "0";
|
||||
return true;
|
||||
}
|
||||
|
||||
function easeOf(spec: OnboardingSpec): [number, number, number, number] {
|
||||
return [spec.ease[0], spec.ease[1], spec.ease[2], spec.ease[3]];
|
||||
}
|
||||
@@ -53,7 +85,7 @@ async function playEnter(node: HTMLElement): Promise<void> {
|
||||
if (!key || !claimOnboardingPlay(`enter:${key}`)) return;
|
||||
const spec = specFor("enter", indexOf(node));
|
||||
if (!spec) return;
|
||||
node.style.opacity = "0";
|
||||
if (!await conceal(node)) return;
|
||||
if (spec.y) node.style.transform = `translateY(${spec.y[0]}px)`;
|
||||
try {
|
||||
await run(node, spec, spec.y ? { opacity: [...spec.opacity!], y: [...spec.y] } : { opacity: [...spec.opacity!] });
|
||||
@@ -68,7 +100,7 @@ async function playPop(node: HTMLElement): Promise<void> {
|
||||
if (!key || !claimOnboardingPlay(`pop:${key}`)) return;
|
||||
const spec = specFor("pop");
|
||||
if (!spec) return;
|
||||
node.style.opacity = "0";
|
||||
if (!await conceal(node)) return;
|
||||
try {
|
||||
await run(node, spec, spec.scale
|
||||
? { opacity: [...spec.opacity!], scale: [...spec.scale] }
|
||||
@@ -92,7 +124,7 @@ async function playDraw(node: HTMLElement, kind: "intake" | "vedic" | "western"
|
||||
const spec = specFor("draw", indexOf(node), kind);
|
||||
if (!spec) return;
|
||||
if (!spec.clipPath) {
|
||||
node.style.opacity = "0";
|
||||
if (!await conceal(node)) return;
|
||||
try {
|
||||
await run(node, spec, { opacity: [0, 1] });
|
||||
} finally {
|
||||
@@ -222,7 +254,7 @@ async function playCelebrate(): Promise<void> {
|
||||
}
|
||||
|
||||
async function fadeIn(node: HTMLElement, spec: OnboardingSpec): Promise<void> {
|
||||
node.style.opacity = "0";
|
||||
if (!await conceal(node)) return;
|
||||
try {
|
||||
await run(node, spec, { opacity: [0, 1] });
|
||||
} finally {
|
||||
@@ -242,7 +274,7 @@ async function playNameFollow(node: HTMLElement): Promise<void> {
|
||||
const from = field.getBoundingClientRect();
|
||||
const to = node.getBoundingClientRect();
|
||||
const text = node.textContent?.trim() || "";
|
||||
node.style.opacity = "0";
|
||||
if (!await conceal(node)) return;
|
||||
try {
|
||||
await flyRect(from, to, text, spec);
|
||||
} finally {
|
||||
@@ -289,7 +321,7 @@ async function playStarterFollow(bubble: HTMLElement): Promise<void> {
|
||||
await fadeIn(bubble, spec);
|
||||
return;
|
||||
}
|
||||
bubble.style.opacity = "0";
|
||||
if (!await conceal(bubble)) return;
|
||||
try {
|
||||
await flyRect(pending.rect, bubble.getBoundingClientRect(), pending.text, spec);
|
||||
} finally {
|
||||
@@ -303,7 +335,7 @@ async function playSynastry(article: HTMLElement): Promise<void> {
|
||||
const spec = specFor("synastry");
|
||||
if (!spec) return;
|
||||
if (!spec.x || !spec.scale) {
|
||||
article.style.opacity = "0";
|
||||
if (!await conceal(article)) return;
|
||||
try {
|
||||
await run(article, spec, { opacity: [0, 1] });
|
||||
} finally {
|
||||
@@ -311,6 +343,7 @@ async function playSynastry(article: HTMLElement): Promise<void> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!await conceal(article)) return;
|
||||
const title = article.querySelector("h1")?.textContent ?? "";
|
||||
const sides = title.split("×").map((part) => part.replace(/·[\s\S]*$/, "").trim());
|
||||
const marks = [sides[0]?.slice(0, 1) || "甲", sides[1]?.slice(0, 1) || "乙"];
|
||||
@@ -335,7 +368,6 @@ async function playSynastry(article: HTMLElement): Promise<void> {
|
||||
glow.style.opacity = "0";
|
||||
layer.append(left, right, glow);
|
||||
document.body.appendChild(layer);
|
||||
article.style.opacity = "0";
|
||||
const approach = onboardingMotion.synastryMs / 1000;
|
||||
try {
|
||||
await Promise.all([
|
||||
@@ -405,6 +437,8 @@ export async function playRing(node: HTMLElement, from: string, to: string): Pro
|
||||
}
|
||||
}
|
||||
|
||||
export const playOnboardingEnter = playEnter;
|
||||
|
||||
export function mountOnboardingMotion(root: ParentNode = document): () => void {
|
||||
const visit = (node: HTMLElement) => {
|
||||
scan(node);
|
||||
|
||||
@@ -159,6 +159,17 @@ export function queuedHint(facts: HintFacts): OnboardingHintId | null {
|
||||
return eligible({ ...facts, blocked: false, anchors: {} });
|
||||
}
|
||||
|
||||
/** Motion plays only inside onboarding: an open checklist with work left, or an account still before its first question. A closed checklist never plays. */
|
||||
export function onboardingMotionEnabled(input: {
|
||||
readonly checklistDismissed: boolean;
|
||||
readonly checklistOpen: boolean;
|
||||
readonly unfinishedItem: boolean;
|
||||
readonly beforeFirstQuestion: boolean;
|
||||
}): boolean {
|
||||
if (input.checklistDismissed) return false;
|
||||
return (input.checklistOpen && input.unfinishedItem) || input.beforeFirstQuestion;
|
||||
}
|
||||
|
||||
/** No observer when every hint was seen, the checklist was closed, nothing is eligible, or an answer is streaming. */
|
||||
export function shouldAttachHintObserver(input: {
|
||||
readonly checklistClosed: boolean;
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
hintCopy,
|
||||
hintsBlocked,
|
||||
nextEphemerisPhase,
|
||||
onboardingMotionEnabled,
|
||||
selectHint,
|
||||
shouldAttachHintObserver,
|
||||
showChartMenuDot,
|
||||
@@ -103,6 +104,8 @@ export type OnboardingJourney = {
|
||||
readonly registerHome: (actions: HomeActions | null) => void;
|
||||
/** False when no hint can appear, the checklist was closed, or an answer is streaming. */
|
||||
readonly hintWatch: boolean;
|
||||
/** False for a closed checklist and for anyone past onboarding. */
|
||||
readonly motionEnabled: boolean;
|
||||
};
|
||||
|
||||
export const OnboardingJourneyContext = createContext<OnboardingJourney | null>(null);
|
||||
@@ -546,6 +549,12 @@ export function OnboardingJourneyProvider({ children }: { readonly children: Rea
|
||||
streaming: live.streaming,
|
||||
facts: hintFacts,
|
||||
});
|
||||
const motionEnabled = onboardingMotionEnabled({
|
||||
checklistDismissed: Boolean(merged.checklistDismissedAt),
|
||||
checklistOpen: showChecklist,
|
||||
unfinishedItem: items.some((item) => !item.done),
|
||||
beforeFirstQuestion: !hasAnswer && remote.status === "ready" && userId !== "",
|
||||
});
|
||||
const showChecklist = remote.status === "ready"
|
||||
&& shouldShowChecklist({
|
||||
profileComplete,
|
||||
@@ -656,6 +665,7 @@ export function OnboardingJourneyProvider({ children }: { readonly children: Rea
|
||||
setLive,
|
||||
registerHome,
|
||||
hintWatch,
|
||||
motionEnabled,
|
||||
}), [
|
||||
showChecklist,
|
||||
items,
|
||||
@@ -674,6 +684,7 @@ export function OnboardingJourneyProvider({ children }: { readonly children: Rea
|
||||
setLive,
|
||||
registerHome,
|
||||
hintWatch,
|
||||
motionEnabled,
|
||||
]);
|
||||
|
||||
return <OnboardingJourneyContext.Provider value={value}>{children}</OnboardingJourneyContext.Provider>;
|
||||
|
||||
@@ -44,6 +44,7 @@ function journey(hintWatch: boolean): OnboardingJourney {
|
||||
setLive: () => {},
|
||||
registerHome: () => {},
|
||||
hintWatch,
|
||||
motionEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { OnboardingMotionHost } from "../src/components/onboarding-motion-host.tsx";
|
||||
import { onboardingMotionEnabled } from "../src/lib/onboarding-hints.ts";
|
||||
import { OnboardingJourneyContext, type OnboardingJourney } from "../src/lib/onboarding-journey.tsx";
|
||||
import { claimOnboardingPlay, resetOnboardingPlays } from "../src/lib/motion/onboarding-play.ts";
|
||||
import { playOnboardingEnter, setOnboardingMotionLoader } from "../src/lib/motion/onboarding-run.ts";
|
||||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
|
||||
|
||||
function closedJourney(): OnboardingJourney {
|
||||
return {
|
||||
showChecklist: false,
|
||||
items: [],
|
||||
label: "",
|
||||
celebration: false,
|
||||
activeHint: null,
|
||||
hintText: "",
|
||||
showChartDot: false,
|
||||
tabHasDot: () => false,
|
||||
dismissChecklist: () => {},
|
||||
dismissHint: async () => true,
|
||||
go: () => {},
|
||||
setChartTab: () => {},
|
||||
markChartVisited: () => {},
|
||||
setChecklistOpen: () => {},
|
||||
setLive: () => {},
|
||||
registerHome: () => {},
|
||||
hintWatch: false,
|
||||
motionEnabled: onboardingMotionEnabled({
|
||||
checklistDismissed: true,
|
||||
checklistOpen: false,
|
||||
unfinishedItem: true,
|
||||
beforeFirstQuestion: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
test("a closed checklist plays none of the four onboarding beats", async () => {
|
||||
const journey = closedJourney();
|
||||
assert.equal(journey.motionEnabled, false);
|
||||
resetOnboardingPlays();
|
||||
const harness = createClientLifecycleHarness();
|
||||
try {
|
||||
await harness.render(
|
||||
<OnboardingJourneyContext.Provider value={journey}>
|
||||
<OnboardingMotionHost />
|
||||
<button type="button" data-onboarding-enter="follow-up">追问</button>
|
||||
<article data-onboarding-enter="archive">档案</article>
|
||||
<svg data-onboarding-draw="vedic" data-onboarding-draw-key="vedic" />
|
||||
<article data-onboarding-synastry="00000000-0000-4000-8000-000000001447">合盘</article>
|
||||
</OnboardingJourneyContext.Provider>,
|
||||
);
|
||||
await harness.idle();
|
||||
assert.equal(claimOnboardingPlay("enter:follow-up"), true);
|
||||
assert.equal(claimOnboardingPlay("enter:archive"), true);
|
||||
assert.equal(claimOnboardingPlay("draw:vedic"), true);
|
||||
assert.equal(claimOnboardingPlay("syn:00000000-0000-4000-8000-000000001447"), true);
|
||||
assert.equal(harness.errors.length, 0, harness.errors.map(String).join("\n"));
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("a failed or slow Motion load leaves the element visible", async () => {
|
||||
const harness = createClientLifecycleHarness();
|
||||
try {
|
||||
resetOnboardingPlays();
|
||||
setOnboardingMotionLoader(() => Promise.reject(new Error("motion failed")));
|
||||
const failed = harness.document.createElement("div");
|
||||
failed.setAttribute("data-onboarding-enter", "follow-up");
|
||||
failed.style.opacity = "1";
|
||||
await playOnboardingEnter(failed as unknown as HTMLElement);
|
||||
assert.notEqual(failed.style.opacity, "0");
|
||||
|
||||
resetOnboardingPlays();
|
||||
setOnboardingMotionLoader(() => new Promise(() => {}));
|
||||
const slow = harness.document.createElement("div");
|
||||
slow.setAttribute("data-onboarding-enter", "archive");
|
||||
slow.style.opacity = "1";
|
||||
const started = Date.now();
|
||||
await playOnboardingEnter(slow as unknown as HTMLElement);
|
||||
assert.ok(Date.now() - started >= 250);
|
||||
assert.notEqual(slow.style.opacity, "0");
|
||||
} finally {
|
||||
setOnboardingMotionLoader(null);
|
||||
await harness.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user