Keep Issues and artist detail history stable
- Route Issues to the React host even while the shell is still booting - Ignore stale bootstrap work when navigation changes mid-load - Clear artist-detail state when leaving the page so browser back can reach Library - Add smoke coverage for the artist-detail back-navigation path
This commit is contained in:
parent
8d6ab4eb74
commit
50c2d6882c
6 changed files with 92 additions and 4 deletions
|
|
@ -1,5 +1,7 @@
|
|||
// SoulSync WebUI JavaScript - Replicating PyQt6 GUI Functionality
|
||||
|
||||
const PAGE_WILL_CHANGE_EVENT = 'ss:webui-page-will-change';
|
||||
|
||||
// Global state management
|
||||
let currentPage = 'dashboard';
|
||||
let currentTrack = null;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
// INITIALIZATION
|
||||
// ===============================
|
||||
let navigationEpoch = 0;
|
||||
|
||||
function notifyPageWillChange(nextPageId) {
|
||||
const fromPageId = typeof currentPage === 'string' ? currentPage : null;
|
||||
if (fromPageId === nextPageId) return;
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(PAGE_WILL_CHANGE_EVENT, {
|
||||
detail: {
|
||||
fromPageId,
|
||||
toPageId: nextPageId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Accent Color System ----
|
||||
|
||||
|
|
@ -2309,6 +2324,8 @@ function initializeWatchlist() {
|
|||
}
|
||||
|
||||
function navigateToPage(pageId, options = {}) {
|
||||
navigationEpoch += 1;
|
||||
|
||||
if (!options.forceReload && pageId === currentPage) return;
|
||||
|
||||
// Permission guard — redirect to home page if not allowed
|
||||
|
|
@ -2322,11 +2339,25 @@ function navigateToPage(pageId, options = {}) {
|
|||
|
||||
const router = getWebRouter();
|
||||
if (router && !options.skipRouteChange) {
|
||||
notifyPageWillChange(pageId);
|
||||
const route = router.routeManifest?.find((entry) => entry.pageId === pageId);
|
||||
if (route?.kind === 'react') {
|
||||
showReactHost(pageId);
|
||||
setActivePageChrome(pageId);
|
||||
}
|
||||
return router.navigateToPage(pageId, { replace: options.replace === true });
|
||||
}
|
||||
|
||||
// Fallback path for initial bootstrap or environments without TanStack routing.
|
||||
activatePage(pageId, { forceReload: options.forceReload === true });
|
||||
const route = router?.routeManifest?.find((entry) => entry.pageId === pageId);
|
||||
notifyPageWillChange(pageId);
|
||||
const legacyPageElement = document.getElementById(`${pageId}-page`);
|
||||
if (route?.kind === 'react' || !legacyPageElement) {
|
||||
showReactHost(pageId);
|
||||
setActivePageChrome(pageId);
|
||||
} else {
|
||||
activatePage(pageId, { forceReload: options.forceReload === true });
|
||||
}
|
||||
|
||||
if (!options.skipPushState) {
|
||||
const urlPath = pageId === 'dashboard' ? '/' : '/' + pageId;
|
||||
|
|
|
|||
|
|
@ -700,6 +700,27 @@ let artistDetailPageState = {
|
|||
enhancedTrackSort: {}
|
||||
};
|
||||
|
||||
function clearArtistDetailPageState() {
|
||||
if (artistDetailPageState.completionController) {
|
||||
artistDetailPageState.completionController.abort();
|
||||
artistDetailPageState.completionController = null;
|
||||
}
|
||||
|
||||
artistDetailPageState.currentArtistId = null;
|
||||
artistDetailPageState.currentArtistName = null;
|
||||
artistDetailPageState.currentArtistSource = null;
|
||||
artistDetailPageState.originStack = [];
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener(PAGE_WILL_CHANGE_EVENT, (event) => {
|
||||
const detail = event.detail || {};
|
||||
if (detail.fromPageId === 'artist-detail' && detail.toPageId !== 'artist-detail') {
|
||||
clearArtistDetailPageState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Discography filter state
|
||||
let discographyFilterState = {
|
||||
categories: { albums: true, eps: true, singles: true },
|
||||
|
|
|
|||
|
|
@ -1161,6 +1161,9 @@ async function startDownload(index) {
|
|||
|
||||
async function loadInitialData() {
|
||||
try {
|
||||
const initialPath = window.location.pathname;
|
||||
const initialNavigationEpoch = navigationEpoch;
|
||||
|
||||
// Load artist bubble state first
|
||||
await hydrateArtistBubblesFromSnapshot();
|
||||
|
||||
|
|
@ -1177,6 +1180,10 @@ async function loadInitialData() {
|
|||
? urlPage
|
||||
: homePage;
|
||||
|
||||
if (window.location.pathname !== initialPath || navigationEpoch !== initialNavigationEpoch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always apply the target page to the legacy shell chrome.
|
||||
const router = getWebRouter();
|
||||
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ function activateLegacyPath(pathname) {
|
|||
return;
|
||||
}
|
||||
|
||||
notifyPageWillChange(targetPage);
|
||||
activatePage(targetPage, { forceReload: true });
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +78,7 @@ function syncActivePageFromLocation() {
|
|||
return;
|
||||
}
|
||||
|
||||
notifyPageWillChange(targetPage);
|
||||
const route = router?.routeManifest?.find((entry) => entry.pageId === targetPage);
|
||||
if (route?.kind === 'react') {
|
||||
showReactHost(targetPage);
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ async function selectProfile(page: Page, baseURL: string, profileId = 1) {
|
|||
async function waitForShellRoute(page: Page, pageId: string) {
|
||||
if (pageId === 'issues') {
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(() => document.querySelector('.page.active')?.id ?? ''),
|
||||
)
|
||||
.poll(async () => page.evaluate(() => document.querySelector('.page.active')?.id ?? ''), {
|
||||
timeout: 15000,
|
||||
})
|
||||
.toBe('webui-react-root');
|
||||
await expect(page.getByTestId('issues-board')).toBeVisible({ timeout: 15000 });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -106,3 +107,27 @@ test('browser history restores top-level routes', async ({ page, baseURL }) => {
|
|||
await waitForShellRoute(page, 'issues');
|
||||
await expect(page).toHaveURL(/\/issues(?:\?status=open&category=all)?$/);
|
||||
});
|
||||
|
||||
test('browser history leaves artist detail when going back to library', async ({
|
||||
page,
|
||||
baseURL,
|
||||
}) => {
|
||||
if (!baseURL) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
await selectProfile(page, baseURL);
|
||||
|
||||
await page.goto(new URL('/library', baseURL).toString(), { waitUntil: 'domcontentloaded' });
|
||||
await waitForShellRoute(page, 'library');
|
||||
await expect.poll(async () => page.locator('.library-artist-card').count()).toBeGreaterThan(0);
|
||||
|
||||
await page.locator('.library-artist-card').first().click();
|
||||
await waitForShellRoute(page, 'artist-detail');
|
||||
await expect(page).toHaveURL(/\/artist-detail$/);
|
||||
|
||||
await page.goBack();
|
||||
await waitForShellRoute(page, 'library');
|
||||
await expect(page).toHaveURL(/\/library$/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue