修复推荐页封面遮罩与登录态刷新
推荐页运行态封面增加加载条并隔离层级,避免 runtime 内容穿透封面 登录态从未登录到已登录或退出后刷新当前页面,退出等待 token 清理完成后再刷新 补充推荐页封面、认证刷新与样式回归测试 同步平台链路、项目基线和 Hermes 决策文档
This commit is contained in:
@@ -19,11 +19,19 @@
|
||||
## 2026-06-07 推荐页运行态先封面预载再 ready 渐隐
|
||||
|
||||
- 背景:移动端推荐页上下切换公开作品时,如果运行态和封面资源没有明确准备边界,用户会看到未加载完成的 runtime、黑底闪动,或切卡后反向回弹。
|
||||
- 决策:推荐页拿到推荐作品列表后预加载每个作品的卡片封面、主封面和玩法兜底封面;嵌入 runtime 的启动遮罩必须复用带玩法标签和标题的作品卡面视觉,不能再切到一层单独的纯封面图。作品切换后遮罩接手当前卡面时必须瞬时显示,不允许从旧预览卡面再淡入到同一张卡面;runtime 统一通过 ready 门控等待 run / profile、lazy 组件和 runtime DOM 内图片资源准备完成,ready 返回 true 后再由外层露出游戏画面并只让卡面遮罩渐隐。推荐 rail 切换完成后归零不能走反向过渡动画。
|
||||
- 决策:推荐页拿到推荐作品列表后预加载每个作品的卡片封面、主封面和玩法兜底封面;嵌入 runtime 的启动遮罩必须复用带玩法标签和标题的作品卡面视觉,不能再切到一层单独的纯封面图。作品切换后遮罩接手当前卡面时必须瞬时显示,不允许从旧预览卡面再淡入到同一张卡面;runtime 统一通过 ready 门控等待 run / profile、lazy 组件和 runtime DOM 内图片资源准备完成,ready 返回 true 后再由外层露出游戏画面并只让卡面遮罩渐隐。遮罩层级必须隔离下层 runtime,防止高 z-index HUD、canvas 或子运行态穿透到封面上;ready 前保留无说明文案的加载条 / 动效,不展示“加载中”文案。推荐 rail 切换完成后归零不能走反向过渡动画。
|
||||
- 影响范围:`src/components/rpg-entry/RpgEntryHomeView.tsx`、推荐页 runtime 生命周期、平台玩法链路文档。
|
||||
- 验证方式:`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`。
|
||||
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||
|
||||
## 2026-06-07 登录态身份边界变更后刷新当前页
|
||||
|
||||
- 背景:推荐页运行态、作品架、个人数据和私有 query 都可能在页面内缓存当前身份;如果登录或退出只改 React 上下文,当前页可能继续拿旧身份的局部状态渲染。
|
||||
- 决策:H5 登录态从未登录变为已登录,或从已登录变为未登录后,前端必须刷新当前页面一次,让平台壳和运行态按新身份重新初始化。普通 access token refresh、账号资料更新、主题或音量设置变化不触发整页刷新。
|
||||
- 影响范围:`src/components/auth/AuthGate.tsx`、平台入口身份初始化、项目基线文档。
|
||||
- 验证方式:`npm run test -- src/components/auth/AuthGate.test.tsx`。
|
||||
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`。
|
||||
|
||||
## 2026-06-07 跳一跳排行榜展示名禁止泄露内部身份键
|
||||
|
||||
- 背景:跳一跳排行榜曾在结果页和运行态失败弹窗里直接展示 `playerId` / `user_id`,用户可见内容暴露了内部身份键。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -50,6 +50,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当
|
||||
7. 小程序 `web-view` 页必须启用好友分享与朋友圈分享,分享目标固定回到 `pages/web-view/index`,不把 H5 当前 URL 作为不受控启动参数传回小程序页。
|
||||
8. 小程序 `web-view` 外壳运行时通过 `wx.getAccountInfoSync().miniProgram.envVersion` 自动识别版本:线上版 `release` 使用 `www.genarrative.world`,体验版 `trial` 与开发版 `develop` 使用 `dev.genarrative.world`;传给后端的 `x-mini-program-env` 分别为 `release`、`trial`、`dev`。
|
||||
9. 账号信息面板只展示 `账号信息` 标题;绑定手机号和绑定微信以紧凑模块展示当前绑定状态,已绑定手机号展示完整手机号,已绑定微信优先展示微信平台实际返回并由后端保存的 `wechatDisplayName`。小程序 `jscode2session` 不能直接返回微信昵称或个人微信号,只能稳定拿到当前小程序维度的 `openid`,并在满足微信开放平台条件时拿到 `unionid`;小程序昵称来自快捷登录后按需展示的原生 `input type="nickname"` 提交的 `displayName`。后端下发 `wechatAccount` 作为绑定账号标识,前端在没有真实昵称时展示微信账号尾号,不展示裸“已绑定”。换绑入口放在对应模块右上角,退出登录和退出全部设备固定放在面板内容最底部。
|
||||
10. H5 登录态从未登录变为已登录,或从已登录变为未登录后,必须刷新当前页面一次,确保推荐运行态、作品架、个人缓存和私有 query 都按新身份重新初始化;普通 access token 续期、账号资料更新和同一登录态内的设置变化不得触发整页刷新。
|
||||
|
||||
## 账户与充值
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { act, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { AuthSessionSummary, AuthUser } from '../../services/authService';
|
||||
import { LEGAL_CONSENT_STORAGE_KEY } from '../common/legalDocuments';
|
||||
import { AuthGate } from './AuthGate';
|
||||
import { AuthGate, setAuthGateReloadForTest } from './AuthGate';
|
||||
import { useAuthUi } from './AuthUiContext';
|
||||
|
||||
const authMocks = vi.hoisted(() => ({
|
||||
@@ -107,6 +107,7 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
window.localStorage.clear();
|
||||
window.history.replaceState(null, '', '/');
|
||||
setAuthGateReloadForTest(vi.fn());
|
||||
authMocks.consumeAuthCallbackResult.mockReturnValue(null);
|
||||
authMocks.ensureStoredAccessToken.mockResolvedValue('jwt-existing-token');
|
||||
authMocks.getStoredAccessToken.mockReturnValue('');
|
||||
@@ -158,6 +159,10 @@ beforeEach(() => {
|
||||
authMocks.requestWechatMiniProgramPhoneLogin.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setAuthGateReloadForTest(null);
|
||||
});
|
||||
|
||||
async function acceptLegalConsent(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
dialog: HTMLElement,
|
||||
@@ -382,6 +387,8 @@ test('auth gate keeps sms and password entries available when login options requ
|
||||
test('auth gate opens a login modal for protected actions and resumes after login', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAuthenticated = vi.fn();
|
||||
const reload = vi.fn();
|
||||
setAuthGateReloadForTest(reload);
|
||||
|
||||
authMocks.getAuthLoginOptions.mockResolvedValue({
|
||||
availableLoginMethods: ['phone'],
|
||||
@@ -411,6 +418,7 @@ test('auth gate opens a login modal for protected actions and resumes after logi
|
||||
);
|
||||
expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(1);
|
||||
expect(onAuthenticated).toHaveBeenCalledTimes(1);
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
|
||||
@@ -636,6 +644,8 @@ test('registration invite modal can skip when invite code is empty', async () =>
|
||||
|
||||
test('auth state refresh keeps mounted platform content and local tab state', async () => {
|
||||
const user = userEvent.setup();
|
||||
const reload = vi.fn();
|
||||
setAuthGateReloadForTest(reload);
|
||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||
user: mockUser,
|
||||
availableLoginMethods: ['phone'],
|
||||
@@ -674,10 +684,13 @@ test('auth state refresh keeps mounted platform content and local tab state', as
|
||||
expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(screen.getByText('当前Tab:创作')).toBeTruthy();
|
||||
expect(reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('logout withdraws user context before backend request finishes', async () => {
|
||||
const user = userEvent.setup();
|
||||
const reload = vi.fn();
|
||||
setAuthGateReloadForTest(reload);
|
||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||
user: mockUser,
|
||||
availableLoginMethods: ['phone'],
|
||||
@@ -703,11 +716,14 @@ test('logout withdraws user context before backend request finishes', async () =
|
||||
expect(await screen.findByText('当前用户:未登录')).toBeTruthy();
|
||||
expect(screen.getByText('私有数据:不可读取')).toBeTruthy();
|
||||
expect(authMocks.logoutAuthUser).toHaveBeenCalledTimes(1);
|
||||
expect(reload).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
resolveLogout();
|
||||
await logoutPromise;
|
||||
});
|
||||
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('auth gate shows sms send feedback in the login modal', async () => {
|
||||
|
||||
@@ -65,6 +65,18 @@ type AuthStatus =
|
||||
|
||||
const REQUIRED_LOGIN_METHODS: AuthLoginMethod[] = ['phone', 'password'];
|
||||
|
||||
let reloadCurrentPageForAuthStateChange = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
export function setAuthGateReloadForTest(handler: (() => void) | null) {
|
||||
reloadCurrentPageForAuthStateChange =
|
||||
handler ??
|
||||
(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function readInviteCodeFromLocation(): string {
|
||||
const params = new URLSearchParams(window.location.search || '');
|
||||
return (params.get('inviteCode') || params.get('invite_code') || '')
|
||||
@@ -140,6 +152,8 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
const autoOpenedInviteCodeRef = useRef<string | null>(null);
|
||||
const hasRenderedPlatformContentRef = useRef(false);
|
||||
const authHydrateVersionRef = useRef(0);
|
||||
const lastStableAuthPresenceRef = useRef<boolean | null>(null);
|
||||
const pendingAuthStateReloadRef = useRef(false);
|
||||
const canKeepPlatformContentMounted =
|
||||
hasRenderedPlatformContentRef.current &&
|
||||
(status === 'checking' || status === 'recovering');
|
||||
@@ -152,36 +166,64 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
hasRenderedPlatformContentRef.current = true;
|
||||
}
|
||||
|
||||
const markAuthStateReloadIfChanged = useCallback(
|
||||
(
|
||||
nextUser: AuthUser | null,
|
||||
options: { reloadOnChange?: boolean } = {},
|
||||
) => {
|
||||
const nextHasUser = Boolean(nextUser);
|
||||
const previousHasUser = lastStableAuthPresenceRef.current;
|
||||
if (previousHasUser === null) {
|
||||
lastStableAuthPresenceRef.current = nextHasUser;
|
||||
return;
|
||||
}
|
||||
|
||||
lastStableAuthPresenceRef.current = nextHasUser;
|
||||
if (
|
||||
previousHasUser !== nextHasUser &&
|
||||
options.reloadOnChange !== false
|
||||
) {
|
||||
pendingAuthStateReloadRef.current = true;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const activateReadyUser = useCallback((nextUser: AuthUser) => {
|
||||
// 受保护业务 hook 只在 readyUser 暴露后启动,必须先保证请求层能带 Bearer token。
|
||||
authHydrateVersionRef.current += 1;
|
||||
markAuthStateReloadIfChanged(nextUser);
|
||||
setUser(nextUser);
|
||||
setStatus('ready');
|
||||
}, []);
|
||||
}, [markAuthStateReloadIfChanged]);
|
||||
|
||||
const clearLocalAuthenticatedState = useCallback(() => {
|
||||
// 退出动作必须先收回前端鉴权上下文,再等待后端吊销完成。
|
||||
// 否则平台壳层会在无刷新状态下继续暴露旧用户的私有作品缓存。
|
||||
authHydrateVersionRef.current += 1;
|
||||
pendingProtectedActionRef.current = null;
|
||||
setUser(null);
|
||||
setStatus('unauthenticated');
|
||||
setShowLoginModal(false);
|
||||
setShowRegistrationInviteModal(false);
|
||||
setShowSettingsModal(false);
|
||||
setSettingsEntryMode('settings');
|
||||
setInitialSettingsSection(null);
|
||||
setSessions([]);
|
||||
setRevokingSessionIds([]);
|
||||
setAuditLogs([]);
|
||||
setRiskBlocks([]);
|
||||
setLoginCaptchaChallenge(null);
|
||||
setBindCaptchaChallenge(null);
|
||||
setChangePhoneCaptchaChallenge(null);
|
||||
setPendingInviteCode('');
|
||||
setRegistrationInviteError('');
|
||||
setError('');
|
||||
}, []);
|
||||
const clearLocalAuthenticatedState = useCallback(
|
||||
(options: { reloadOnChange?: boolean } = {}) => {
|
||||
// 退出动作必须先收回前端鉴权上下文,再等待后端吊销完成。
|
||||
// 否则平台壳层会在无刷新状态下继续暴露旧用户的私有作品缓存。
|
||||
authHydrateVersionRef.current += 1;
|
||||
markAuthStateReloadIfChanged(null, options);
|
||||
pendingProtectedActionRef.current = null;
|
||||
setUser(null);
|
||||
setStatus('unauthenticated');
|
||||
setShowLoginModal(false);
|
||||
setShowRegistrationInviteModal(false);
|
||||
setShowSettingsModal(false);
|
||||
setSettingsEntryMode('settings');
|
||||
setInitialSettingsSection(null);
|
||||
setSessions([]);
|
||||
setRevokingSessionIds([]);
|
||||
setAuditLogs([]);
|
||||
setRiskBlocks([]);
|
||||
setLoginCaptchaChallenge(null);
|
||||
setBindCaptchaChallenge(null);
|
||||
setChangePhoneCaptchaChallenge(null);
|
||||
setPendingInviteCode('');
|
||||
setRegistrationInviteError('');
|
||||
setError('');
|
||||
},
|
||||
[markAuthStateReloadIfChanged],
|
||||
);
|
||||
|
||||
const restoreAuthSession = useCallback(async () => {
|
||||
const hadLocalAccessToken = Boolean(getStoredAccessToken());
|
||||
@@ -234,7 +276,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
}, []);
|
||||
|
||||
const logoutCurrentSession = useCallback(async () => {
|
||||
clearLocalAuthenticatedState();
|
||||
clearLocalAuthenticatedState({ reloadOnChange: false });
|
||||
try {
|
||||
await logoutAuthUser();
|
||||
} catch (logoutError) {
|
||||
@@ -243,11 +285,13 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
? logoutError.message
|
||||
: '退出登录失败,请刷新页面确认状态。',
|
||||
);
|
||||
} finally {
|
||||
reloadCurrentPageForAuthStateChange();
|
||||
}
|
||||
}, [clearLocalAuthenticatedState]);
|
||||
|
||||
const logoutAllSessions = useCallback(async () => {
|
||||
clearLocalAuthenticatedState();
|
||||
clearLocalAuthenticatedState({ reloadOnChange: false });
|
||||
try {
|
||||
await logoutAllAuthSessions();
|
||||
} catch (logoutError) {
|
||||
@@ -256,6 +300,8 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
? logoutError.message
|
||||
: '退出全部设备失败,请刷新页面确认状态。',
|
||||
);
|
||||
} finally {
|
||||
reloadCurrentPageForAuthStateChange();
|
||||
}
|
||||
}, [clearLocalAuthenticatedState]);
|
||||
|
||||
@@ -386,6 +432,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
markAuthStateReloadIfChanged(null);
|
||||
setUser(null);
|
||||
setStatus('unauthenticated');
|
||||
} catch (optionsError) {
|
||||
@@ -394,6 +441,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
}
|
||||
|
||||
setAvailableLoginMethods(REQUIRED_LOGIN_METHODS);
|
||||
markAuthStateReloadIfChanged(null);
|
||||
setUser(null);
|
||||
// 中文注释:登录方式接口失败时按产品约定保留验证码和密码登录入口;
|
||||
// 这里不展示接口读取错误,避免用户误以为登录本身不可用。
|
||||
@@ -413,6 +461,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
return;
|
||||
}
|
||||
if (restoredSession.kind === 'guest') {
|
||||
markAuthStateReloadIfChanged(null);
|
||||
setAvailableLoginMethods(
|
||||
normalizeAvailableLoginMethods(
|
||||
restoredSession.session?.availableLoginMethods,
|
||||
@@ -423,6 +472,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
}
|
||||
|
||||
const nextSession = restoredSession.session;
|
||||
markAuthStateReloadIfChanged(nextSession.user);
|
||||
setUser(nextSession.user);
|
||||
setAvailableLoginMethods(
|
||||
normalizeAvailableLoginMethods(nextSession.availableLoginMethods),
|
||||
@@ -470,19 +520,23 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
window.removeEventListener(AUTH_STATE_EVENT, handleAuthStateChange);
|
||||
window.removeEventListener('hashchange', handleAuthHashChange);
|
||||
};
|
||||
}, [restoreAuthSession]);
|
||||
}, [markAuthStateReloadIfChanged, restoreAuthSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!readyUser) {
|
||||
setShowSettingsModal(false);
|
||||
return;
|
||||
} else {
|
||||
setShowLoginModal(false);
|
||||
|
||||
const pendingAction = pendingProtectedActionRef.current;
|
||||
pendingProtectedActionRef.current = null;
|
||||
pendingAction?.();
|
||||
}
|
||||
|
||||
setShowLoginModal(false);
|
||||
|
||||
const pendingAction = pendingProtectedActionRef.current;
|
||||
pendingProtectedActionRef.current = null;
|
||||
pendingAction?.();
|
||||
if (pendingAuthStateReloadRef.current) {
|
||||
pendingAuthStateReloadRef.current = false;
|
||||
reloadCurrentPageForAuthStateChange();
|
||||
}
|
||||
}, [readyUser]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3908,6 +3908,9 @@ test('mobile recommend startup keeps cover visible without loading copy', () =>
|
||||
expect(
|
||||
document.querySelector('.platform-recommend-runtime-cover'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector('.platform-recommend-runtime-loading'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('加载中...')).toBeNull();
|
||||
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -1288,6 +1288,7 @@ function RecommendRuntimeCover({
|
||||
position="cover"
|
||||
resolvedCoverUrls={resolvedCoverUrls}
|
||||
/>
|
||||
<div className="platform-recommend-runtime-loading" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+43
-1
@@ -220,6 +220,16 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes platform-recommend-runtime-loading {
|
||||
0% {
|
||||
transform: translateX(-110%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(240%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes puzzle-clear-card-clear-pop {
|
||||
0% {
|
||||
opacity: 1;
|
||||
@@ -5281,6 +5291,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
.platform-recommend-runtime-viewport {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
isolation: isolate;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: var(--platform-recommend-runtime-fill);
|
||||
@@ -5298,7 +5310,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
.platform-recommend-runtime-cover {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
z-index: 30;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
background: var(--platform-recommend-runtime-fill);
|
||||
opacity: 1;
|
||||
@@ -5306,6 +5319,35 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.platform-recommend-runtime-loading {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
bottom: 0.72rem;
|
||||
left: 1rem;
|
||||
z-index: 4;
|
||||
height: 0.18rem;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
box-shadow: 0 0 18px rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
.platform-recommend-runtime-loading::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 42%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.38),
|
||||
rgba(255, 255, 255, 0.92),
|
||||
rgba(255, 180, 97, 0.86)
|
||||
);
|
||||
content: '';
|
||||
transform: translateX(-110%);
|
||||
animation: platform-recommend-runtime-loading 1.15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.platform-recommend-runtime-cover--hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
+26
-1
@@ -131,12 +131,37 @@ describe('index stylesheet creation agent hero contrast', () => {
|
||||
});
|
||||
|
||||
describe('index stylesheet recommend runtime cover', () => {
|
||||
it('only fades the card cover out after runtime is ready', () => {
|
||||
it('keeps the card cover above embedded runtime and only fades it when ready', () => {
|
||||
const css = readIndexCss();
|
||||
|
||||
const viewportBlock = getCssBlock(
|
||||
css,
|
||||
'.platform-recommend-runtime-viewport',
|
||||
);
|
||||
expect(viewportBlock).toContain('z-index: 1;');
|
||||
expect(viewportBlock).toContain('isolation: isolate;');
|
||||
|
||||
const coverBlock = getCssBlock(css, '.platform-recommend-runtime-cover');
|
||||
expect(coverBlock).toContain('z-index: 30;');
|
||||
expect(coverBlock).toContain('isolation: isolate;');
|
||||
expect(coverBlock).not.toContain('transition: opacity');
|
||||
|
||||
const loadingBlock = getCssBlock(
|
||||
css,
|
||||
'.platform-recommend-runtime-loading',
|
||||
);
|
||||
expect(loadingBlock).toContain('position: absolute;');
|
||||
expect(loadingBlock).toContain('z-index: 4;');
|
||||
|
||||
const loadingAnimationBlock = getCssBlock(
|
||||
css,
|
||||
'.platform-recommend-runtime-loading::before',
|
||||
);
|
||||
expect(loadingAnimationBlock).toContain(
|
||||
'animation: platform-recommend-runtime-loading 1.15s ease-in-out infinite;',
|
||||
);
|
||||
expect(css).toContain('@keyframes platform-recommend-runtime-loading');
|
||||
|
||||
const hiddenCoverBlock = getCssBlock(
|
||||
css,
|
||||
'.platform-recommend-runtime-cover--hidden',
|
||||
|
||||
Reference in New Issue
Block a user