|
1 | | -import { describe, expect, it, vi } from 'vitest'; |
2 | | -import { installNavigatedPageHmrReload } from './navigate-app'; |
| 1 | +import { afterEach, describe, expect, it, vi } from 'vitest'; |
| 2 | +import { inheritAppContext, installNavigatedPageHmrReload, installVueNavigateUsingApp } from './navigate-app'; |
3 | 3 | import { readFileSync } from 'fs'; |
4 | 4 | import path from 'path'; |
5 | 5 | import { fileURLToPath } from 'url'; |
@@ -42,15 +42,119 @@ describe('__nsNavigateUsingApp prop forwarding', () => { |
42 | 42 | expect(callMatch).toBeTruthy(); |
43 | 43 | const argList = callMatch![1]; |
44 | 44 | // Two top-level arguments: component, props |
45 | | - expect(argList).toContain('normalizeComponent(comp,'); |
| 45 | + expect(argList).toContain('normalizeComponent(target,'); |
46 | 46 | expect(argList).toMatch(/,\s*opts\s*&&\s*\(opts\s*as\s*any\)\.props\s*$/); |
47 | 47 | }); |
48 | 48 |
|
49 | 49 | it('still calls normalizeComponent so non-defineComponent inputs resolve correctly', () => { |
50 | 50 | // Regression guard: a prior refactor passed `comp` directly to AppFactory |
51 | 51 | // which broke <script setup> destinations. The normalizeComponent wrap |
52 | 52 | // must stay in place. |
53 | | - expect(navigateSrc).toMatch(/AppFactory\(normalizeComponent\(comp,/); |
| 53 | + expect(navigateSrc).toMatch(/AppFactory\(normalizeComponent\(target,/); |
| 54 | + }); |
| 55 | +}); |
| 56 | + |
| 57 | +describe('inheritAppContext', () => { |
| 58 | + it('fills in components, directives, mixins and globalProperties the page app lacks, never overriding its own', () => { |
| 59 | + const Widget = { name: 'Widget' }; |
| 60 | + const Own = { name: 'Own' }; |
| 61 | + const mixin = { created() {} }; |
| 62 | + const focus = {}; |
| 63 | + const base = { components: { Widget, Own: { name: 'RootOwn' } }, directives: { focus }, mixins: [mixin], config: { globalProperties: { $http: 'http', $navigateTo: 'root-nav' } } }; |
| 64 | + const ctx: any = { components: { Own }, directives: {}, mixins: [mixin], config: { globalProperties: { $navigateTo: 'page-nav' } } }; |
| 65 | + inheritAppContext(ctx, base); |
| 66 | + expect(ctx.components.Widget).toBe(Widget); |
| 67 | + expect(ctx.components.Own).toBe(Own); |
| 68 | + expect(ctx.directives.focus).toBe(focus); |
| 69 | + expect(ctx.mixins).toEqual([mixin]); |
| 70 | + expect(ctx.config.globalProperties).toEqual({ $navigateTo: 'page-nav', $http: 'http' }); |
| 71 | + }); |
| 72 | + |
| 73 | + it('tolerates a missing side', () => { |
| 74 | + expect(() => inheritAppContext(null, { components: {} })).not.toThrow(); |
| 75 | + expect(() => inheritAppContext({}, null)).not.toThrow(); |
| 76 | + }); |
| 77 | +}); |
| 78 | + |
| 79 | +/** |
| 80 | + * Drives the installed navigator with a Vue-shaped factory: like Vue's |
| 81 | + * createApp, it clones a non-function root component, and the mounted root |
| 82 | + * instance's `type` is that clone — the object Vue's HMR `reload` mutates. |
| 83 | + */ |
| 84 | +describe('__nsNavigateUsingApp page apps', () => { |
| 85 | + const g: any = globalThis; |
| 86 | + const apps: any[] = []; |
| 87 | + |
| 88 | + function installFakeVue() { |
| 89 | + apps.length = 0; |
| 90 | + g.NSVRoot = class NSVRoot {}; |
| 91 | + g.createApp = vi.fn((rootComponent: any, rootProps?: any) => { |
| 92 | + const type = typeof rootComponent === 'function' ? rootComponent : { ...rootComponent }; |
| 93 | + const _context: any = { app: null, config: { globalProperties: { $navigateTo: 'page-nav' } }, mixins: [], components: {}, directives: {}, provides: {} }; |
| 94 | + const app: any = { |
| 95 | + _context, |
| 96 | + rootProps, |
| 97 | + mount: vi.fn(() => ({ $el: { nativeView: { constructor: { name: 'Page' } } }, $: { type } })), |
| 98 | + unmount: vi.fn(), |
| 99 | + }; |
| 100 | + _context.app = app; |
| 101 | + apps.push(app); |
| 102 | + return app; |
| 103 | + }); |
| 104 | + } |
| 105 | + |
| 106 | + function makeFrame() { |
| 107 | + const frame: any = { |
| 108 | + currentPage: null, |
| 109 | + replacePage: vi.fn(), |
| 110 | + once: vi.fn(), |
| 111 | + navigate: vi.fn((entry: any) => { |
| 112 | + const page = entry.create(); |
| 113 | + page.frame = frame; |
| 114 | + frame.currentPage = page; |
| 115 | + }), |
| 116 | + }; |
| 117 | + return frame; |
| 118 | + } |
| 119 | + |
| 120 | + afterEach(() => { |
| 121 | + delete g.createApp; |
| 122 | + delete g.NSVRoot; |
| 123 | + delete g.__NS_VUE_ROOT_APP__; |
| 124 | + }); |
| 125 | + |
| 126 | + it('inherits the root app recorded by the bridge when no app has navigated yet', () => { |
| 127 | + installFakeVue(); |
| 128 | + const Widget = { name: 'Widget' }; |
| 129 | + g.__NS_VUE_ROOT_APP__ = { _context: { components: { Widget }, directives: {}, mixins: [], provides: {}, config: { globalProperties: { $http: 'http' } } } }; |
| 130 | + installVueNavigateUsingApp(); |
| 131 | + g.__nsNavigateUsingApp({ name: 'Home', render: () => 'v1' }, { frame: makeFrame() }); |
| 132 | + expect(apps).toHaveLength(1); |
| 133 | + expect(apps[0]._context.components.Widget).toBe(Widget); |
| 134 | + expect(apps[0]._context.config.globalProperties.$http).toBe('http'); |
| 135 | + expect(apps[0]._context.config.globalProperties.$navigateTo).toBe('page-nav'); |
| 136 | + }); |
| 137 | + |
| 138 | + it('rebuilds a hot-reloaded page from the mounted type Vue mutated, not from the original component', () => { |
| 139 | + installFakeVue(); |
| 140 | + installVueNavigateUsingApp(); |
| 141 | + const frame = makeFrame(); |
| 142 | + const Home = { name: 'Home', render: () => 'v1' }; |
| 143 | + g.__nsNavigateUsingApp(Home, { frame }); |
| 144 | + const pageApp = apps[0]; |
| 145 | + const mountedType = pageApp.mount.mock.results[0].value.$.type; |
| 146 | + expect(mountedType).not.toBe(Home); |
| 147 | + |
| 148 | + // Vue's HMR reload mutates instance.type in place; the original is untouched. |
| 149 | + mountedType.render = () => 'v2'; |
| 150 | + pageApp._context.reload(); |
| 151 | + |
| 152 | + expect(frame.replacePage).toHaveBeenCalledTimes(1); |
| 153 | + frame.replacePage.mock.calls[0][0].create(); |
| 154 | + expect(apps).toHaveLength(2); |
| 155 | + const rebuiltFrom = g.createApp.mock.calls[1][0]; |
| 156 | + expect(rebuiltFrom.render()).toBe('v2'); |
| 157 | + expect(Home.render()).toBe('v1'); |
54 | 158 | }); |
55 | 159 | }); |
56 | 160 |
|
|
0 commit comments