Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ module.exports = {
collectCoverage: true,
modulePathIgnorePatterns: ['<rootDir>/dist/'],
coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'],
moduleNameMapper: {
'^@luigi-project/client-support-angular$': '<rootDir>/projects/lib/_mocks_/luigi-client-support-angular.ts',
},
};
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"@angular-builders/jest": "^20.0.0",
"@angular-devkit/build-angular": "^20.0.0",
"@angular-eslint/builder": "^20.2.0",
"@angular/cli": "^20.2.0",
"@angular/build": "^20.2.1",
"@angular/cli": "^20.2.0",
"@angular/compiler-cli": "^20.2.1",
"@angular/localize": "^20.2.1",
"@briebug/jest-schematic": "^6.0.0",
Expand All @@ -30,11 +30,11 @@
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jmespath": "0.16.0",
"mkdirp": "^3.0.1",
"ng-packagr": "^20.2.0",
"ts-jest": "29.3.2",
"typescript": "~5.8.0",
"nodemon": "3.1.10",
"rimraf": "6.0.1",
"mkdirp": "^3.0.1",
"nodemon": "3.1.10"
"ts-jest": "29.3.2",
"typescript": "~5.8.0"
}
}
6 changes: 6 additions & 0 deletions projects/lib/_mocks_/luigi-client-support-angular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Observable, of } from 'rxjs';
export class LuigiContextService {
contextObservable(): Observable<{ context: any }> {
return of({ context: null });
}
}
32 changes: 32 additions & 0 deletions projects/lib/_mocks_/ui5‑mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Component } from '@angular/core';
import { ButtonComponent } from '@ui5/webcomponents-ngx';

@Component({ selector: 'ui5-component', template: '', standalone: true })
export class MockComponent {}

