修正画布路由与刷新加载
缺少项目 ID 的画布直达链接会回到创作页 路由级黑底加载只保留给移动端推荐页 补充画布直达和推荐加载开关回归测试
This commit is contained in:
@@ -294,6 +294,61 @@ describe('App navigation history', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('缺少 projectid 的项目画布直达会替换回创作页', () => {
|
||||
window.history.replaceState(null, '', '/editor/canvas');
|
||||
const replaceStateSpy = vi.spyOn(window.history, 'replaceState');
|
||||
|
||||
renderApp();
|
||||
|
||||
expect(screen.getByTestId('selection-stage').textContent).toBe(
|
||||
'creation-home',
|
||||
);
|
||||
expect(window.location.pathname).toBe('/creation');
|
||||
expect(window.location.search).toBe('');
|
||||
expect(replaceStateSpy).toHaveBeenCalledWith(
|
||||
{ [APP_HISTORY_STATE_KEY]: true },
|
||||
'',
|
||||
'/creation',
|
||||
);
|
||||
|
||||
replaceStateSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('带 projectid 的项目画布直达保持编辑器阶段', () => {
|
||||
mockMatchMedia(false);
|
||||
window.history.replaceState(
|
||||
null,
|
||||
'',
|
||||
'/editor/canvas?projectid=project-from-url',
|
||||
);
|
||||
|
||||
renderApp();
|
||||
|
||||
expect(screen.getByTestId('selection-stage').textContent).toBe(
|
||||
'image-editor',
|
||||
);
|
||||
expect(window.location.pathname).toBe('/editor/canvas');
|
||||
expect(window.location.search).toBe('?projectid=project-from-url');
|
||||
});
|
||||
|
||||
test('历史回到缺少 projectid 的项目画布时会替换回创作页', async () => {
|
||||
mockMatchMedia(false);
|
||||
window.history.replaceState(null, '', '/creation');
|
||||
|
||||
renderApp();
|
||||
|
||||
window.history.pushState(null, '', '/editor/canvas');
|
||||
await act(async () => {
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('selection-stage').textContent).toBe(
|
||||
'creation-home',
|
||||
);
|
||||
expect(window.location.pathname).toBe('/creation');
|
||||
expect(window.location.search).toBe('');
|
||||
});
|
||||
|
||||
test('项目画布导航只写入一次带 projectid 的历史记录', async () => {
|
||||
mockMatchMedia(true);
|
||||
window.history.replaceState(null, '', '/creation');
|
||||
|
||||
+32
-5
@@ -25,6 +25,7 @@ import {
|
||||
readPublicWorkCodeFromLocationSearch,
|
||||
resolveInitialSelectionStageFromPath,
|
||||
resolvePathForSelectionStage,
|
||||
shouldRedirectEditorCanvasWithoutProject,
|
||||
} from './routing/appPageRoutes';
|
||||
import type { RpgRuntimeAppIntent } from './RpgRuntimeApp';
|
||||
import {
|
||||
@@ -62,6 +63,23 @@ function isRpgRuntimeRoute(pathname: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveInitialAppSelectionStage() {
|
||||
if (
|
||||
shouldRedirectEditorCanvasWithoutProject(
|
||||
window.location.pathname,
|
||||
window.location.search,
|
||||
)
|
||||
) {
|
||||
replaceAppHistoryPath('/creation');
|
||||
return 'creation-home';
|
||||
}
|
||||
|
||||
return resolveInitialSelectionStageFromPath(
|
||||
window.location.pathname,
|
||||
getInitialPlatformDesktopLayout(),
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const authUi = useAuthUi();
|
||||
const runtimeIntentTokenRef = useRef(0);
|
||||
@@ -75,11 +93,8 @@ export default function App() {
|
||||
const [isRuntimeActive, setIsRuntimeActive] = useState(() =>
|
||||
isRpgRuntimeRoute(window.location.pathname),
|
||||
);
|
||||
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(() =>
|
||||
resolveInitialSelectionStageFromPath(
|
||||
window.location.pathname,
|
||||
getInitialPlatformDesktopLayout(),
|
||||
),
|
||||
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(
|
||||
resolveInitialAppSelectionStage,
|
||||
);
|
||||
const [runtimeReturnStage, setRuntimeReturnStage] =
|
||||
useState<SelectionStage>('platform');
|
||||
@@ -111,6 +126,18 @@ export default function App() {
|
||||
window.history.state,
|
||||
);
|
||||
|
||||
if (
|
||||
shouldRedirectEditorCanvasWithoutProject(
|
||||
window.location.pathname,
|
||||
window.location.search,
|
||||
)
|
||||
) {
|
||||
replaceAppHistoryPath('/creation');
|
||||
setIsRuntimeActive(false);
|
||||
setRawSelectionStage('creation-home');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRpgRuntimeRoute(window.location.pathname)) {
|
||||
setIsRuntimeActive(true);
|
||||
return;
|
||||
|
||||
+30
-8
@@ -6,9 +6,13 @@ import {StrictMode, Suspense} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
|
||||
import {FloatingFeedbackEntry} from './components/common/FloatingFeedbackEntry';
|
||||
import {getInitialPlatformDesktopLayout} from './components/platform-entry/platformEntryResponsive';
|
||||
import {stabilizeMobileViewportKeyboardFocus} from './mobileViewportKeyboardFocus';
|
||||
import {lockMobileViewportZoom} from './mobileViewportZoomLock';
|
||||
import {resolveAppRoute} from './routing/appRoutes';
|
||||
import {
|
||||
resolveAppRoute,
|
||||
shouldUseRecommendationRouteLoading,
|
||||
} from './routing/appRoutes';
|
||||
import {RouteImageReadyGate} from './routing/RouteImageReadyGate';
|
||||
import {RouteLoadingScreen} from './routing/RouteLoadingScreen';
|
||||
import {
|
||||
@@ -39,6 +43,11 @@ function markWechatMiniProgramRuntime() {
|
||||
|
||||
const root = window.__tavernRealmsRoot__ ??= createRoot(rootElement);
|
||||
const RouteComponent = route.Component;
|
||||
const routeElement = <RouteComponent {...(route.componentProps ?? {})} />;
|
||||
const shouldGateRouteImages = shouldUseRecommendationRouteLoading(
|
||||
window.location.pathname,
|
||||
getInitialPlatformDesktopLayout(),
|
||||
);
|
||||
|
||||
lockMobileViewportZoom();
|
||||
stabilizeMobileViewportKeyboardFocus();
|
||||
@@ -47,13 +56,26 @@ void refreshNativeAppHostRuntime();
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Suspense fallback={<RouteLoadingScreen eyebrow={route.loadingEyebrow} text={route.loadingText} />}>
|
||||
<RouteImageReadyGate
|
||||
eyebrow={route.loadingEyebrow}
|
||||
text={route.loadingText}
|
||||
>
|
||||
<RouteComponent {...(route.componentProps ?? {})} />
|
||||
</RouteImageReadyGate>
|
||||
<Suspense
|
||||
fallback={
|
||||
shouldGateRouteImages ? (
|
||||
<RouteLoadingScreen
|
||||
eyebrow={route.loadingEyebrow}
|
||||
text={route.loadingText}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{shouldGateRouteImages ? (
|
||||
<RouteImageReadyGate
|
||||
eyebrow={route.loadingEyebrow}
|
||||
text={route.loadingText}
|
||||
>
|
||||
{routeElement}
|
||||
</RouteImageReadyGate>
|
||||
) : (
|
||||
routeElement
|
||||
)}
|
||||
</Suspense>
|
||||
<FloatingFeedbackEntry />
|
||||
</StrictMode>,
|
||||
|
||||
@@ -5,9 +5,11 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
isAppHistoryState,
|
||||
pushAppHistoryPath,
|
||||
readEditorCanvasProjectIdFromLocationSearch,
|
||||
replaceAppHistoryPath,
|
||||
resolvePathForSelectionStage,
|
||||
resolveSelectionStageFromPath,
|
||||
shouldRedirectEditorCanvasWithoutProject,
|
||||
} from './appPageRoutes';
|
||||
|
||||
describe('appPageRoutes', () => {
|
||||
@@ -39,6 +41,27 @@ describe('appPageRoutes', () => {
|
||||
expect(window.location.search).toBe('?projectid=project-from-route-test');
|
||||
});
|
||||
|
||||
it('detects editor canvas routes that are missing project id', () => {
|
||||
expect(readEditorCanvasProjectIdFromLocationSearch('?projectid=abc')).toBe(
|
||||
'abc',
|
||||
);
|
||||
expect(shouldRedirectEditorCanvasWithoutProject('/editor/canvas', '')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
shouldRedirectEditorCanvasWithoutProject('/EDITOR/CANVAS/', '?foo=bar'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldRedirectEditorCanvasWithoutProject(
|
||||
'/editor/canvas',
|
||||
'?projectid=project-from-route-test',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(shouldRedirectEditorCanvasWithoutProject('/creation', '')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves the project route', () => {
|
||||
expect(resolveSelectionStageFromPath('/project')).toBe('project');
|
||||
expect(resolveSelectionStageFromPath('/PROJECT/')).toBe('project');
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
export type RuntimePageRoute = 'rpg-character-select' | 'rpg-adventure';
|
||||
|
||||
export const PUBLIC_WORK_QUERY_PARAM = 'work';
|
||||
export const EDITOR_CANVAS_PROJECT_ID_QUERY_PARAM = 'projectid';
|
||||
|
||||
const STAGE_ROUTE_ENTRIES = [
|
||||
['platform', '/'],
|
||||
@@ -122,6 +123,21 @@ export function readPublicWorkCodeFromLocationSearch(search: string) {
|
||||
return params.get(PUBLIC_WORK_QUERY_PARAM)?.trim() || null;
|
||||
}
|
||||
|
||||
export function readEditorCanvasProjectIdFromLocationSearch(search: string) {
|
||||
const params = new URLSearchParams(search);
|
||||
return params.get(EDITOR_CANVAS_PROJECT_ID_QUERY_PARAM)?.trim() || null;
|
||||
}
|
||||
|
||||
export function shouldRedirectEditorCanvasWithoutProject(
|
||||
pathname: string,
|
||||
search: string,
|
||||
) {
|
||||
return (
|
||||
normalizeAppPath(pathname) === APP_STAGE_ROUTES['image-editor'] &&
|
||||
!readEditorCanvasProjectIdFromLocationSearch(search)
|
||||
);
|
||||
}
|
||||
|
||||
export function buildPublicWorkDetailPath(publicWorkCode: string) {
|
||||
return buildPublicWorkStagePath('work-detail', publicWorkCode);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { matchAppRoute } from './appRoutes';
|
||||
import { matchAppRoute, shouldUseRecommendationRouteLoading } from './appRoutes';
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
@@ -89,3 +89,15 @@ describe('matchAppRoute', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldUseRecommendationRouteLoading', () => {
|
||||
it('only enables the dark recommendation loading screen for mobile root route', () => {
|
||||
expect(shouldUseRecommendationRouteLoading('/', false)).toBe(true);
|
||||
expect(shouldUseRecommendationRouteLoading('/', true)).toBe(false);
|
||||
expect(shouldUseRecommendationRouteLoading('/creation', false)).toBe(false);
|
||||
expect(shouldUseRecommendationRouteLoading('/project', false)).toBe(false);
|
||||
expect(shouldUseRecommendationRouteLoading('/editor/canvas', false)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,13 @@ function normalizeRoutePath(pathname: string) {
|
||||
return normalizeAppPath(pathname);
|
||||
}
|
||||
|
||||
export function shouldUseRecommendationRouteLoading(
|
||||
pathname: string,
|
||||
isDesktopLayout: boolean,
|
||||
) {
|
||||
return normalizeRoutePath(pathname) === '/' && !isDesktopLayout;
|
||||
}
|
||||
|
||||
export function matchAppRoute(pathname: string): AppRouteMatch {
|
||||
const normalizedPath = normalizeRoutePath(pathname);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user