From 3e12c487691e461e20057ad4136cac4221c0cc2e Mon Sep 17 00:00:00 2001 From: Matthew White Date: Sat, 15 Aug 2026 14:15:25 -0400 Subject: [PATCH 1/2] mockHttp(): don't put time limit on import of async route components --- apps/central/src/util/load-async.js | 27 +++++++++++++++++++++++---- apps/central/test/util/http.js | 13 +++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/central/src/util/load-async.js b/apps/central/src/util/load-async.js index 48dd53e37..d4309daaf 100644 --- a/apps/central/src/util/load-async.js +++ b/apps/central/src/util/load-async.js @@ -18,13 +18,24 @@ loadAsync() has a couple of benefits: - webpack magic comments will not be repeated across files. */ +let loadingCount = 0; + const loader = (load) => { const obj = { loaded: false, load: async () => { - const m = await load(); - obj.loaded = true; - return m; + loadingCount += 1; + try { + const m = await load(); + obj.loaded = true; + return m; + } catch (error) { + // eslint-disable-next-line no-console + if (import.meta.env.NODE_ENV === 'development') console.error(error); + throw error; + } finally { + loadingCount -= 1; + } } }; return obj; @@ -212,7 +223,15 @@ const loaders = new Map() export const loadAsync = (name) => loaders.get(name).load; export const loadedAsync = (name) => loaders.get(name).loaded; -// Exported for use in testing + + +//////////////////////////////////////////////////////////////////////////////// +// TEST UTILS + +// These functions are exported for use in testing. + +export const loadingAnyAsync = () => loadingCount !== 0; + export const setLoader = (name, load) => { loaders.set(name, loader(load)); }; diff --git a/apps/central/test/util/http.js b/apps/central/test/util/http.js index faede74cd..c67b83cf7 100644 --- a/apps/central/test/util/http.js +++ b/apps/central/test/util/http.js @@ -278,6 +278,7 @@ import { clone, identity, last, pick } from 'ramda'; import App from '../../src/components/app.vue'; +import { loadingAnyAsync } from '../../src/util/load-async'; import { noop } from '../../src/util/util'; import { routeProps } from '../../src/util/router'; @@ -643,24 +644,24 @@ class MockHttp { : noop; try { - const routeBefore = router != null ? router.currentRoute.value : null; if (this._location != null) await router.push(this._location); if (this._mount != null) { this._component = this._mount(); // Mounting may have triggered the initial navigation. if (router != null) await router.isReady(); } + // If there was a navigation, then we need to wait for any async + // components associated with the route to load. + await waitUntil(() => !loadingAnyAsync()); if (this._request != null) { - // If there has been a navigation, then wait for any async components - // associated with the route to load. - if (router != null && router.currentRoute.value !== routeBefore) - await wait(); - this._checkStateBeforeRequest(); await this._request(this._component); } } finally { + // Wait for any router navigation to finish. + if (router != null) await wait(); + await waitUntil(() => !loadingAnyAsync()); // Wait for any responses to be processed. await wait(); if (pollWork != null) await waitUntil(() => pollWork(this._component)); From c813fb779e48b01a00f978ef15c7b930625bdf43 Mon Sep 17 00:00:00 2001 From: Matthew White Date: Sat, 15 Aug 2026 14:15:26 -0400 Subject: [PATCH 2/2] Load async route components as needed rather than all at once --- apps/central/src/util/load-async.js | 9 +++++-- apps/central/test/index.js | 5 ---- apps/central/test/util/http.js | 35 ++++++++++++++-------------- apps/central/test/util/load-async.js | 28 ---------------------- 4 files changed, 24 insertions(+), 53 deletions(-) delete mode 100644 apps/central/test/util/load-async.js diff --git a/apps/central/src/util/load-async.js b/apps/central/src/util/load-async.js index d4309daaf..8d2f377c6 100644 --- a/apps/central/src/util/load-async.js +++ b/apps/central/src/util/load-async.js @@ -220,8 +220,13 @@ const loaders = new Map() '../components/user/list.vue' ))); -export const loadAsync = (name) => loaders.get(name).load; -export const loadedAsync = (name) => loaders.get(name).loaded; +const getLoader = (name) => { + if (!loaders.has(name)) throw new Error(`loader not found for ${name}`); + return loaders.get(name); +}; + +export const loadAsync = (name) => getLoader(name).load; +export const loadedAsync = (name) => getLoader(name).loaded; diff --git a/apps/central/test/index.js b/apps/central/test/index.js index 9eb0b2b6c..ab6e8c176 100644 --- a/apps/central/test/index.js +++ b/apps/central/test/index.js @@ -5,7 +5,6 @@ import { expect, should } from 'chai'; import '../src/styles'; import testData from './data'; -import { loadAsyncRouteComponents } from './util/load-async'; import { mockLogin } from './util/session'; import { setupLanguages } from './util/i18n'; import './assertions'; @@ -22,10 +21,6 @@ window.expect = expect; //////////////////////////////////////////////////////////////////////////////// // HOOKS -// Even if a route is lazy-loaded, load() will need synchronous access to the -// async components associated with the route. -beforeAll(loadAsyncRouteComponents); - enableAutoUnmount(afterEach); afterEach(() => { const app = document.querySelector('[data-v-app]'); diff --git a/apps/central/test/util/http.js b/apps/central/test/util/http.js index c67b83cf7..faed8eee0 100644 --- a/apps/central/test/util/http.js +++ b/apps/central/test/util/http.js @@ -278,7 +278,7 @@ import { clone, identity, last, pick } from 'ramda'; import App from '../../src/components/app.vue'; -import { loadingAnyAsync } from '../../src/util/load-async'; +import { loadAsync, loadingAnyAsync } from '../../src/util/load-async'; import { noop } from '../../src/util/util'; import { routeProps } from '../../src/util/router'; @@ -286,7 +286,6 @@ import createTestContainer from './container'; import requestDataByComponent from './http/data'; import testData from '../data'; import * as commonTests from './http/common'; -import { loadAsyncCache } from './load-async'; import { mockAxiosError, mockResponse } from './axios'; import { mockRouter, setInstallLocation, testRouter } from './router'; import { mount as lifecycleMount, withSetup } from './lifecycle'; @@ -296,20 +295,13 @@ import { wait, waitUntil } from './util'; const routeResolver = createTestContainer({ router: testRouter() }).router; const resolveRoute = (location) => routeResolver.resolve(location); // Returns the components associated with a route. If the route is lazy-loaded, -// any async component will be unwrapped from AsyncRoute. +// any async component will be unwrapped from AsyncRoute. If a component is +// async, only its name will be returned, not the full component. const routeComponents = (route) => route.matched.map(routeRecord => { const { asyncRoute } = routeRecord.meta; - if (asyncRoute == null) return routeRecord.components.default; - - const m = loadAsyncCache.get(asyncRoute.componentName); - if (m == null) { - // eslint-disable-next-line no-console - console.error(`Component ${asyncRoute.componentName} was not found in the loadAsync() cache.`); - // eslint-disable-next-line no-console - console.error('The loadAsync() cache contains', loadAsyncCache.size, 'entries.'); - throw new Error('component not found in loadAsync() cache'); - } - return m.default; + return asyncRoute == null + ? routeRecord.components.default + : { name: asyncRoute.componentName }; }); class MockHttp { @@ -385,8 +377,11 @@ class MockHttp { ? containerOption : createTestContainer(containerOption)); - const mount = () => { - const wrapper = lifecycleMount(component, { ...options, container }); + const mount = async () => { + const loadedComponent = typeof component === 'string' + ? (await loadAsync(component)()).default + : component; + const wrapper = lifecycleMount(loadedComponent, { ...options, container }); if (throwIfEmit != null) { const emitted = wrapper.emitted(); @@ -646,7 +641,7 @@ class MockHttp { try { if (this._location != null) await router.push(this._location); if (this._mount != null) { - this._component = this._mount(); + this._component = await this._mount(); // Mounting may have triggered the initial navigation. if (router != null) await router.isReady(); } @@ -952,7 +947,11 @@ const loadBottomComponent = (location, mountOptions, respondForOptions) => { const throwIfEmit = `${bottomComponent.name} emitted an event, but it is not expected to do so. In this case, root cannot be specified as false.`; return mockHttp() - .mount(bottomComponent, fullMountOptions, throwIfEmit) + .mount( + bottomComponent.render != null ? bottomComponent : bottomComponent.name, + fullMountOptions, + throwIfEmit + ) .modify(series => (respondForOptions !== false ? series.respondForComponent(bottomComponent.name, respondForOptions) : series)); diff --git a/apps/central/test/util/load-async.js b/apps/central/test/util/load-async.js deleted file mode 100644 index 04b729482..000000000 --- a/apps/central/test/util/load-async.js +++ /dev/null @@ -1,28 +0,0 @@ -import createRoutes from '../../src/routes'; -import { loadAsync } from '../../src/util/load-async'; - -import createTestContainer from './container'; - -export const loadAsyncCache = new Map(); - -export const loadAsyncRouteComponents = () => { - const promises = []; - const stack = [...createRoutes(createTestContainer())]; - while (stack.length !== 0) { - const route = stack.pop(); - - const { asyncRoute } = route.meta; - if (asyncRoute != null) { - const { componentName } = asyncRoute; - promises.push(loadAsync(componentName)().then(m => { - loadAsyncCache.set(componentName, m); - })); - } - - if (route.children != null) { - for (const child of route.children) - stack.push(child); - } - } - return Promise.all(promises); -};