jest.mock('@ui5/webcomponents-ngx', () => {
return {
BreadcrumbsComponent: MockComponent,
BreadcrumbsItemComponent: MockComponent,
ButtonComponent: MockComponent,
DialogComponent: MockComponent,
DynamicPageComponent: MockComponent,
DynamicPageHeaderComponent: MockComponent,
DynamicPageTitleComponent: MockComponent,
IconComponent: MockComponent,
IllustratedMessageComponent: MockComponent,
InputComponent: MockComponent,
LabelComponent: MockComponent,
OptionComponent: MockComponent,
SelectComponent: MockComponent,
TableCellComponent: MockComponent,
TableComponent: MockComponent,
TableHeaderCellComponent: MockComponent,
TableHeaderRowComponent: MockComponent,
TableRowComponent: MockComponent,
TextComponent: MockComponent,
TitleComponent: MockComponent,
ToolbarButtonComponent: MockComponent,
ToolbarComponent: MockComponent,
};
});
1 change: 1 addition & 0 deletions projects/lib/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
module.exports = {
displayName: 'lib',
coverageDirectory: path.resolve(__dirname, '../../coverage/lib'),
setupFilesAfterEnv: [`${__dirname}/jest.setup.ts`],
coverageThreshold: {
global: {
branches: 67,
Expand Down
1 change: 1 addition & 0 deletions projects/lib/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
jest.requireMock('./_mocks_/ui5‑mock');
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div class="organization-management">
<div>
{{ texts.explanation }}
</div>

<div>
<ui5-label for="select-switch" show-colon>{{
texts.switchOrganization.label
}}</ui5-label
><br />
<div class="organization-management-input">
<ui5-select
id="select-switch"
[value]="organizationToSwitch"
(input)="setOrganizationToSwitch($event)"
(change)="setOrganizationToSwitch($event)"
>
@for (org of organizations(); track org) {
<ui5-option [value]="org" [selected]="org === organizationToSwitch">{{
org
}}</ui5-option>
}
</ui5-select>
<ui5-button design="Emphasized" (ui5Click)="switchOrganization()">{{
texts.switchOrganization.button
}}</ui5-button>
</div>
</div>

<div>
<ui5-label for="input-onboard" show-colon>{{
texts.onboardOrganization.label
}}</ui5-label
><br />
<div class="organization-management-input">
<ui5-input
id="input-onboard"
placeholder="{{ texts.onboardOrganization.placeholder }}"
[(ngModel)]="newOrganization"
></ui5-input>
<ui5-button design="Emphasized" (ui5Click)="onboardOrganization()">{{
texts.onboardOrganization.button
}}</ui5-button>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.organization-management {
margin: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}

.organization-management-input {
display: flex;
align-items: center;
gap: 1rem;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
CUSTOM_ELEMENTS_SCHEMA,
NO_ERRORS_SCHEMA,
signal
} from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { MutationResult } from '@apollo/client';
import { LuigiContextService } from '@luigi-project/client-support-angular';
import {
ClientEnvironment, EnvConfigService,
I18nService,
LuigiCoreService, LuigiGlobalContext, NodeContext, ResourceService
} from '@openmfp/portal-ui-lib';
import { of, throwError } from 'rxjs';
import { OrganizationManagementComponent } from './organization-management.component';

describe('OrganizationManagementComponent', () => {
let component: OrganizationManagementComponent;
let fixture: ComponentFixture<OrganizationManagementComponent>;
let resourceServiceMock: jest.Mocked<ResourceService>;
let i18nServiceMock: jest.Mocked<I18nService>;
let luigiCoreServiceMock: jest.Mocked<LuigiCoreService>;
let envConfigServiceMock: jest.Mocked<EnvConfigService>;

beforeEach(async () => {
resourceServiceMock = {
readOrganizations: jest.fn(),
create: jest.fn(),
} as any;

i18nServiceMock = {
translationTable: {},
getTranslation: jest.fn(),
} as any;

luigiCoreServiceMock = {
getGlobalContext: jest.fn(),
showAlert: jest.fn(),
} as any;

envConfigServiceMock = {
getEnvConfig: jest.fn(),
} as any;

await TestBed.configureTestingModule({
imports: [OrganizationManagementComponent, FormsModule],
providers: [
{ provide: ResourceService, useValue: resourceServiceMock },
{ provide: I18nService, useValue: i18nServiceMock },
{ provide: LuigiCoreService, useValue: luigiCoreServiceMock },
{ provide: EnvConfigService, useValue: envConfigServiceMock },
LuigiContextService,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA],
})
.overrideComponent(OrganizationManagementComponent, {
set: { template: '', imports: [] },
})
.compileComponents();

fixture = TestBed.createComponent(OrganizationManagementComponent);
component = fixture.componentInstance;
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should react to context input change', () => {
const mockContext = {
translationTable: { hello: 'world' },
} as any as NodeContext;

resourceServiceMock.readOrganizations.mockReturnValue(of({} as any));

const contextSignal = signal<NodeContext | null>(mockContext);
component.context = contextSignal as any;

fixture.detectChanges();

expect(component['i18nService'].translationTable).toEqual(
mockContext.translationTable,
);
});

it('should initialize with empty organizations', () => {
expect(component.organizations()).toEqual([]);
});

it('should read organizations on init', () => {
const mockOrganizations = {
Accounts: [
{ metadata: { name: 'org1' } },
{ metadata: { name: 'org2' } },
],
};
const mockGlobalContext: LuigiGlobalContext = {
portalContext: {},
userId: 'user1',
userEmail: '[email protected]',
token: 'token',
organization: 'org1',
portalBaseUrl: 'https://test.com',
};
luigiCoreServiceMock.getGlobalContext.mockReturnValue(mockGlobalContext);
resourceServiceMock.readOrganizations.mockReturnValue(
of(mockOrganizations as any),
);

component.ngOnInit();

expect(resourceServiceMock.readOrganizations).toHaveBeenCalled();
expect(component.organizations()).toEqual(['org2']);
});

it('should set organization to switch', () => {
const event = { target: { value: 'testOrg' } };
component.setOrganizationToSwitch(event);
expect(component.organizationToSwitch).toBe('testOrg');
});

it('should onboard new organization successfully', () => {
const mockResponse: MutationResult<void> = {
data: undefined,
loading: false,
error: undefined,
called: true,
client: {} as any,
reset: jest.fn(),
};
resourceServiceMock.create.mockReturnValue(of(mockResponse));
component.newOrganization = 'newOrg';
component.organizations.set(['existingOrg']);

component.onboardOrganization();

expect(resourceServiceMock.create).toHaveBeenCalled();
expect(component.organizations()).toEqual(['newOrg', 'existingOrg']);
expect(component.organizationToSwitch).toBe('newOrg');
expect(component.newOrganization).toBe('');
});

it('should handle organization creation error', () => {
resourceServiceMock.create.mockReturnValue(
throwError(() => new Error('Creation failed')),
);
component.newOrganization = 'newOrg';

component.onboardOrganization();

expect(luigiCoreServiceMock.showAlert).toHaveBeenCalledWith({
text: 'Failure! Could not create organization: newOrg.',
type: 'error',
});
});

it('should switch organization', async () => {
const mockEnvConfig: ClientEnvironment = {
idpName: 'test',
organization: 'test',
oauthServerUrl: 'https://test.com',
clientId: 'test',
baseDomain: 'test.com',
isLocal: false,
developmentInstance: false,
authData: {
expires_in: '3600',
access_token: 'test-access-token',
id_token: 'test-id-token',
},
};
envConfigServiceMock.getEnvConfig.mockResolvedValue(mockEnvConfig);
component.organizationToSwitch = 'newOrg';
Object.defineProperty(window, 'location', {
value: { protocol: 'https:', port: '8080' },
writable: true,
});

await component.switchOrganization();

expect(window.location.href).toBe('https://newOrg.test.com:8080');
});
});
Loading