diff --git a/src/router/router.test.js b/src/router/router.test.js
index e37428f8..68944ff4 100644
--- a/src/router/router.test.js
+++ b/src/router/router.test.js
@@ -16,7 +16,7 @@
*/
import test from 'tape'
-import { matchHash, getHash, to, navigate, state } from './router.js'
+import { matchHash, getHash, to, navigate, state, back } from './router.js'
import { stage } from '../launch.js'
import Component from '../component.js'
import symbols from '../lib/symbols.js'
@@ -682,3 +682,294 @@ test('Route meta data is accessible in route object', async (assert) => {
)
assert.end()
})
+
+test('keepAlive: view is cached and can be restored', async (assert) => {
+ const originalElement = stage.element
+ let initCallCount = 0
+
+ stage.element = ({ parent }) => ({
+ populate() {},
+ set(prop, value) {
+ if (value && typeof value === 'object' && value.transition && value.transition.end) {
+ setTimeout(() => value.transition.end(), 0)
+ }
+ },
+ destroy() {},
+ parent,
+ })
+
+ const TestComponent1 = Component('TestComponent1', {
+ template: '',
+ code: {
+ render: () => ({
+ elms: [
+ {
+ [symbols.holder]: { destroy: () => {} },
+ node: {},
+ },
+ ],
+ cleanup: () => {},
+ }),
+ effects: [],
+ init() {
+ initCallCount++
+ },
+ },
+ })
+
+ const TestComponent2 = Component('TestComponent2', {
+ template: '',
+ code: {
+ render: () => ({
+ elms: [
+ {
+ [symbols.holder]: { destroy: () => {} },
+ node: {},
+ },
+ ],
+ cleanup: () => {},
+ }),
+ effects: [],
+ },
+ })
+
+ const host = {
+ parent: {
+ [symbols.routes]: [
+ {
+ path: '/page1',
+ component: TestComponent1,
+ options: { keepAlive: true, inHistory: true, passFocus: false },
+ },
+ {
+ path: '/page2',
+ component: TestComponent2,
+ options: { inHistory: true, passFocus: false },
+ },
+ ],
+ },
+ [symbols.children]: [{}],
+ [symbols.props]: {},
+ }
+
+ to('/page1')
+ await navigate.call(host)
+ const page1View = host[symbols.children][host[symbols.children].length - 1]
+ const initialInitCount = initCallCount
+
+ to('/page2')
+ await navigate.call(host)
+
+ back.call(host)
+ await navigate.call(host)
+
+ const restoredView = host[symbols.children][host[symbols.children].length - 1]
+ assert.equal(restoredView, page1View, 'Should restore the same cached view instance')
+ assert.equal(
+ initCallCount,
+ initialInitCount,
+ 'Component init should not be called again when restored from cache'
+ )
+
+ stage.element = originalElement
+ assert.end()
+})
+
+test('reuseComponent: same component instance is reused when navigating to same route', async (assert) => {
+ const originalElement = stage.element
+
+ stage.element = ({ parent }) => ({
+ populate() {},
+ set(prop, value) {
+ if (value && typeof value === 'object' && value.transition && value.transition.end) {
+ setTimeout(() => value.transition.end(), 0)
+ }
+ },
+ destroy() {},
+ parent,
+ })
+
+ const TestComponent = Component('TestComponent', {
+ template: '',
+ code: {
+ render: () => ({
+ elms: [
+ {
+ [symbols.holder]: { destroy: () => {} },
+ node: {},
+ },
+ ],
+ cleanup: () => {},
+ }),
+ effects: [],
+ },
+ })
+
+ const TestComponent2 = Component('TestComponent2', {
+ template: '',
+ code: {
+ render: () => ({
+ elms: [
+ {
+ [symbols.holder]: { destroy: () => {} },
+ node: {},
+ },
+ ],
+ cleanup: () => {},
+ }),
+ effects: [],
+ },
+ })
+
+ const host = {
+ parent: {
+ [symbols.routes]: [
+ {
+ path: '/page1',
+ component: TestComponent,
+ options: { reuseComponent: true, keepAlive: false, inHistory: true, passFocus: false },
+ },
+ {
+ path: '/page2',
+ component: TestComponent2,
+ options: { inHistory: true, passFocus: false },
+ },
+ ],
+ },
+ [symbols.children]: [{}],
+ [symbols.props]: {},
+ }
+
+ to('/page1')
+ await navigate.call(host)
+ const firstView = host[symbols.children][host[symbols.children].length - 1]
+
+ to('/page2')
+ await navigate.call(host)
+
+ to('/page1')
+ await navigate.call(host)
+ const secondView = host[symbols.children][host[symbols.children].length - 1]
+
+ assert.equal(
+ firstView,
+ secondView,
+ 'Should reuse the same component instance when reuseComponent is true'
+ )
+
+ stage.element = originalElement
+ assert.end()
+})
+
+test('Edge case: async hook failure with empty history should not cause endless loop', async (assert) => {
+ const originalElement = stage.element
+ stage.element = ({ parent }) => ({
+ populate() {},
+ set() {},
+ destroy() {},
+ parent,
+ })
+
+ const TestComponent = Component('TestComponent', {
+ template: '',
+ code: {
+ render: () => ({
+ elms: [
+ {
+ [symbols.holder]: { destroy: () => {} },
+ node: {},
+ },
+ ],
+ cleanup: () => {},
+ }),
+ effects: [],
+ },
+ })
+
+ const host = {
+ parent: {
+ [symbols.routes]: [
+ {
+ path: '/page1',
+ component: TestComponent,
+ hooks: {
+ async before() {
+ throw new Error('Hook failed')
+ },
+ },
+ },
+ ],
+ [symbols.routerHooks]: {
+ async beforeEach() {
+ throw new Error('BeforeEach hook failed')
+ },
+ },
+ },
+ [symbols.children]: [{}],
+ [symbols.props]: {},
+ }
+
+ to('/page1')
+
+ try {
+ await navigate.call(host)
+ } catch {
+ // Expected to fail
+ }
+
+ // Wait a bit to ensure no endless loop
+ await new Promise((resolve) => setTimeout(resolve, 50))
+
+ // Verify navigation state is properly reset
+ assert.equal(state.navigating, false, 'Navigation state should be reset after hook failure')
+ assert.ok(true, 'Should not cause endless loop when hook fails with empty history')
+
+ stage.element = originalElement
+ assert.end()
+})
+
+test('Router hooks: beforeEach and before returning false cancel navigation', async (assert) => {
+ const originalElement = stage.element
+ stage.element = ({ parent }) => ({ populate() {}, set() {}, parent })
+
+ const TestComponent = Component('TestComponent', {
+ template: '',
+ code: { render: () => ({ elms: [], cleanup: () => {} }), effects: [] },
+ })
+
+ const host = {
+ parent: {
+ [symbols.routes]: [
+ { path: '/page1', component: TestComponent },
+ {
+ path: '/page2',
+ component: TestComponent,
+ hooks: {
+ before() {
+ return false
+ },
+ },
+ },
+ ],
+ [symbols.routerHooks]: {
+ beforeEach(to) {
+ if (to.path === '/page2') {
+ return false
+ }
+ },
+ },
+ },
+ [symbols.children]: [{}],
+ [symbols.props]: {},
+ }
+
+ to('/page1')
+ await navigate.call(host)
+
+ to('/page2')
+ await navigate.call(host)
+ assert.equal(state.path, '/page1', 'Navigation should be cancelled when hooks return false')
+
+ stage.element = originalElement
+ assert.end()
+})