Merge branch 'master' into hermes/hermes-996d586b
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -222,6 +222,7 @@ import { useRpgCreationEnterWorld } from '../rpg-entry/useRpgCreationEnterWorld'
|
||||
import { useRpgCreationResultAutosave } from '../rpg-entry/useRpgCreationResultAutosave';
|
||||
import { useRpgCreationSessionController } from '../rpg-entry/useRpgCreationSessionController';
|
||||
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||||
import { isPlatformCreationTypeVisible } from './platformEntryCreationTypes';
|
||||
import {
|
||||
@@ -1352,6 +1353,22 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
const { setPlatformTab } = platformBootstrap;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionStage === 'profile-feedback') {
|
||||
setPlatformTab('profile');
|
||||
}
|
||||
}, [selectionStage, setPlatformTab]);
|
||||
|
||||
const openProfileFeedback = useCallback(() => {
|
||||
if (!authUi?.user) {
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
setPlatformTab('profile');
|
||||
setSelectionStage('profile-feedback');
|
||||
}, [authUi, setPlatformTab, setSelectionStage]);
|
||||
|
||||
const enterCreateTab = useCallback(() => {
|
||||
// 只依赖稳定的 setter,避免把 bootstrap 对象的 render 级引用变化
|
||||
// 传导成 Agent session 恢复 effect 的重复触发。
|
||||
@@ -5465,6 +5482,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsProfilePlayStatsOpen(false);
|
||||
}}
|
||||
onOpenPlayedWork={openPlayedWork}
|
||||
onOpenFeedback={openProfileFeedback}
|
||||
onOpenProfileDashboardCard={(cardKey) => {
|
||||
if (cardKey === 'playedWorks') {
|
||||
openProfilePlayedWorks();
|
||||
@@ -5479,6 +5497,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'profile-feedback' && (
|
||||
<motion.div
|
||||
key="platform-profile-feedback"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -12 }}
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<PlatformFeedbackView
|
||||
onBack={() => {
|
||||
setPlatformTab('profile');
|
||||
setSelectionStage('platform');
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'work-detail' && selectedPublicWorkDetail && (
|
||||
<motion.div
|
||||
key="platform-work-detail"
|
||||
|
||||
63
src/components/platform-entry/PlatformFeedbackView.test.tsx
Normal file
63
src/components/platform-entry/PlatformFeedbackView.test.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
|
||||
test('PlatformFeedbackView renders reference feedback fields', () => {
|
||||
render(<PlatformFeedbackView onBack={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText('帮助与反馈')).toBeTruthy();
|
||||
expect(screen.getByText('反馈问题')).toBeTruthy();
|
||||
expect(screen.getByLabelText('问题描述')).toBeTruthy();
|
||||
expect(screen.getByText('0/200')).toBeTruthy();
|
||||
expect(screen.getByText('上传凭证(提供问题截图)')).toBeTruthy();
|
||||
expect(screen.getByText('上传凭证')).toBeTruthy();
|
||||
expect(screen.getByLabelText('联系电话')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '提交' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '查看反馈与投诉记录' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('PlatformFeedbackView validates minimum description length before submit', () => {
|
||||
const onSubmit = vi.fn();
|
||||
render(<PlatformFeedbackView onBack={vi.fn()} onSubmit={onSubmit} />);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('问题描述'), {
|
||||
target: { value: '太短' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交' }));
|
||||
|
||||
expect(screen.getByText('请填写10个字以上的问题描述')).toBeTruthy();
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('PlatformFeedbackView submits trimmed payload', async () => {
|
||||
const onSubmit = vi.fn();
|
||||
render(<PlatformFeedbackView onBack={vi.fn()} onSubmit={onSubmit} />);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('问题描述'), {
|
||||
target: { value: ' 这个反馈页面无法正常上传图片 ' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('联系电话'), {
|
||||
target: { value: ' 13800000000 ' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交' }));
|
||||
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
description: '这个反馈页面无法正常上传图片',
|
||||
contactPhone: '13800000000',
|
||||
evidenceFiles: [],
|
||||
});
|
||||
await waitFor(() => expect(screen.getByText('反馈已提交')).toBeTruthy());
|
||||
});
|
||||
|
||||
test('PlatformFeedbackView calls back from header home button', () => {
|
||||
const onBack = vi.fn();
|
||||
render(<PlatformFeedbackView onBack={onBack} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '返回我的页签' }));
|
||||
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
322
src/components/platform-entry/PlatformFeedbackView.tsx
Normal file
322
src/components/platform-entry/PlatformFeedbackView.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import { ArrowLeft, CheckCircle2, Home, ImagePlus, Send, X } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
const MIN_FEEDBACK_DESCRIPTION_LENGTH = 10;
|
||||
const MAX_FEEDBACK_DESCRIPTION_LENGTH = 200;
|
||||
const MAX_FEEDBACK_EVIDENCE_COUNT = 4;
|
||||
const MAX_CONTACT_PHONE_LENGTH = 40;
|
||||
|
||||
export type PlatformFeedbackPayload = {
|
||||
description: string;
|
||||
contactPhone: string;
|
||||
evidenceFiles: File[];
|
||||
};
|
||||
|
||||
export type PlatformFeedbackViewProps = {
|
||||
onBack: () => void;
|
||||
onSubmit?: (payload: PlatformFeedbackPayload) => void | Promise<void>;
|
||||
};
|
||||
|
||||
type EvidencePreview = {
|
||||
id: string;
|
||||
file: File;
|
||||
url: string;
|
||||
};
|
||||
|
||||
function buildEvidencePreviewId(file: File, index: number) {
|
||||
return `${file.name}:${file.size}:${file.lastModified}:${index}`;
|
||||
}
|
||||
|
||||
export function PlatformFeedbackView({
|
||||
onBack,
|
||||
onSubmit,
|
||||
}: PlatformFeedbackViewProps) {
|
||||
const evidenceInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [description, setDescription] = useState('');
|
||||
const [contactPhone, setContactPhone] = useState('');
|
||||
const [evidencePreviews, setEvidencePreviews] = useState<EvidencePreview[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const descriptionLength = description.length;
|
||||
const evidenceFiles = useMemo(
|
||||
() => evidencePreviews.map((preview) => preview.file),
|
||||
[evidencePreviews],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
evidencePreviews.forEach((preview) => URL.revokeObjectURL(preview.url));
|
||||
},
|
||||
[evidencePreviews],
|
||||
);
|
||||
|
||||
const showTemporaryNotice = (message: string) => {
|
||||
setNotice(message);
|
||||
window.setTimeout(() => setNotice(null), 1600);
|
||||
};
|
||||
|
||||
const updateDescription = (value: string) => {
|
||||
setDescription(value.slice(0, MAX_FEEDBACK_DESCRIPTION_LENGTH));
|
||||
setError(null);
|
||||
setSubmitted(false);
|
||||
};
|
||||
|
||||
const updateContactPhone = (value: string) => {
|
||||
setContactPhone(value.slice(0, MAX_CONTACT_PHONE_LENGTH));
|
||||
setError(null);
|
||||
setSubmitted(false);
|
||||
};
|
||||
|
||||
const openEvidencePicker = () => {
|
||||
evidenceInputRef.current?.click();
|
||||
};
|
||||
|
||||
const addEvidenceFiles = (files: FileList | null) => {
|
||||
if (evidenceInputRef.current) {
|
||||
evidenceInputRef.current.value = '';
|
||||
}
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedFiles = Array.from(files).filter((file) =>
|
||||
file.type.startsWith('image/'),
|
||||
);
|
||||
const remainingCount = MAX_FEEDBACK_EVIDENCE_COUNT - evidencePreviews.length;
|
||||
if (remainingCount <= 0 || selectedFiles.length > remainingCount) {
|
||||
setError('最多上传四张凭证');
|
||||
}
|
||||
|
||||
const nextFiles = selectedFiles.slice(0, Math.max(remainingCount, 0));
|
||||
if (nextFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextPreviews = nextFiles.map((file, index) => ({
|
||||
id: buildEvidencePreviewId(file, evidencePreviews.length + index),
|
||||
file,
|
||||
url: URL.createObjectURL(file),
|
||||
}));
|
||||
setEvidencePreviews((currentPreviews) => [...currentPreviews, ...nextPreviews]);
|
||||
setSubmitted(false);
|
||||
};
|
||||
|
||||
const removeEvidencePreview = (id: string) => {
|
||||
setEvidencePreviews((currentPreviews) => {
|
||||
const previewToRemove = currentPreviews.find((preview) => preview.id === id);
|
||||
if (previewToRemove) {
|
||||
URL.revokeObjectURL(previewToRemove.url);
|
||||
}
|
||||
return currentPreviews.filter((preview) => preview.id !== id);
|
||||
});
|
||||
setError(null);
|
||||
setSubmitted(false);
|
||||
};
|
||||
|
||||
const submitFeedback = () => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedDescription = description.trim();
|
||||
const trimmedContactPhone = contactPhone.trim();
|
||||
if (trimmedDescription.length < MIN_FEEDBACK_DESCRIPTION_LENGTH) {
|
||||
setError('请填写10个字以上的问题描述');
|
||||
setSubmitted(false);
|
||||
return;
|
||||
}
|
||||
if (trimmedDescription.length > MAX_FEEDBACK_DESCRIPTION_LENGTH) {
|
||||
setError('问题描述不能超过 200 字');
|
||||
setSubmitted(false);
|
||||
return;
|
||||
}
|
||||
if (trimmedContactPhone.length > MAX_CONTACT_PHONE_LENGTH) {
|
||||
setError('联系电话不能超过 40 字');
|
||||
setSubmitted(false);
|
||||
return;
|
||||
}
|
||||
if (evidenceFiles.length > MAX_FEEDBACK_EVIDENCE_COUNT) {
|
||||
setError('最多上传四张凭证');
|
||||
setSubmitted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
// 中文注释:首版反馈页只完成前端收集与成功态;接入后端时在 onSubmit 中替换为 API 调用。
|
||||
void Promise.resolve(
|
||||
onSubmit?.({
|
||||
description: trimmedDescription,
|
||||
contactPhone: trimmedContactPhone,
|
||||
evidenceFiles,
|
||||
}),
|
||||
)
|
||||
.then(() => setSubmitted(true))
|
||||
.catch((submitError: unknown) => {
|
||||
setSubmitted(false);
|
||||
setError(submitError instanceof Error ? submitError.message : '提交失败');
|
||||
})
|
||||
.finally(() => setIsSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto bg-[#f5f6f8] text-[#202124]">
|
||||
<div className="mx-auto flex min-h-full w-full max-w-[30rem] flex-col pb-8">
|
||||
<header className="sticky top-0 z-10 rounded-b-[1.35rem] bg-white px-4 pb-3 pt-4 shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
|
||||
<div className="grid grid-cols-[2.5rem_1fr_5.75rem] items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
aria-label="返回我的页签"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full text-[#24262b] transition hover:bg-slate-100"
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="text-center text-base font-semibold text-[#1f2329]">
|
||||
帮助与反馈
|
||||
</div>
|
||||
<div
|
||||
className="flex h-8 items-center justify-center gap-2 rounded-full border border-[#d8dce3] px-2 text-[#1f2329]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="text-lg leading-none">···</span>
|
||||
<span className="h-4 w-px bg-[#d8dce3]" />
|
||||
<span className="h-0.5 w-3 rounded-full bg-[#1f2329]" />
|
||||
<span className="h-3.5 w-3.5 rounded-full border-2 border-[#1f2329]" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex flex-1 flex-col gap-3 px-4 pt-5">
|
||||
<div className="text-sm font-medium text-[#8b93a1]">反馈问题</div>
|
||||
|
||||
<section className="rounded-2xl bg-white px-4 py-4 shadow-[0_8px_24px_rgba(15,23,42,0.03)]">
|
||||
<label htmlFor="profile-feedback-description" className="block text-base font-semibold text-[#1f2329]">
|
||||
问题描述
|
||||
</label>
|
||||
<textarea
|
||||
id="profile-feedback-description"
|
||||
value={description}
|
||||
maxLength={MAX_FEEDBACK_DESCRIPTION_LENGTH}
|
||||
onChange={(event) => updateDescription(event.target.value)}
|
||||
placeholder="请填写10个字以上的问题描述以便我们提供更好的帮助,温馨提醒您请勿填写身份证号等个人隐私信息。"
|
||||
className="mt-3 min-h-[10.5rem] w-full resize-none border-0 bg-transparent text-sm leading-6 text-[#1f2329] outline-none placeholder:text-[#b4bac4]"
|
||||
/>
|
||||
<div className="text-right text-xs text-[#a5adba]">
|
||||
{descriptionLength}/{MAX_FEEDBACK_DESCRIPTION_LENGTH}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl bg-white px-4 py-4 shadow-[0_8px_24px_rgba(15,23,42,0.03)]">
|
||||
<div className="text-base font-semibold text-[#1f2329]">
|
||||
上传凭证(提供问题截图)
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
{evidencePreviews.map((preview) => (
|
||||
<div
|
||||
key={preview.id}
|
||||
className="relative h-[5.75rem] w-[5.75rem] overflow-hidden rounded-xl border border-[#e3e6eb] bg-[#f7f8fa]"
|
||||
>
|
||||
<img
|
||||
src={preview.url}
|
||||
alt="反馈凭证预览"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeEvidencePreview(preview.id)}
|
||||
aria-label="移除上传凭证"
|
||||
className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/55 text-white"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{evidencePreviews.length < MAX_FEEDBACK_EVIDENCE_COUNT ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openEvidencePicker}
|
||||
className="flex h-[5.75rem] w-[5.75rem] flex-col items-center justify-center rounded-xl border border-dashed border-[#cdd3dc] bg-[#fbfcfd] text-[#9aa3af] transition hover:border-[#2f7cf6] hover:text-[#2f7cf6]"
|
||||
>
|
||||
<ImagePlus className="h-6 w-6" />
|
||||
<span className="mt-2 text-xs font-medium">上传凭证</span>
|
||||
<span className="mt-0.5 text-[11px]">(最多四张)</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<input
|
||||
ref={evidenceInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(event) => addEvidenceFiles(event.target.files)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="rounded-2xl bg-white px-4 py-4 shadow-[0_8px_24px_rgba(15,23,42,0.03)]">
|
||||
<label htmlFor="profile-feedback-phone" className="block text-base font-semibold text-[#1f2329]">
|
||||
联系电话
|
||||
</label>
|
||||
<input
|
||||
id="profile-feedback-phone"
|
||||
type="tel"
|
||||
value={contactPhone}
|
||||
maxLength={MAX_CONTACT_PHONE_LENGTH}
|
||||
onChange={(event) => updateContactPhone(event.target.value)}
|
||||
placeholder="选填,如您填写则将会同步开发者与您联系"
|
||||
className="mt-3 w-full border-0 bg-transparent text-sm leading-6 text-[#1f2329] outline-none placeholder:text-[#b4bac4]"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{error ? (
|
||||
<div className="rounded-2xl bg-rose-50 px-4 py-3 text-sm font-medium text-rose-600">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
{submitted ? (
|
||||
<div className="flex items-center gap-2 rounded-2xl bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
反馈已提交
|
||||
</div>
|
||||
) : null}
|
||||
{notice ? (
|
||||
<div className="rounded-2xl bg-blue-50 px-4 py-3 text-sm font-medium text-[#2f7cf6]">
|
||||
{notice}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitFeedback}
|
||||
disabled={isSubmitting}
|
||||
className="mt-2 flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-[#2f7cf6] text-base font-semibold text-white shadow-[0_10px_22px_rgba(47,124,246,0.26)] transition hover:bg-[#1f6bea] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
{isSubmitting ? '提交中' : '提交'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showTemporaryNotice('反馈记录暂未开放')}
|
||||
className="self-center px-3 py-2 text-sm font-medium text-[#2f7cf6]"
|
||||
>
|
||||
查看反馈与投诉记录
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
className="mt-auto flex items-center justify-center gap-2 self-center px-3 py-2 text-xs font-medium text-[#8b93a1]"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回我的
|
||||
</button>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export type CustomWorldRuntimeLaunchOptions = {
|
||||
|
||||
export type SelectionStage =
|
||||
| 'platform'
|
||||
| 'profile-feedback'
|
||||
| 'work-detail'
|
||||
| 'detail'
|
||||
| 'agent-workspace'
|
||||
|
||||
@@ -134,6 +134,7 @@ export interface RpgEntryHomeViewProps {
|
||||
profilePlayStatsError?: string | null;
|
||||
onCloseProfilePlayStats?: () => void;
|
||||
onOpenPlayedWork?: (work: ProfilePlayedWorkSummary) => void;
|
||||
onOpenFeedback?: () => void;
|
||||
onRechargeSuccess?: () => void | Promise<void>;
|
||||
createTabContent?: ReactNode;
|
||||
}
|
||||
@@ -2679,6 +2680,7 @@ export function RpgEntryHomeView({
|
||||
profilePlayStatsError = null,
|
||||
onCloseProfilePlayStats,
|
||||
onOpenPlayedWork,
|
||||
onOpenFeedback,
|
||||
onRechargeSuccess,
|
||||
createTabContent,
|
||||
}: RpgEntryHomeViewProps) {
|
||||
@@ -3996,6 +3998,12 @@ export function RpgEntryHomeView({
|
||||
icon={MessageCircle}
|
||||
onClick={() => openProfilePopupPanel('community')}
|
||||
/>
|
||||
<ProfileShortcutButton
|
||||
label="反馈"
|
||||
subLabel="问题与建议"
|
||||
icon={MessageCircle}
|
||||
onClick={onOpenFeedback}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,78 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
APP_RUNTIME_ROUTES,
|
||||
buildPublicWorkDetailPath,
|
||||
buildPublicWorkDetailUrl,
|
||||
buildPublicWorkStagePath,
|
||||
isKnownMainAppPagePath,
|
||||
normalizeAppPath,
|
||||
readPublicWorkCodeFromLocationSearch,
|
||||
resolvePathForSelectionStage,
|
||||
resolveSelectionStageFromPath,
|
||||
} from './appPageRoutes';
|
||||
|
||||
describe('appPageRoutes', () => {
|
||||
it('normalizes page paths for stable matching', () => {
|
||||
expect(normalizeAppPath('')).toBe('/');
|
||||
expect(normalizeAppPath('/CREATION/RPG/AGENT/')).toBe(
|
||||
'/creation/rpg/agent',
|
||||
it('resolves profile feedback route', () => {
|
||||
expect(resolveSelectionStageFromPath('/profile/feedback')).toBe(
|
||||
'profile-feedback',
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves platform entry stages from independent paths', () => {
|
||||
expect(resolveSelectionStageFromPath('/creation/rpg/agent')).toBe(
|
||||
'agent-workspace',
|
||||
expect(resolveSelectionStageFromPath('/profile/feedback/')).toBe(
|
||||
'profile-feedback',
|
||||
);
|
||||
expect(resolveSelectionStageFromPath('/creation/big-fish/result/')).toBe(
|
||||
'big-fish-result',
|
||||
);
|
||||
expect(resolveSelectionStageFromPath('/creation/match3d/result')).toBe(
|
||||
'match3d-result',
|
||||
);
|
||||
expect(resolveSelectionStageFromPath('/gallery/puzzle/detail')).toBe(
|
||||
'puzzle-gallery-detail',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to platform for unknown paths inside the main app', () => {
|
||||
expect(resolveSelectionStageFromPath('/missing')).toBe('platform');
|
||||
});
|
||||
|
||||
it('resolves paths from selection stages', () => {
|
||||
expect(resolvePathForSelectionStage('custom-world-generating')).toBe(
|
||||
'/creation/rpg/generating',
|
||||
);
|
||||
expect(resolvePathForSelectionStage('puzzle-runtime')).toBe(
|
||||
'/runtime/puzzle',
|
||||
);
|
||||
});
|
||||
|
||||
it('recognizes runtime pages as main app pages', () => {
|
||||
expect(
|
||||
isKnownMainAppPagePath(APP_RUNTIME_ROUTES['rpg-character-select']),
|
||||
).toBe(true);
|
||||
expect(isKnownMainAppPagePath('/runtime/rpg/adventure/')).toBe(true);
|
||||
});
|
||||
|
||||
it('builds and reads public work detail query routes', () => {
|
||||
expect(buildPublicWorkDetailPath('CW-00000001')).toBe(
|
||||
'/works/detail?work=CW-00000001',
|
||||
);
|
||||
expect(
|
||||
buildPublicWorkDetailUrl('CW-00000001', 'https://example.test'),
|
||||
).toBe('https://example.test/works/detail?work=CW-00000001');
|
||||
expect(readPublicWorkCodeFromLocationSearch('?work=CW-00000001')).toBe(
|
||||
'CW-00000001',
|
||||
);
|
||||
expect(
|
||||
buildPublicWorkStagePath('puzzle-gallery-detail', 'PZ-00000002'),
|
||||
).toBe('/gallery/puzzle/detail?work=PZ-00000002');
|
||||
expect(buildPublicWorkStagePath('big-fish-runtime', 'BF-00000003')).toBe(
|
||||
'/runtime/big-fish?work=BF-00000003',
|
||||
);
|
||||
expect(buildPublicWorkStagePath('match3d-runtime', 'M3-00000004')).toBe(
|
||||
'/runtime/match3d?work=M3-00000004',
|
||||
expect(resolvePathForSelectionStage('profile-feedback')).toBe(
|
||||
'/profile/feedback',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ export const PUBLIC_WORK_QUERY_PARAM = 'work';
|
||||
|
||||
const STAGE_ROUTE_ENTRIES = [
|
||||
['platform', '/'],
|
||||
['profile-feedback', '/profile/feedback'],
|
||||
['work-detail', '/works/detail'],
|
||||
['detail', '/worlds/detail'],
|
||||
['agent-workspace', '/creation/rpg/agent'],
|
||||
|
||||
Reference in New Issue
Block a user