From 12ef72d595038d0ea46a6a0226bbd912f8fae404 Mon Sep 17 00:00:00 2001 From: cameronapak Date: Tue, 22 Jul 2025 08:52:32 -0500 Subject: [PATCH] test: add comprehensive tests for AppReduced utility and path normalization --- .../ui/client/utils/AppReduced.spec.ts | 231 ++++++++++++++++++ app/src/ui/client/BkndProvider.tsx | 4 +- app/src/ui/client/utils/AppReduced.ts | 3 +- 3 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 app/__test__/ui/client/utils/AppReduced.spec.ts diff --git a/app/__test__/ui/client/utils/AppReduced.spec.ts b/app/__test__/ui/client/utils/AppReduced.spec.ts new file mode 100644 index 0000000..355170e --- /dev/null +++ b/app/__test__/ui/client/utils/AppReduced.spec.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { AppReduced, type AppType } from 'ui/client/utils/AppReduced'; +import type { BkndAdminOptions } from 'ui/client/BkndProvider'; + +// Import the normalizeAdminPath function for testing +// Note: This assumes the function is exported or we need to test it indirectly through public methods + +describe('AppReduced', () => { + let mockAppJson: AppType; + let appReduced: AppReduced; + + beforeEach(() => { + mockAppJson = { + data: { + entities: {}, + relations: {} + }, + flows: { + flows: {} + }, + auth: {} + } as AppType; + }); + + describe('getSettingsPath', () => { + it('should return settings path with admin_basepath', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(); + + expect(result).toBe('~/admin/settings'); + }); + + it('should return settings path with empty admin_basepath', () => { + const options: BkndAdminOptions = { + admin_basepath: '', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(); + + expect(result).toBe('~/settings'); + }); + + it('should append additional path segments', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(['user', 'profile']); + + expect(result).toBe('~/admin/settings/user/profile'); + }); + + it('should normalize multiple slashes', () => { + const options: BkndAdminOptions = { + admin_basepath: '//admin//', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(['//user//']); + + expect(result).toBe('~/admin/settings/user'); + }); + + it('should handle admin_basepath without leading slash', () => { + const options: BkndAdminOptions = { + admin_basepath: 'admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(); + + expect(result).toBe('~/admin/settings'); + }); + }); + + describe('getAbsolutePath', () => { + it('should return absolute path with admin_basepath', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getAbsolutePath('dashboard'); + + expect(result).toBe('~/admin/dashboard'); + }); + + it('should return base path when no path provided', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getAbsolutePath(); + + expect(result).toBe('~/admin'); + }); + + it('should normalize paths correctly', () => { + const options: BkndAdminOptions = { + admin_basepath: '//admin//', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getAbsolutePath('//dashboard//'); + + expect(result).toBe('~/admin/dashboard'); + }); + }); + + describe('options getter', () => { + it('should return merged options with defaults', () => { + const customOptions: BkndAdminOptions = { + admin_basepath: '/custom-admin', + logo_return_path: '/custom-home' + }; + + appReduced = new AppReduced(mockAppJson, customOptions); + const options = appReduced.options; + + expect(options).toEqual({ + logo_return_path: '/custom-home', + admin_basepath: '/custom-admin' + }); + }); + + it('should use default logo_return_path when not provided', () => { + const customOptions: BkndAdminOptions = { + admin_basepath: '/admin' + }; + + appReduced = new AppReduced(mockAppJson, customOptions); + const options = appReduced.options; + + expect(options.logo_return_path).toBe('/'); + expect(options.admin_basepath).toBe('/admin'); + }); + }); + + describe('path normalization behavior', () => { + it('should normalize duplicate slashes in settings path', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(['//nested//path//']); + + expect(result).toBe('~/admin/settings/nested/path'); + }); + + it('should handle root path normalization', () => { + const options: BkndAdminOptions = { + admin_basepath: '/', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getAbsolutePath(); + + // The normalizeAdminPath function removes trailing slashes except for root "/" + // When admin_basepath is "/", the result is "~/" which becomes "~" after normalization + expect(result).toBe('~'); + }); + + it('should preserve entity paths ending with slash', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getAbsolutePath('entity/'); + + expect(result).toBe('~/admin/entity/'); + }); + + it('should remove trailing slashes from non-entity paths', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getAbsolutePath('dashboard/'); + + expect(result).toBe('~/admin/dashboard'); + }); + }); + + describe('edge cases', () => { + it('should handle undefined admin_basepath', () => { + const options: BkndAdminOptions = { + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(); + + // When admin_basepath is undefined, it defaults to empty string + expect(result).toBe('~/settings'); + }); + + it('should handle null path segments', () => { + const options: BkndAdminOptions = { + admin_basepath: '/admin', + logo_return_path: '/' + }; + + appReduced = new AppReduced(mockAppJson, options); + const result = appReduced.getSettingsPath(['', 'valid', '']); + + expect(result).toBe('~/admin/settings/valid'); + }); + }); +}); \ No newline at end of file diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx index 964d82a..a6e09ff 100644 --- a/app/src/ui/client/BkndProvider.tsx +++ b/app/src/ui/client/BkndProvider.tsx @@ -8,7 +8,9 @@ import { Message } from "ui/components/display/Message"; import { useNavigate } from "ui/lib/routes"; import type { AdminBkndWindowContext } from "modules/server/AdminController"; -export type BkndAdminOptions = Omit +export type BkndAdminOptions = Omit & { + admin_basepath?: string; +} type BkndContext = { version: number; schema: ModuleSchemas; diff --git a/app/src/ui/client/utils/AppReduced.ts b/app/src/ui/client/utils/AppReduced.ts index 66561c9..e69f542 100644 --- a/app/src/ui/client/utils/AppReduced.ts +++ b/app/src/ui/client/utils/AppReduced.ts @@ -88,7 +88,8 @@ export class AppReduced { get options() { return { - logo_return_path: "/", + admin_basepath: '', + logo_return_path: '/', ...this._options, }; }