1
This commit is contained in:
48
src/routing/appPageRoutes.test.ts
Normal file
48
src/routing/appPageRoutes.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
APP_RUNTIME_ROUTES,
|
||||
isKnownMainAppPagePath,
|
||||
normalizeAppPath,
|
||||
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 platform entry stages from independent paths', () => {
|
||||
expect(resolveSelectionStageFromPath('/creation/rpg/agent')).toBe(
|
||||
'agent-workspace',
|
||||
);
|
||||
expect(resolveSelectionStageFromPath('/creation/big-fish/result/')).toBe(
|
||||
'big-fish-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);
|
||||
});
|
||||
});
|
||||
69
src/routing/appPageRoutes.ts
Normal file
69
src/routing/appPageRoutes.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { SelectionStage } from '../components/platform-entry';
|
||||
|
||||
export type RuntimePageRoute = 'rpg-character-select' | 'rpg-adventure';
|
||||
|
||||
const STAGE_ROUTE_ENTRIES = [
|
||||
['platform', '/'],
|
||||
['detail', '/worlds/detail'],
|
||||
['agent-workspace', '/creation/rpg/agent'],
|
||||
['custom-world-generating', '/creation/rpg/generating'],
|
||||
['custom-world-result', '/creation/rpg/result'],
|
||||
['big-fish-agent-workspace', '/creation/big-fish/agent'],
|
||||
['big-fish-result', '/creation/big-fish/result'],
|
||||
['big-fish-runtime', '/runtime/big-fish'],
|
||||
['puzzle-agent-workspace', '/creation/puzzle/agent'],
|
||||
['puzzle-result', '/creation/puzzle/result'],
|
||||
['puzzle-gallery-detail', '/gallery/puzzle/detail'],
|
||||
['puzzle-runtime', '/runtime/puzzle'],
|
||||
] as const satisfies readonly (readonly [SelectionStage, string])[];
|
||||
|
||||
export const APP_STAGE_ROUTES: Record<SelectionStage, string> =
|
||||
Object.fromEntries(STAGE_ROUTE_ENTRIES) as Record<SelectionStage, string>;
|
||||
|
||||
export const APP_RUNTIME_ROUTES: Record<RuntimePageRoute, string> = {
|
||||
'rpg-character-select': '/runtime/rpg/characters',
|
||||
'rpg-adventure': '/runtime/rpg/adventure',
|
||||
};
|
||||
|
||||
const ROUTE_STAGE_BY_PATH = new Map(
|
||||
STAGE_ROUTE_ENTRIES.map(([stage, path]) => [path, stage] as const),
|
||||
) as Map<string, SelectionStage>;
|
||||
|
||||
export function normalizeAppPath(pathname: string) {
|
||||
const trimmedPathname = pathname.trim().toLowerCase();
|
||||
|
||||
if (!trimmedPathname || trimmedPathname === '/') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return trimmedPathname.replace(/\/+$/u, '');
|
||||
}
|
||||
|
||||
export function resolveSelectionStageFromPath(
|
||||
pathname: string,
|
||||
): SelectionStage {
|
||||
return ROUTE_STAGE_BY_PATH.get(normalizeAppPath(pathname)) ?? 'platform';
|
||||
}
|
||||
|
||||
export function resolvePathForSelectionStage(stage: SelectionStage) {
|
||||
return APP_STAGE_ROUTES[stage] ?? APP_STAGE_ROUTES.platform;
|
||||
}
|
||||
|
||||
export function isKnownMainAppPagePath(pathname: string) {
|
||||
const normalizedPath = normalizeAppPath(pathname);
|
||||
const runtimePaths: readonly string[] = Object.values(APP_RUNTIME_ROUTES);
|
||||
return (
|
||||
ROUTE_STAGE_BY_PATH.has(normalizedPath) ||
|
||||
runtimePaths.includes(normalizedPath)
|
||||
);
|
||||
}
|
||||
|
||||
export function pushAppHistoryPath(path: string) {
|
||||
const normalizedPath = normalizeAppPath(path);
|
||||
if (normalizeAppPath(window.location.pathname) === normalizedPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 页面阶段变化是用户可感知导航,写入 history 以支持前进后退。
|
||||
window.history.pushState(null, '', normalizedPath);
|
||||
}
|
||||
@@ -33,6 +33,18 @@ describe('matchAppRoute', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('routes independent page paths to the main app shell', () => {
|
||||
expect(matchAppRoute('/creation/rpg/agent')).toEqual({
|
||||
kind: 'game',
|
||||
});
|
||||
expect(matchAppRoute('/runtime/puzzle')).toEqual({
|
||||
kind: 'game',
|
||||
});
|
||||
expect(matchAppRoute('/runtime/big-fish')).toEqual({
|
||||
kind: 'game',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not treat unrelated prefixes as preset editor routes', () => {
|
||||
expect(matchAppRoute('/npc-editorial')).toEqual({
|
||||
kind: 'game',
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { type ComponentType, lazy, type LazyExoticComponent } from 'react';
|
||||
|
||||
import { normalizeAppPath } from './appPageRoutes';
|
||||
|
||||
type AppRouteComponent = LazyExoticComponent<
|
||||
ComponentType<Record<string, unknown>>
|
||||
>;
|
||||
@@ -30,13 +32,7 @@ const BigFishPlaygroundApp = lazy(() => import('../BigFishPlaygroundApp')) as Ap
|
||||
const PuzzlePlaygroundApp = lazy(() => import('../PuzzlePlaygroundApp')) as AppRouteComponent;
|
||||
|
||||
function normalizeRoutePath(pathname: string) {
|
||||
const trimmedPathname = pathname.trim().toLowerCase();
|
||||
|
||||
if (!trimmedPathname || trimmedPathname === '/') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return trimmedPathname.replace(/\/+$/u, '');
|
||||
return normalizeAppPath(pathname);
|
||||
}
|
||||
|
||||
export function matchAppRoute(pathname: string): AppRouteMatch {
|
||||
|
||||
Reference in New Issue
Block a user