diff --git a/packages/dockview-angular/jest.config.ts b/packages/dockview-angular/jest.config.ts index e620ca257..32e34f6fa 100644 --- a/packages/dockview-angular/jest.config.ts +++ b/packages/dockview-angular/jest.config.ts @@ -1,7 +1,7 @@ import { JestConfigWithTsJest } from 'ts-jest'; const config: JestConfigWithTsJest = { - preset: 'ts-jest', + preset: 'jest-preset-angular', roots: ['/packages/dockview-angular'], modulePaths: ['/packages/dockview-angular/src'], displayName: { name: 'dockview-angular', color: 'blue' }, @@ -12,11 +12,9 @@ const config: JestConfigWithTsJest = { '!/packages/dockview-angular/src/**/index.ts', '!/packages/dockview-angular/src/public-api.ts', ], - setupFiles: [ - '/packages/dockview-angular/src/__tests__/__mocks__/resizeObserver.js', - '/packages/dockview-angular/src/__tests__/__mocks__/angular-testing.js', + setupFilesAfterEnv: [ + '/packages/dockview-angular/src/__tests__/setup-jest.ts' ], - setupFilesAfterEnv: ['/jest-setup.ts'], coveragePathIgnorePatterns: ['/node_modules/'], modulePathIgnorePatterns: [ '/packages/dockview-angular/src/__tests__/__mocks__', @@ -25,20 +23,19 @@ const config: JestConfigWithTsJest = { coverageDirectory: '/packages/dockview-angular/coverage/', testResultsProcessor: 'jest-sonar-reporter', testEnvironment: 'jsdom', - transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - tsconfig: '/tsconfig.test.json', - }, - ], - }, - moduleNameMapper: { - '^@angular/(.*)$': '/../../node_modules/@angular/$1', - }, + testMatch: [ + '/packages/dockview-angular/src/**/*.spec.ts', + '/packages/dockview-angular/src/**/*.test.ts' + ], transformIgnorePatterns: [ 'node_modules/(?!(.*\\.mjs$|@angular|rxjs))' ], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + }, }; export default config; diff --git a/packages/dockview-angular/package.json b/packages/dockview-angular/package.json index 9314115d6..29ec6272e 100644 --- a/packages/dockview-angular/package.json +++ b/packages/dockview-angular/package.json @@ -56,11 +56,13 @@ "dockview-core": "^4.7.0" }, "peerDependencies": { - "@angular/core": ">=14.0.0", "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0", "rxjs": ">=7.0.0" }, "devDependencies": { - "ng-packagr": "^17.0.0" + "jest-preset-angular": "^14.6.1", + "ng-packagr": "^17.0.0", + "zone.js": "^0.15.1" } } diff --git a/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts b/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts index 6af675a1b..a8ba2f213 100644 --- a/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts +++ b/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts @@ -1,7 +1,33 @@ -// NOTE: These tests require Angular testing dependencies to be installed in the root node_modules -// For now they are commented out to demonstrate the build works +import { TestBed } from '@angular/core/testing'; +import { Component, Injector, EnvironmentInjector } from '@angular/core'; +import { AngularRenderer } from '../lib/utils/angular-renderer'; + +@Component({ + selector: 'test-component', + template: '
{{ title || "Test" }} - {{ value || "default" }}
', +}) +class TestComponent { + title?: string; + value?: string; +} describe('AngularRenderer', () => { + let injector: Injector; + let environmentInjector: EnvironmentInjector; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TestComponent] + }).compileComponents(); + + injector = TestBed.inject(Injector); + environmentInjector = TestBed.inject(EnvironmentInjector); + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); + it('should be testable when Angular dependencies are available', () => { expect(true).toBe(true); }); @@ -17,8 +43,7 @@ describe('AngularRenderer', () => { renderer.init(parameters); expect(renderer.element).toBeTruthy(); - expect(renderer.element.tagName).toBe('DIV'); - expect(renderer.element.classList.contains('test-component')).toBe(true); + expect(renderer.element.tagName).toBe('TEST-COMPONENT'); }); it('should update component properties', () => { @@ -28,13 +53,9 @@ describe('AngularRenderer', () => { environmentInjector }); - // Initialize with initial parameters renderer.init({ title: 'Initial Title' }); - - // Update properties renderer.update({ title: 'Updated Title', value: 'new-value' }); - // The component should have updated properties expect(renderer.element).toBeTruthy(); }); @@ -52,12 +73,10 @@ describe('AngularRenderer', () => { renderer.dispose(); - // After dispose, accessing element should throw expect(() => renderer.element).toThrow('Angular renderer not initialized'); }); it('should handle component creation errors gracefully', () => { - // Use an invalid component to trigger error const renderer = new AngularRenderer({ component: null as any, injector, @@ -79,7 +98,6 @@ describe('AngularRenderer', () => { renderer.init({ title: 'Test Title' }); renderer.dispose(); - // Should not throw expect(() => { renderer.update({ title: 'Updated Title' }); }).not.toThrow(); @@ -94,7 +112,6 @@ describe('AngularRenderer', () => { renderer.init({ title: 'Test Title' }); - // Multiple dispose calls should not throw expect(() => { renderer.dispose(); renderer.dispose(); diff --git a/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts b/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts index 9afd7e108..d0ab1e934 100644 --- a/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts +++ b/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts @@ -4,11 +4,9 @@ import { By } from '@angular/platform-browser'; import { DockviewAngularComponent } from '../lib/dockview/dockview-angular.component'; import { DockviewApi } from 'dockview-core'; -import { setupTestBed, getTestComponents, TestPanelComponent } from './__test_utils__/test-helpers'; +import { setupTestBed, getTestComponents } from './__test_utils__/test-helpers'; -// NOTE: These tests require Angular testing dependencies to be installed in the root node_modules -// For now they are commented out to demonstrate the build works -describe.skip('DockviewAngularComponent', () => { +describe('DockviewAngularComponent', () => { let component: DockviewAngularComponent; let fixture: ComponentFixture; let debugElement: DebugElement; @@ -21,7 +19,6 @@ describe.skip('DockviewAngularComponent', () => { component = fixture.componentInstance; debugElement = fixture.debugElement; - // Set required inputs component.components = getTestComponents(); }); @@ -84,7 +81,6 @@ describe.skip('DockviewAngularComponent', () => { const api = component.getDockviewApi(); const updateOptionsSpy = jest.spyOn(api!, 'updateOptions'); - // Simulate input change component.className = 'test-class'; component.ngOnChanges({ className: { @@ -105,12 +101,10 @@ describe.skip('DockviewAngularComponent', () => { component.ngOnInit(); - // API should be initialized without throwing expect(component.getDockviewApi()).toBeDefined(); }); }); -// Integration test with template @Component({ template: ` { +describe('DockviewAngularComponent Integration', () => { let hostComponent: TestHostComponent; let fixture: ComponentFixture; @@ -168,7 +162,5 @@ describe.skip('DockviewAngularComponent Integration', () => { fixture.detectChanges(); expect(hostComponent.api).toBeDefined(); - // Additional assertions could be added here to verify the properties - // were passed to the core dockview component }); }); \ No newline at end of file diff --git a/packages/dockview-angular/src/__tests__/setup-jest.ts b/packages/dockview-angular/src/__tests__/setup-jest.ts new file mode 100644 index 000000000..8d5ffb196 --- /dev/null +++ b/packages/dockview-angular/src/__tests__/setup-jest.ts @@ -0,0 +1,56 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv(); + +// Global mocks for browser APIs +Object.defineProperty(window, 'CSS', {value: null}); +Object.defineProperty(window, 'getComputedStyle', { + value: () => { + return { + display: 'none', + appearance: ['-webkit-appearance'] + }; + } +}); + +Object.defineProperty(document, 'doctype', { + value: '' +}); + +Object.defineProperty(document.body.style, 'transform', { + value: () => { + return { + enumerable: true, + configurable: true + }; + } +}); + +// Mock ResizeObserver +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Mock getBoundingClientRect +Element.prototype.getBoundingClientRect = jest.fn(() => ({ + width: 100, + height: 100, + top: 0, + left: 0, + bottom: 100, + right: 100, + x: 0, + y: 0, + toJSON: jest.fn(), +})); + +// Mock scrollIntoView +HTMLElement.prototype.scrollIntoView = jest.fn(); +HTMLElement.prototype.focus = jest.fn(); +HTMLElement.prototype.blur = jest.fn(); + +// Mock requestAnimationFrame +global.requestAnimationFrame = jest.fn((cb) => setTimeout(cb, 16)); +global.cancelAnimationFrame = jest.fn((id) => clearTimeout(id)); \ No newline at end of file diff --git a/packages/docs/src/components/demo/Button.tsx b/packages/docs/src/components/demo/Button.tsx new file mode 100644 index 000000000..c4766ac3c --- /dev/null +++ b/packages/docs/src/components/demo/Button.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { Button as ChakraButton, ChakraProvider, defaultSystem } from '@chakra-ui/react'; + +interface ButtonProps { + children: React.ReactNode; + onClick?: () => void; + variant?: 'primary' | 'secondary' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + disabled?: boolean; + className?: string; +} + +export const Button: React.FC = ({ + children, + onClick, + variant = 'secondary', + size = 'md', + disabled = false, + className = '', +}) => { + const chakraVariant = variant === 'primary' ? 'solid' : variant === 'ghost' ? 'ghost' : 'outline'; + const colorPalette = variant === 'primary' ? 'blue' : 'gray'; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/packages/docs/src/components/demo/ButtonGroup.tsx b/packages/docs/src/components/demo/ButtonGroup.tsx new file mode 100644 index 000000000..af21a98e2 --- /dev/null +++ b/packages/docs/src/components/demo/ButtonGroup.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { ButtonGroup as ChakraButtonGroup } from '@chakra-ui/react'; + +interface ButtonGroupProps { + children: React.ReactNode; + className?: string; +} + +export const ButtonGroup: React.FC = ({ children, className = '' }) => { + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/packages/docs/src/components/demo/DockviewDemo.scss b/packages/docs/src/components/demo/DockviewDemo.scss new file mode 100644 index 000000000..215db5fd0 --- /dev/null +++ b/packages/docs/src/components/demo/DockviewDemo.scss @@ -0,0 +1,215 @@ +// Modern Button Styles +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.5; + border-radius: 0.375rem; + border: 1px solid transparent; + text-decoration: none; + cursor: pointer; + transition: all 0.15s ease-in-out; + white-space: nowrap; + user-select: none; + + &:focus { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 2px; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; + } +} + +.btn-primary { + color: white; + background-color: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + + &:hover:not(:disabled) { + background-color: var(--ifm-color-primary-dark); + border-color: var(--ifm-color-primary-dark); + } +} + +.btn-secondary { + color: var(--ifm-color-emphasis-800); + background-color: var(--ifm-background-surface-color); + border-color: var(--ifm-color-emphasis-300); + + &:hover:not(:disabled) { + background-color: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-400); + } +} + +.btn-ghost { + color: var(--ifm-color-emphasis-700); + background-color: transparent; + border-color: transparent; + + &:hover:not(:disabled) { + background-color: var(--ifm-color-emphasis-100); + color: var(--ifm-color-emphasis-800); + } +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.btn-lg { + padding: 0.5rem 1rem; + font-size: 1rem; +} + +// Button Group Styles +.btn-group { + display: inline-flex; + vertical-align: middle; + + .btn { + position: relative; + flex: 1 1 auto; + + &:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; + } + + &:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &:hover { + z-index: 1; + } + + &:focus { + z-index: 2; + } + } +} + +.dockview-demo { + .group-control { + .action { + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + font-size: 18px; + cursor: pointer; + + &:hover { + border-radius: 2px; + color: var(--dv-activegroup-visiblepanel-tab-color); + background-color: var(--dv-icon-hover-background-color); + } + } + } + + .data-table { + overflow: auto; + flex: 1; + min-height: 0; + + table { + font-size: 11px; + width: 100%; + border-collapse: collapse; + + th, td { + padding: 4px 8px; + border: 1px solid var(--ifm-color-emphasis-200); + text-align: left; + vertical-align: top; + word-break: break-word; + max-width: 200px; + } + + th:nth-child(3), td:nth-child(3) { + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + + input { + outline: 1px solid #4c65d4; + border: none; + margin: 0px; + height: 25px; + + &:focus { + outline: 1px solid #4c65d4 !important; + } + } + + .action-container { + display: flex; + padding: 4px; + overflow: auto; + + .text-button { + margin: 0px 4px; + } + + .button-action { + margin: 0px 4px; + + .selected { + background-color: #4864dc; + } + } + + .button-group { + button { + margin-right: 0px; + } + } + + .demo-button { + min-width: 50px; + padding: 0px 2px; + border-radius: 0px; + display: flex; + flex-grow: 1; + align-items: center; + outline: 1px solid #4c65d4; + } + + .demo-icon-button { + outline: 1px solid #4c65d4; + flex-grow: 1; + display: flex; + align-items: center; + border-radius: 0px; + padding: 0px 4px; + border: none; + cursor: pointer; + + &:disabled { + color: gray; + cursor: help; + } + + span { + font-size: 16px; + } + } + } +} \ No newline at end of file diff --git a/packages/docs/src/components/demo/DockviewDemo.tsx b/packages/docs/src/components/demo/DockviewDemo.tsx new file mode 100644 index 000000000..2130bbb9f --- /dev/null +++ b/packages/docs/src/components/demo/DockviewDemo.tsx @@ -0,0 +1,650 @@ +import { + DockviewDefaultTab, + DockviewReact, + DockviewReadyEvent, + IDockviewPanelHeaderProps, + IDockviewPanelProps, + DockviewApi, + DockviewTheme, +} from 'dockview'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; +import './DockviewDemo.scss'; +import { defaultConfig } from './defaultLayout'; +import { GridActions } from './gridActions'; +import { PanelActions } from './panelActions'; +import { GroupActions } from './groupActions'; +import { LeftControls, PrefixHeaderControls, RightControls } from './controls'; +import { Table, usePanelApiMetadata } from './debugPanel'; +import { Button } from './Button'; +import { + ChakraProvider, + createSystem, + defaultConfig as chakraDefaultConfig, +} from '@chakra-ui/react'; +import { useColorMode } from '@docusaurus/theme-common'; + +const DebugContext = React.createContext(false); + +const Option = (props: { + title: string; + onClick: () => void; + value: string; +}) => { + return ( +
+ {`${props.title}: `} + +
+ ); +}; + +const ShadowIframe = (props: IDockviewPanelProps) => { + return ( +