slicejs-web-framework 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Slice/Components/Structural/Router/Router.js +17 -0
- package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +15 -2
- package/Slice/Slice.js +115 -103
- package/Slice/tests/bundle-v2-runtime-contract.test.js +10 -1
- package/package.json +1 -1
- package/tests/router-build-failure.test.js +125 -0
|
@@ -466,6 +466,11 @@ export default class Router {
|
|
|
466
466
|
const componentName = route.parentRoute ? route.parentRoute.component : route.component;
|
|
467
467
|
const sliceId = `route-${componentName}`;
|
|
468
468
|
|
|
469
|
+
if (!targetElement) {
|
|
470
|
+
slice.logger.logError('Router', `Cannot render route "${route?.path ?? componentName}": no #app element in the document`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
469
474
|
const existingComponent = slice.controller.getComponent(sliceId);
|
|
470
475
|
|
|
471
476
|
if (slice.loading) {
|
|
@@ -487,6 +492,18 @@ export default class Router {
|
|
|
487
492
|
sliceId: sliceId,
|
|
488
493
|
});
|
|
489
494
|
|
|
495
|
+
// build() returns null whenever the component could not be produced
|
|
496
|
+
// (not registered, a resource fetch failed or was aborted, init()
|
|
497
|
+
// threw). Appending null raises "parameter 1 is not of type 'Node'",
|
|
498
|
+
// which buries the real cause. Report it and keep the current view.
|
|
499
|
+
if (!component) {
|
|
500
|
+
slice.logger.logError(
|
|
501
|
+
'Router',
|
|
502
|
+
`Cannot render route "${route?.path ?? componentName}": slice.build('${componentName}') returned null`
|
|
503
|
+
);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
490
507
|
targetElement.innerHTML = '';
|
|
491
508
|
targetElement.appendChild(component);
|
|
492
509
|
|
|
@@ -50,8 +50,21 @@ export default class ThemeManager {
|
|
|
50
50
|
* @returns {Promise<void>}
|
|
51
51
|
*/
|
|
52
52
|
async loadThemeCSS(themeName) {
|
|
53
|
-
let themeContent =
|
|
54
|
-
|
|
53
|
+
let themeContent = localStorage.getItem(`sliceTheme-${themeName}`);
|
|
54
|
+
|
|
55
|
+
if (!themeContent) {
|
|
56
|
+
try {
|
|
57
|
+
themeContent = await slice.controller.fetchText(themeName, 'theme');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// A theme that fails to fetch must not abort framework init.
|
|
60
|
+
// applyTheme() is awaited from StylesManager.init(), which runs
|
|
61
|
+
// inside Slice.js init() — letting this throw leaves the runtime
|
|
62
|
+
// half-built (no slice.router). An unstyled app is recoverable;
|
|
63
|
+
// no app at all is not. fetchText already logged the cause.
|
|
64
|
+
slice.logger.logError('ThemeManager', `Failed to load theme: ${themeName}`, error);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
55
68
|
|
|
56
69
|
if (!themeContent) {
|
|
57
70
|
slice.logger.logError('ThemeManager', `Failed to load theme: ${themeName}`);
|
package/Slice/Slice.js
CHANGED
|
@@ -311,40 +311,27 @@ export default class Slice {
|
|
|
311
311
|
if (componentIds.id && isVisual) componentInstance.id = componentIds.id;
|
|
312
312
|
if (componentIds.sliceId) componentInstance.sliceId = componentIds.sliceId;
|
|
313
313
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// Dev guard: update() called during init() runs BEFORE the component is
|
|
320
|
-
// registered, so the framework's update() wrapper (serialization +
|
|
321
|
-
// liveness) isn't installed yet and the call misbehaves silently. Warn
|
|
322
|
-
// instead. Restored right after init() so registerComponent →
|
|
323
|
-
// installUpdate wraps the real method, not this guard.
|
|
324
|
-
let _rawUpdate = null;
|
|
325
|
-
if (typeof componentInstance.update === 'function') {
|
|
326
|
-
_rawUpdate = componentInstance.update;
|
|
327
|
-
componentInstance.update = function (...args) {
|
|
328
|
-
slice.logger.logWarning(
|
|
329
|
-
componentInstance.constructor?.name || 'Component',
|
|
330
|
-
'update() was called during init() (before registration). Run your render logic directly from init(); the update() wrapper (serialization + liveness) is not active until the component is registered.'
|
|
331
|
-
);
|
|
332
|
-
return _rawUpdate.apply(this, args);
|
|
333
|
-
};
|
|
334
|
-
}
|
|
314
|
+
if (!this.controller.verifyComponentIds(componentInstance)) {
|
|
315
|
+
this.logger.logError('Slice', `Error registering instance ${componentName} ${componentInstance.sliceId}`);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
335
318
|
|
|
336
|
-
|
|
319
|
+
// Register before init so the component is visible in activeComponents
|
|
320
|
+
// during init(). This lets slice.router.navigate() work from inside
|
|
321
|
+
// init() without the Router creating a duplicate instance (the Router
|
|
322
|
+
// finds the existing component via getComponent). Calling update()
|
|
323
|
+
// from init() is safe because installUpdate already wrapped it.
|
|
324
|
+
this.controller.registerComponent(componentInstance);
|
|
337
325
|
|
|
338
|
-
|
|
326
|
+
if (componentInstance.init) await componentInstance.init();
|
|
339
327
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
328
|
+
if (slice.debuggerConfig.enabled && isVisual) {
|
|
329
|
+
this.debugger.attachDebugMode(componentInstance);
|
|
330
|
+
}
|
|
343
331
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
332
|
+
if (isVisual) {
|
|
333
|
+
this.controller.registerComponentsRecursively(componentInstance);
|
|
334
|
+
}
|
|
348
335
|
|
|
349
336
|
this.logger.logInfo('Slice', `Instance ${componentInstance.sliceId} created`);
|
|
350
337
|
return componentInstance;
|
|
@@ -457,77 +444,19 @@ async function init() {
|
|
|
457
444
|
}
|
|
458
445
|
}
|
|
459
446
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const createBundlingInitError = (step, error) => {
|
|
466
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
467
|
-
return new Error(`Bundling V2 initialization failed (${step}): ${detail}`, { cause: error });
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
// Initialize bundles before building components.
|
|
471
|
-
// Only in production — dev mode loads each component individually from source.
|
|
472
|
-
// bundleConfigJson was already fetched above (step 2); reuse it.
|
|
473
|
-
if (resolvedMode === 'production' && bundleConfigJson) {
|
|
474
|
-
window.slice.controller.bundleConfig = bundleConfigJson;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
|
|
478
|
-
const config = window.slice.controller.bundleConfig;
|
|
479
|
-
if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
|
|
480
|
-
window.__SLICE_SHARED_DEPS__ = {};
|
|
481
|
-
}
|
|
482
|
-
const criticalFile = config?.bundles?.critical?.file;
|
|
483
|
-
if (criticalFile) {
|
|
484
|
-
try {
|
|
485
|
-
await window.slice.controller.loadBundle('critical');
|
|
486
|
-
} catch (error) {
|
|
487
|
-
throw createBundlingInitError(`critical bundle "${criticalFile}"`, error);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const routeBundles = config?.routeBundles || {};
|
|
492
|
-
const initialPath = window.location.pathname || '/';
|
|
493
|
-
const bundlesForRoute = routeBundles[initialPath] || [];
|
|
494
|
-
|
|
495
|
-
const loadRouteBundles = async () => {
|
|
496
|
-
for (const bundleName of bundlesForRoute) {
|
|
497
|
-
if (bundleName === 'critical') continue;
|
|
498
|
-
const bundleInfo = config?.bundles?.routes?.[bundleName];
|
|
499
|
-
if (!bundleInfo?.file) continue;
|
|
500
|
-
await window.slice.controller.loadBundle(bundleName);
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const preloadRouteBundles = () => {
|
|
505
|
-
loadRouteBundles().catch((error) => {
|
|
506
|
-
window.slice?.logger?.error('Slice', `Idle route preload failed for "${initialPath}"`, error);
|
|
507
|
-
});
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
const safePreload = () => {
|
|
511
|
-
try {
|
|
512
|
-
preloadRouteBundles();
|
|
513
|
-
} catch (error) {
|
|
514
|
-
window.slice?.logger?.error('Slice', 'Error in route preload callback', error);
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
if (typeof requestIdleCallback === 'function') {
|
|
519
|
-
requestIdleCallback(() => safePreload());
|
|
520
|
-
} else {
|
|
521
|
-
setTimeout(() => safePreload(), 0);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
447
|
+
// 5. Create Slice instance and set resolved mode
|
|
448
|
+
window.slice = new Slice(sliceConfig, frameworkClasses);
|
|
449
|
+
window.slice._mode = resolvedMode;
|
|
450
|
+
window.slice.setPublicEnv(envResult?.env || {});
|
|
524
451
|
|
|
525
452
|
slice.paths.structuralComponentFolderPath = '/Slice/Components/Structural';
|
|
526
453
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
454
|
+
// Logger must be available before bundle loading — Controller internally calls
|
|
455
|
+
// slice.logger.* methods during loadBundle/registerBundle.
|
|
456
|
+
if (sliceConfig.logger.enabled) {
|
|
457
|
+
const LoggerModule = window.slice.frameworkClasses?.Logger
|
|
458
|
+
|| await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Logger/Logger.js`);
|
|
459
|
+
window.slice.logger = new LoggerModule();
|
|
531
460
|
} else {
|
|
532
461
|
const noop = () => {};
|
|
533
462
|
window.slice.logger = {
|
|
@@ -536,6 +465,66 @@ async function init() {
|
|
|
536
465
|
};
|
|
537
466
|
}
|
|
538
467
|
|
|
468
|
+
const createBundlingInitError = (step, error) => {
|
|
469
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
470
|
+
return new Error(`Bundling V2 initialization failed (${step}): ${detail}`, { cause: error });
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// Initialize bundles before building components.
|
|
474
|
+
// Only in production — dev mode loads each component individually from source.
|
|
475
|
+
// bundleConfigJson was already fetched above (step 2); reuse it.
|
|
476
|
+
if (resolvedMode === 'production' && bundleConfigJson) {
|
|
477
|
+
window.slice.controller.bundleConfig = bundleConfigJson;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
|
|
481
|
+
const config = window.slice.controller.bundleConfig;
|
|
482
|
+
if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
|
|
483
|
+
window.__SLICE_SHARED_DEPS__ = {};
|
|
484
|
+
}
|
|
485
|
+
const criticalFile = config?.bundles?.critical?.file;
|
|
486
|
+
if (criticalFile) {
|
|
487
|
+
try {
|
|
488
|
+
await window.slice.controller.loadBundle('critical');
|
|
489
|
+
} catch (error) {
|
|
490
|
+
throw createBundlingInitError(`critical bundle "${criticalFile}"`, error);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const routeBundles = config?.routeBundles || {};
|
|
495
|
+
const initialPath = window.location.pathname || '/';
|
|
496
|
+
const bundlesForRoute = routeBundles[initialPath] || [];
|
|
497
|
+
|
|
498
|
+
const loadRouteBundles = async () => {
|
|
499
|
+
for (const bundleName of bundlesForRoute) {
|
|
500
|
+
if (bundleName === 'critical') continue;
|
|
501
|
+
const bundleInfo = config?.bundles?.routes?.[bundleName];
|
|
502
|
+
if (!bundleInfo?.file) continue;
|
|
503
|
+
await window.slice.controller.loadBundle(bundleName);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const preloadRouteBundles = () => {
|
|
508
|
+
loadRouteBundles().catch((error) => {
|
|
509
|
+
window.slice?.logger?.error('Slice', `Idle route preload failed for "${initialPath}"`, error);
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const safePreload = () => {
|
|
514
|
+
try {
|
|
515
|
+
preloadRouteBundles();
|
|
516
|
+
} catch (error) {
|
|
517
|
+
window.slice?.logger?.error('Slice', 'Error in route preload callback', error);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (typeof requestIdleCallback === 'function') {
|
|
522
|
+
requestIdleCallback(() => safePreload());
|
|
523
|
+
} else {
|
|
524
|
+
setTimeout(() => safePreload(), 0);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
539
528
|
if (sliceConfig.debugger.enabled) {
|
|
540
529
|
const DebuggerModule = window.slice.frameworkClasses?.Debugger
|
|
541
530
|
|| await window.slice.getClass(`${slice.paths.structuralComponentFolderPath}/Debugger/Debugger.js`);
|
|
@@ -691,8 +680,31 @@ async function init() {
|
|
|
691
680
|
await window.slice.router.init();
|
|
692
681
|
}
|
|
693
682
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
683
|
+
// Booting requires a DOM. Under node — where this module is imported for the
|
|
684
|
+
// Slice class alone (see Slice/tests) — there is nothing to initialize, so skip
|
|
685
|
+
// it rather than starting an init() that can only fail.
|
|
686
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
687
|
+
// Exposed so callers that cannot rely on module import order — test
|
|
688
|
+
// harnesses, embedders, anything loaded via a plain <script> — have a real
|
|
689
|
+
// readiness signal. It settles exactly when init() does. Note that
|
|
690
|
+
// `window.slice` and `slice.build` exist well before init() finishes, so
|
|
691
|
+
// checking for those is NOT a readiness test; await this (or check
|
|
692
|
+
// `slice.router`) instead.
|
|
693
|
+
window.__SLICE_READY__ = init();
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
await window.__SLICE_READY__;
|
|
697
|
+
if (window.slice) window.slice.initError = null;
|
|
698
|
+
} catch (initError) {
|
|
699
|
+
// Deliberately not swallowed. A half-initialized runtime (no
|
|
700
|
+
// slice.router, no registered components) makes every downstream call
|
|
701
|
+
// fail with its own unrelated-looking TypeError — "Cannot read
|
|
702
|
+
// properties of undefined (reading 'afterEach')", "appendChild:
|
|
703
|
+
// parameter 1 is not of type 'Node'" — each of which hides this, the
|
|
704
|
+
// actual cause. Record it, then rethrow so that importing this module
|
|
705
|
+
// fails loudly and on the real error.
|
|
706
|
+
if (window.slice) window.slice.initError = initError;
|
|
707
|
+
console.error('[Slice.js] Initialization failed:', initError);
|
|
708
|
+
throw initError;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
@@ -707,7 +707,16 @@ test('Slice init fails fast with contextual error on invalid Bundling V2 contrac
|
|
|
707
707
|
|
|
708
708
|
const sliceModuleUrl = new URL(`../Slice.js?fail-fast-test=${Date.now()}`, import.meta.url).href;
|
|
709
709
|
|
|
710
|
-
|
|
710
|
+
// Failing fast means the failure propagates: a rejected import stops
|
|
711
|
+
// consumers from running against a runtime that has no router and no
|
|
712
|
+
// registered components. Previously init() logged and resolved anyway,
|
|
713
|
+
// so the first symptom a developer saw was an unrelated TypeError
|
|
714
|
+
// further downstream rather than this contextual error.
|
|
715
|
+
await assert.rejects(
|
|
716
|
+
() => import(sliceModuleUrl),
|
|
717
|
+
/Bundling V2 initialization failed/,
|
|
718
|
+
'a failed init must reject the module import, not resolve a half-built runtime'
|
|
719
|
+
);
|
|
711
720
|
|
|
712
721
|
assert.ok(
|
|
713
722
|
loggedMessages.some((message) => /Bundling V2 initialization failed/i.test(message)),
|
package/package.json
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Regression test: handleRoute() must not append a failed build to the DOM.
|
|
2
|
+
//
|
|
3
|
+
// slice.build() returns null on any unrecoverable path (component not
|
|
4
|
+
// registered, a resource fetch failed or was aborted mid-flight, init() threw).
|
|
5
|
+
// Passing that null straight to appendChild() raised
|
|
6
|
+
// TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'
|
|
7
|
+
// which surfaced far from the real cause and made the failure look like a DOM
|
|
8
|
+
// bug. handleRoute() now reports the build failure and leaves the current view
|
|
9
|
+
// in place.
|
|
10
|
+
//
|
|
11
|
+
// Router has no imports and no module-level DOM access, so its prototype can be
|
|
12
|
+
// exercised directly against stubs (same approach as router-emit-ordering).
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/router-build-failure.test.js
|
|
15
|
+
|
|
16
|
+
import { test, describe, before, beforeEach } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
|
|
19
|
+
describe('Router.handleRoute — failed component build', () => {
|
|
20
|
+
let Router;
|
|
21
|
+
let appended;
|
|
22
|
+
let loggedErrors;
|
|
23
|
+
let appElement;
|
|
24
|
+
|
|
25
|
+
before(async () => {
|
|
26
|
+
globalThis.window = globalThis.window || { location: { pathname: '/' } };
|
|
27
|
+
globalThis.slice = globalThis.slice || {};
|
|
28
|
+
({ default: Router } = await import('../Slice/Components/Structural/Router/Router.js'));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
appended = [];
|
|
33
|
+
loggedErrors = [];
|
|
34
|
+
appElement = {
|
|
35
|
+
innerHTML: 'previous view',
|
|
36
|
+
appendChild(node) {
|
|
37
|
+
// Mirror the real DOM contract: appending a non-Node is a TypeError.
|
|
38
|
+
if (!node || typeof node !== 'object') {
|
|
39
|
+
throw new TypeError(
|
|
40
|
+
"Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
appended.push(node);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
globalThis.document = { querySelector: (sel) => (sel === '#app' ? appElement : null) };
|
|
48
|
+
globalThis.slice = {
|
|
49
|
+
logger: { logError: (...args) => loggedErrors.push(args) },
|
|
50
|
+
controller: { getComponent: () => null },
|
|
51
|
+
router: {},
|
|
52
|
+
loading: null,
|
|
53
|
+
build: async () => null, // the failure under test
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const makeRouter = () => {
|
|
58
|
+
const r = Object.create(Router.prototype);
|
|
59
|
+
r.renderRoutesInComponent = async () => {};
|
|
60
|
+
r.invalidateCache = () => {};
|
|
61
|
+
return r;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
test('does not throw when slice.build() returns null', async () => {
|
|
65
|
+
const r = makeRouter();
|
|
66
|
+
await r.handleRoute({ path: '/broken', component: 'Broken' }, {});
|
|
67
|
+
assert.equal(appended.length, 0, 'nothing should be appended');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('logs the build failure naming the route and component', async () => {
|
|
71
|
+
const r = makeRouter();
|
|
72
|
+
await r.handleRoute({ path: '/broken', component: 'Broken' }, {});
|
|
73
|
+
|
|
74
|
+
assert.equal(loggedErrors.length, 1, 'exactly one error should be reported');
|
|
75
|
+
const [scope, message] = loggedErrors[0];
|
|
76
|
+
assert.equal(scope, 'Router');
|
|
77
|
+
assert.match(message, /\/broken/);
|
|
78
|
+
assert.match(message, /Broken/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('leaves the current view untouched instead of blanking it', async () => {
|
|
82
|
+
const r = makeRouter();
|
|
83
|
+
await r.handleRoute({ path: '/broken', component: 'Broken' }, {});
|
|
84
|
+
assert.equal(appElement.innerHTML, 'previous view');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('does not mark the failed route as active', async () => {
|
|
88
|
+
const r = makeRouter();
|
|
89
|
+
await r.handleRoute({ path: '/broken', component: 'Broken' }, {});
|
|
90
|
+
assert.equal(slice.router.activeRoute, undefined);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('still stops the loading indicator', async () => {
|
|
94
|
+
const stops = [];
|
|
95
|
+
slice.loading = { start() {}, stop() { stops.push(1); } };
|
|
96
|
+
|
|
97
|
+
const r = makeRouter();
|
|
98
|
+
await r.handleRoute({ path: '/broken', component: 'Broken' }, {});
|
|
99
|
+
assert.equal(stops.length, 1, 'loading.stop() must run even on the failure path');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a successful build still renders and activates the route', async () => {
|
|
103
|
+
const component = { tagName: 'DIV' };
|
|
104
|
+
slice.build = async () => component;
|
|
105
|
+
|
|
106
|
+
const r = makeRouter();
|
|
107
|
+
const route = { path: '/ok', component: 'Ok' };
|
|
108
|
+
await r.handleRoute(route, {});
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(appended, [component]);
|
|
111
|
+
assert.equal(appElement.innerHTML, '');
|
|
112
|
+
assert.equal(slice.router.activeRoute, route);
|
|
113
|
+
assert.equal(loggedErrors.length, 0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('reports instead of throwing when #app is missing', async () => {
|
|
117
|
+
globalThis.document = { querySelector: () => null };
|
|
118
|
+
|
|
119
|
+
const r = makeRouter();
|
|
120
|
+
await r.handleRoute({ path: '/ok', component: 'Ok' }, {});
|
|
121
|
+
|
|
122
|
+
assert.equal(loggedErrors.length, 1);
|
|
123
|
+
assert.match(loggedErrors[0][1], /#app/);
|
|
124
|
+
});
|
|
125
|
+
});
|