feat(agc): 确认页去掉须知内层滚动改吸附页脚,发布面板继承授权改徽章
This commit is contained in:
+9
-6
@@ -1107,14 +1107,17 @@ export function GameDistributionPublishPanel({
|
||||
*/}
|
||||
{forkSource ? (
|
||||
<p
|
||||
className="game-distribution-publish-panel__fork-hint"
|
||||
className="game-distribution-publish-panel__fork-inherited"
|
||||
aria-label="授权继承"
|
||||
>
|
||||
{`授权继承自父作品${
|
||||
parentAuthorizationLabel
|
||||
? `:${parentAuthorizationLabel}`
|
||||
: '(档位读取中或不可用)'
|
||||
}。衍生作品的授权档位在创建时由父作品快照继承,本页与网页端都不能再修改(服务端会拒绝任何提升)。`}
|
||||
<span className="game-distribution-publish-panel__fork-inherited-badge">
|
||||
{parentAuthorizationLabel
|
||||
? `授权继承自父作品:${parentAuthorizationLabel}`
|
||||
: '授权继承自父作品'}
|
||||
</span>
|
||||
<span>
|
||||
衍生作品的授权档位在创建时由父作品快照继承,本页与网页端都不能再修改(服务端会拒绝任何提升)。
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
* Fork 工程确认页(创作用户端):创作前的授权校验与工程同步环节。
|
||||
*
|
||||
* 结构照需求文档「Fork 工程确认页(客户端)」:父作品信息卡 → 二次确认 + 授权须知 + 风险说明
|
||||
* (**强制阅读 3 秒**)→ 工程同步状态条 → 底部「取消 / 确认并进入编辑」。
|
||||
* (**强制阅读 3 秒**)→ 工程同步状态条 → 底部吸附操作条「取消 / 确认并进入编辑」。
|
||||
*
|
||||
* 两条硬要求:
|
||||
* 1. 须知**全文真实可见**(可滚动,不折叠成一句),五类内容逐条写清;
|
||||
* 2. 状态条只显示**原生真实发出的阶段**(`fork.progress`),阶段内部不可测量就只做进行中指示,
|
||||
* 三条硬要求:
|
||||
* 1. 须知**全文真实可见**(五条逐条写清、**不折叠也不自建内层滚动**,随页面一起滚);
|
||||
* 2. 强制阅读只靠**计时门禁**(底部按钮倒计时 + 顶部徽章),不要求「滚到底」;
|
||||
* 3. 状态条只显示**原生真实发出的阶段**(`fork.progress`),阶段内部不可测量就只做进行中指示,
|
||||
* **不造百分比**。
|
||||
*
|
||||
* 滚动:页面滚动容器是外壳的 `.launcher-main`(见 `styles.css` 里那条注释),本页**不自建**
|
||||
* 滚动容器;底部操作条用 `position: sticky` 吸附在滚动容器下沿,因此始终可见。
|
||||
*/
|
||||
|
||||
import { RefreshCcw } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { resolveTauriInvoke } from '../../app/tauri';
|
||||
import type { InitLocalProjectResult } from '../../app/types';
|
||||
@@ -20,6 +24,25 @@ import {
|
||||
forkNoticeSections,
|
||||
MANDATORY_READING_SECONDS,
|
||||
} from '../../features/platform-fork/forkNotice';
|
||||
import {
|
||||
FORK_BADGE_NEUTRAL,
|
||||
FORK_BADGE_SUCCESS,
|
||||
FORK_BADGE_WARM,
|
||||
FORK_BODY_TEXT,
|
||||
FORK_BUTTON_PRIMARY,
|
||||
FORK_BUTTON_SECONDARY,
|
||||
FORK_CARD,
|
||||
FORK_META_GRID,
|
||||
FORK_META_LABEL,
|
||||
FORK_META_VALUE,
|
||||
FORK_PAGE_COLUMN,
|
||||
FORK_PAGE_ROOT,
|
||||
FORK_PAGE_TITLE,
|
||||
FORK_SECTION,
|
||||
FORK_SECTION_HEAD,
|
||||
FORK_SECTION_TITLE,
|
||||
FORK_STICKY_FOOTER,
|
||||
} from '../../features/platform-fork/forkPageStyles';
|
||||
import {
|
||||
type ForkParentCard,
|
||||
forkParentCardFromGame,
|
||||
@@ -44,9 +67,6 @@ type ForkConfirmViewProps = {
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const sectionClass =
|
||||
'mx-auto mt-5.5 grid w-[min(814px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]';
|
||||
|
||||
export default function ForkConfirmView({
|
||||
onProjectCreated,
|
||||
onCancel,
|
||||
@@ -62,7 +82,6 @@ export default function ForkConfirmView({
|
||||
const [remainingSeconds, setRemainingSeconds] = useState(
|
||||
MANDATORY_READING_SECONDS,
|
||||
);
|
||||
const noticeListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const gameId = target?.gameId ?? '';
|
||||
const notices = useMemo(
|
||||
@@ -120,13 +139,9 @@ export default function ForkConfirmView({
|
||||
}, [card?.coverObjectKey]);
|
||||
|
||||
// 强制阅读:进入页面后倒计时 3 秒,其间确认按钮禁用(重新打开确认页会重新计时)。
|
||||
// 门禁只与**时间**有关:须知随页面自然流动,不要求用户把某个内层容器滚到底。
|
||||
useEffect(() => {
|
||||
setRemainingSeconds(MANDATORY_READING_SECONDS);
|
||||
// 须知区一进来就滚到顶部,避免上次滚动位置让用户误以为已经读过。
|
||||
// 用 `scrollTop` 而不是 `scrollTo()`:jsdom 没实现后者,而这里只需要复位。
|
||||
if (noticeListRef.current) {
|
||||
noticeListRef.current.scrollTop = 0;
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
const timer = window.setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
|
||||
@@ -146,6 +161,7 @@ export default function ForkConfirmView({
|
||||
);
|
||||
const reachedStages = fork.progress.reached;
|
||||
const activeStage = fork.progress.current;
|
||||
const syncedCount = reachedStages.length;
|
||||
|
||||
async function confirmFork() {
|
||||
if (!gameId || readingLocked || fork.busy) {
|
||||
@@ -169,13 +185,16 @@ export default function ForkConfirmView({
|
||||
|
||||
if (!target) {
|
||||
return (
|
||||
<div className="grid content-start gap-3 pb-10">
|
||||
<section className={sectionClass} aria-label="Fork 确认">
|
||||
<p className="m-0 text-[12px] leading-5 text-(--platform-text-soft)">
|
||||
<div className={FORK_PAGE_ROOT}>
|
||||
<section
|
||||
className={`${FORK_PAGE_COLUMN} ${FORK_SECTION}`}
|
||||
aria-label="Fork 确认"
|
||||
>
|
||||
<p className={FORK_BODY_TEXT}>
|
||||
没有待确认的 Fork;请回到「共创」页选择一个作品。
|
||||
</p>
|
||||
<button
|
||||
className="w-fit cursor-pointer rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) px-3 py-1.5 text-[12px] text-(--platform-text-strong)"
|
||||
className={`${FORK_BUTTON_SECONDARY} w-fit`}
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
>
|
||||
@@ -187,12 +206,10 @@ export default function ForkConfirmView({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid content-start gap-0 pb-10">
|
||||
<header className={sectionClass}>
|
||||
<h1 className="m-0 text-[19px] font-semibold text-(--platform-text-strong)">
|
||||
Fork 工程确认
|
||||
</h1>
|
||||
<p className="m-0 mt-1 text-[12px] leading-5 text-(--platform-text-soft)">
|
||||
<div className={FORK_PAGE_ROOT}>
|
||||
<header className={`${FORK_PAGE_COLUMN} grid gap-1`}>
|
||||
<h1 className={FORK_PAGE_TITLE}>Fork 工程确认</h1>
|
||||
<p className={`${FORK_BODY_TEXT} max-w-[64ch]`}>
|
||||
确认授权边界后再开始。Fork
|
||||
会把整个工程复制一份到你的项目里继续改造(像 git
|
||||
fork),副本完全独立。
|
||||
@@ -200,10 +217,15 @@ export default function ForkConfirmView({
|
||||
</header>
|
||||
|
||||
{/* 父作品信息卡:封面 / 标题 / 作者 / 当前授权类型(缺失逐项降级,不报错) */}
|
||||
<section className={sectionClass} aria-label="父作品信息">
|
||||
<div className="grid grid-cols-[112px_minmax(0,1fr)] items-stretch gap-3.5 rounded-xl border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-3 shadow-(--platform-panel-shadow) max-[760px]:grid-cols-[88px_minmax(0,1fr)]">
|
||||
<section
|
||||
className={`${FORK_PAGE_COLUMN} ${FORK_SECTION}`}
|
||||
aria-label="父作品信息"
|
||||
>
|
||||
<div
|
||||
className={`${FORK_CARD} grid grid-cols-[104px_minmax(0,1fr)] items-stretch gap-3.5 p-3 max-[760px]:grid-cols-[80px_minmax(0,1fr)]`}
|
||||
>
|
||||
<span
|
||||
className="grid min-h-29 place-items-center overflow-hidden rounded-lg text-[26px] font-bold text-white shadow-inner"
|
||||
className="grid min-h-27 place-items-center overflow-hidden rounded-lg text-[26px] font-bold text-white shadow-inner"
|
||||
style={{ backgroundColor: card?.coverColor || '#c2653d' }}
|
||||
>
|
||||
{coverUrl ? (
|
||||
@@ -218,40 +240,44 @@ export default function ForkConfirmView({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="grid min-w-0 content-center gap-1.5">
|
||||
<small className="text-[10px] text-(--platform-text-soft)">
|
||||
<span className="grid min-w-0 content-center gap-2">
|
||||
<small className="text-[10px] leading-3.5 tracking-[0.08em] text-(--platform-text-soft)">
|
||||
父作品
|
||||
</small>
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[15px] font-semibold text-(--platform-text-strong)">
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[15px] font-semibold leading-5 text-(--platform-text-strong)">
|
||||
{card?.title?.trim() ||
|
||||
(cardLoading ? '正在读取作品信息…' : '(未获取到标题)')}
|
||||
</strong>
|
||||
<small className="grid grid-cols-[52px_minmax(0,1fr)] gap-x-2 gap-y-1 text-[11px] leading-4 text-(--platform-text-soft)">
|
||||
<span className="whitespace-nowrap">作者</span>
|
||||
<span className="truncate whitespace-nowrap text-(--platform-neutral-text)">
|
||||
<small className={`${FORK_META_GRID} text-[11px]`}>
|
||||
<span className={FORK_META_LABEL}>作者</span>
|
||||
<span className={FORK_META_VALUE}>
|
||||
{card?.authorName?.trim() || '—'}
|
||||
</span>
|
||||
<span className="whitespace-nowrap">授权类型</span>
|
||||
<span className="truncate whitespace-nowrap text-(--platform-warm-text)">
|
||||
<span className={FORK_META_LABEL}>授权类型</span>
|
||||
<span className="min-w-0 truncate whitespace-nowrap text-(--platform-warm-text)">
|
||||
{card ? card.authorizationLabel : '—'}
|
||||
</span>
|
||||
<span className="whitespace-nowrap">作品 ID</span>
|
||||
<code className="truncate whitespace-nowrap">{gameId}</code>
|
||||
<span className={FORK_META_LABEL}>作品 ID</span>
|
||||
<code className={FORK_META_VALUE}>{gameId}</code>
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 二次确认 + 授权须知 + 风险说明:全文可见(可滚动),前 3 秒确认按钮禁用 */}
|
||||
<section className={sectionClass} aria-label="授权须知与风险说明">
|
||||
<header className="flex items-center justify-between gap-3">
|
||||
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
|
||||
授权须知与风险说明
|
||||
</h2>
|
||||
<small className="text-[11px] text-(--platform-text-soft)">
|
||||
{readingLocked
|
||||
? `请阅读下面的须知,${remainingSeconds} 秒后可确认`
|
||||
: '已可确认'}
|
||||
{/*
|
||||
二次确认 + 授权须知 + 风险说明:全文随页面自然流动(**没有内层滚动**),
|
||||
前 3 秒确认按钮禁用;顶部徽章给出倒计时,底部按钮同样带倒计时。
|
||||
*/}
|
||||
<section
|
||||
className={`${FORK_PAGE_COLUMN} ${FORK_SECTION}`}
|
||||
aria-label="授权须知与风险说明"
|
||||
>
|
||||
<header className={FORK_SECTION_HEAD}>
|
||||
<h2 className={FORK_SECTION_TITLE}>授权须知与风险说明</h2>
|
||||
<small
|
||||
className={readingLocked ? FORK_BADGE_WARM : FORK_BADGE_SUCCESS}
|
||||
>
|
||||
{readingLocked ? `${remainingSeconds}s 后可确认` : '已可确认'}
|
||||
</small>
|
||||
</header>
|
||||
<div
|
||||
@@ -268,38 +294,58 @@ export default function ForkConfirmView({
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="grid max-h-72 gap-3 overflow-y-auto rounded-xl border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-3.5"
|
||||
ref={noticeListRef}
|
||||
className={`${FORK_CARD} grid gap-3 p-3.5`}
|
||||
aria-label="授权须知全文"
|
||||
>
|
||||
{notices.map((notice) => (
|
||||
<article className="grid gap-1" key={notice.key}>
|
||||
<strong className="text-[12px] font-bold text-(--platform-text-strong)">
|
||||
{notice.title}
|
||||
</strong>
|
||||
<p className="m-0 text-[12px] leading-5 text-(--platform-text-soft)">
|
||||
{notice.body}
|
||||
</p>
|
||||
{notices.map((notice, index) => (
|
||||
<article
|
||||
className="grid grid-cols-[22px_minmax(0,1fr)] items-start gap-2.5"
|
||||
key={notice.key}
|
||||
>
|
||||
<span
|
||||
className="mt-0.5 grid size-5 place-items-center rounded-full border border-(--platform-warm-border) bg-(--platform-warm-bg) text-[10px] font-bold text-(--platform-warm-text)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="grid gap-1">
|
||||
<strong className="text-[12px] font-bold leading-5 text-(--platform-text-strong)">
|
||||
{notice.title}
|
||||
</strong>
|
||||
<p className={FORK_BODY_TEXT}>{notice.body}</p>
|
||||
</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 工程同步状态条:只显示原生真实阶段,不造百分比 */}
|
||||
<section className={sectionClass} aria-label="工程同步状态">
|
||||
<header className="flex items-center justify-between gap-3">
|
||||
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
|
||||
工程同步
|
||||
</h2>
|
||||
<small className="text-[11px] text-(--platform-text-soft)">
|
||||
<section
|
||||
className={`${FORK_PAGE_COLUMN} ${FORK_SECTION}`}
|
||||
aria-label="工程同步状态"
|
||||
>
|
||||
<header className={FORK_SECTION_HEAD}>
|
||||
<h2 className={FORK_SECTION_TITLE}>工程同步</h2>
|
||||
<small
|
||||
className={
|
||||
fork.busy
|
||||
? FORK_BADGE_WARM
|
||||
: syncedCount > 0
|
||||
? FORK_BADGE_SUCCESS
|
||||
: FORK_BADGE_NEUTRAL
|
||||
}
|
||||
>
|
||||
{fork.busy
|
||||
? `正在:${activeStage ? forkProgressStageLabel(activeStage) : '准备中'}`
|
||||
: reachedStages.length > 0
|
||||
? '已同步完成'
|
||||
: syncedCount > 0
|
||||
? `已同步 ${syncedCount}/${FORK_PROGRESS_STAGES.length} 步`
|
||||
: '确认后开始'}
|
||||
</small>
|
||||
</header>
|
||||
<ol className="m-0 grid list-none gap-2 p-0" aria-label="工程同步阶段">
|
||||
<ol
|
||||
className="m-0 grid list-none gap-2.5 p-0"
|
||||
aria-label="工程同步阶段"
|
||||
>
|
||||
{FORK_PROGRESS_STAGES.map((stage, index) => {
|
||||
const done =
|
||||
reachedStages.includes(stage.key) &&
|
||||
@@ -334,66 +380,67 @@ export default function ForkConfirmView({
|
||||
{stage.label}
|
||||
{active ? '(进行中)' : ''}
|
||||
</span>
|
||||
<small className="text-[10px] text-(--platform-text-soft)">
|
||||
{stage.detail}
|
||||
</small>
|
||||
<small className={FORK_BODY_TEXT}>{stage.detail}</small>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
{fork.error ? (
|
||||
<p className="m-0 text-[12px] leading-5 text-[#b42318]" role="alert">
|
||||
<p
|
||||
className="m-0 max-w-[72ch] text-[12px] leading-5 text-[#b42318]"
|
||||
role="alert"
|
||||
>
|
||||
{fork.error}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className={sectionClass}>
|
||||
<div className="flex items-center justify-end gap-2.5">
|
||||
<button
|
||||
className="cursor-pointer rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) px-3.5 py-2 text-[12px] text-(--platform-text-strong) disabled:cursor-not-allowed disabled:opacity-55"
|
||||
type="button"
|
||||
disabled={fork.busy}
|
||||
onClick={cancelFork}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="cursor-pointer rounded-lg border-0 bg-(image:--platform-button-primary-fill) px-3.5 py-2 text-[12px] font-bold text-(--platform-button-primary-text) disabled:cursor-not-allowed disabled:opacity-55"
|
||||
type="button"
|
||||
disabled={readingLocked || fork.busy || !gameId}
|
||||
onClick={() => void confirmFork()}
|
||||
>
|
||||
{fork.busy
|
||||
? '正在 Fork…'
|
||||
: readingLocked
|
||||
? `确认并进入编辑(${remainingSeconds}s)`
|
||||
: '确认并进入编辑'}
|
||||
</button>
|
||||
{/*
|
||||
吸附底部操作条:`sticky bottom-0` 挂在外壳滚动容器(`.launcher-main`)下沿,
|
||||
长页面滚动时始终可见;状态说明也放在条内,避免它被顶到看不见的地方。
|
||||
*/}
|
||||
<footer className={FORK_STICKY_FOOTER} aria-label="Fork 操作">
|
||||
<div className={`${FORK_PAGE_COLUMN} grid gap-2`}>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2.5">
|
||||
<button
|
||||
className={FORK_BUTTON_SECONDARY}
|
||||
type="button"
|
||||
disabled={fork.busy}
|
||||
onClick={cancelFork}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className={FORK_BUTTON_PRIMARY}
|
||||
type="button"
|
||||
disabled={readingLocked || fork.busy || !gameId}
|
||||
onClick={() => void confirmFork()}
|
||||
>
|
||||
{fork.busy
|
||||
? '正在 Fork…'
|
||||
: readingLocked
|
||||
? `确认并进入编辑(${remainingSeconds}s)`
|
||||
: '确认并进入编辑'}
|
||||
</button>
|
||||
</div>
|
||||
{fork.busy ? (
|
||||
<p className={FORK_BODY_TEXT} role="status">
|
||||
正在同步工程:内容较大时需要几分钟,请不要关闭客户端。同步期间不能取消。
|
||||
</p>
|
||||
) : null}
|
||||
{!fork.busy && reachedStages.length > 0 ? (
|
||||
<p className={FORK_BODY_TEXT} role="status">
|
||||
<RefreshCcw
|
||||
className="mr-1 inline-block align-[-2px]"
|
||||
size={11}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
同步已完成,即将进入工作台。
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{fork.busy ? (
|
||||
<p
|
||||
className="m-0 mt-2 text-[11px] leading-4 text-(--platform-text-soft)"
|
||||
role="status"
|
||||
>
|
||||
正在同步工程:内容较大时需要几分钟,请不要关闭客户端。同步期间不能取消。
|
||||
</p>
|
||||
) : null}
|
||||
{!fork.busy && reachedStages.length > 0 ? (
|
||||
<p
|
||||
className="m-0 mt-2 text-[11px] leading-4 text-(--platform-text-soft)"
|
||||
role="status"
|
||||
>
|
||||
<RefreshCcw
|
||||
className="mr-1 inline-block align-[-2px]"
|
||||
size={11}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
同步已完成,即将进入工作台。
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,10 +56,10 @@ function installInvoke(
|
||||
function renderPage() {
|
||||
const onProjectCreated = vi.fn();
|
||||
const onCancel = vi.fn();
|
||||
render(
|
||||
const rendered = render(
|
||||
<ForkConfirmView onCancel={onCancel} onProjectCreated={onProjectCreated} />,
|
||||
);
|
||||
return { onProjectCreated, onCancel };
|
||||
return { ...rendered, onProjectCreated, onCancel };
|
||||
}
|
||||
|
||||
function confirmButton() {
|
||||
@@ -185,6 +185,61 @@ describe('Fork 工程确认页', () => {
|
||||
expect(notices.textContent).toContain('会被平台下架并要求整改');
|
||||
});
|
||||
|
||||
it('须知块不再自建内层滚动,页面只靠外壳滚动容器', async () => {
|
||||
installInvoke(() => null);
|
||||
const { container } = renderPage();
|
||||
|
||||
const root = container.querySelector('.fork-page');
|
||||
expect(root).not.toBeNull();
|
||||
// 页面内不得出现任何纵向滚动容器:须知全文随页面自然流动。
|
||||
expect(container.querySelector('.fork-page .overflow-y-auto')).toBeNull();
|
||||
expect(container.querySelector('.fork-page .overflow-auto')).toBeNull();
|
||||
expect(container.querySelector('.fork-page .overflow-scroll')).toBeNull();
|
||||
const notices = screen.getByLabelText('授权须知全文');
|
||||
for (const className of Array.from(notices.classList)) {
|
||||
expect(className.startsWith('max-h-')).toBe(false);
|
||||
expect(className.startsWith('overflow-y-')).toBe(false);
|
||||
}
|
||||
// 须知仍然是**全文**:五条正文都在这一个容器里逐条渲染。
|
||||
expect(notices.querySelectorAll('article')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('底部操作条是吸附页脚:停在滚动容器下沿,不随内容滚走', async () => {
|
||||
installInvoke(() => null);
|
||||
const { container } = renderPage();
|
||||
|
||||
const footer = container.querySelector(
|
||||
'[aria-label="Fork 操作"]',
|
||||
) as HTMLElement;
|
||||
expect(footer).not.toBeNull();
|
||||
// `sticky bottom-0` 挂在页面根下(滚动容器是外壳的 `.launcher-main`)。
|
||||
expect(footer.classList.contains('sticky')).toBe(true);
|
||||
expect(footer.classList.contains('bottom-0')).toBe(true);
|
||||
const pageRoot = container.querySelector('.fork-page') as HTMLElement;
|
||||
expect(footer.parentElement).toBe(pageRoot);
|
||||
expect(pageRoot.lastElementChild).toBe(footer);
|
||||
expect(footer.className).toContain('border-t');
|
||||
// 两个动作都在页脚里,长页面下也够得到。
|
||||
expect(footer.textContent).toContain('取消');
|
||||
expect(footer.textContent).toContain('确认并进入编辑');
|
||||
});
|
||||
|
||||
it('状态是徽章形态,与按钮可区分(倒计时同时出现在徽章与按钮上)', async () => {
|
||||
installInvoke(() => null);
|
||||
renderPage();
|
||||
|
||||
const badge = screen.getByText(/^3s 后可确认$/);
|
||||
// 徽章:圆角胶囊 + 描边,不是按钮。
|
||||
expect(badge.tagName).toBe('SMALL');
|
||||
expect(badge.className).toContain('rounded-full');
|
||||
expect(badge.className).toContain('border');
|
||||
expect(badge.closest('button')).toBeNull();
|
||||
expect(screen.getByText('确认后开始').className).toContain('rounded-full');
|
||||
|
||||
finishMandatoryReading();
|
||||
expect(await screen.findByText('已可确认')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('全开放授权时商用权限改写成可商用', async () => {
|
||||
useForkConfirmStore.getState().close();
|
||||
useForkConfirmStore.getState().open({
|
||||
|
||||
Reference in New Issue
Block a user