Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions projects/wc/_mocks_/ui5-mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';


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

Expand Down Expand Up @@ -27,6 +28,6 @@ jest.mock('@ui5/webcomponents-ngx', () => {
TitleComponent: MockComponent,
ToolbarButtonComponent: MockComponent,
ToolbarComponent: MockComponent,
BarComponent: MockComponent,
};
});

});
14 changes: 12 additions & 2 deletions projects/wc/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
const path = require('path');

module.exports = {
preset: 'jest-preset-angular',
testRunner: 'jest-jasmine2',
displayName: 'wc',
roots: [__dirname],
testMatch: ['**/*.spec.ts'],
module: 'NodeNext',
moduleResolution: 'NodeNext',
target: 'ES2022',
types: ['jest', 'node'],
testEnvironment: 'jsdom',
coverageDirectory: path.resolve(__dirname, '../../coverage/wc'),
collectCoverageFrom: ['!<rootDir>/projects/wc/**/*.spec.ts'],
coveragePathIgnorePatterns: [
Expand All @@ -12,8 +19,11 @@ module.exports = {
'<rootDir>/projects/wc/src/app/app.config.ts',
'<rootDir>/projects/wc/jest.config.js',
],
setupFilesAfterEnv: [`${__dirname}/jest.setup.ts`],
modulePathIgnorePatterns: ['<rootDir>/projects/wc/_mocks_/'],
// Ensure mocks are applied before modules are loaded
setupFiles: [`${__dirname}/jest.setup.ts`],
setupFilesAfterEnv: [],
// Do not ignore mocks; they are loaded via setupFiles
modulePathIgnorePatterns: [],
coverageThreshold: {
global: {
branches: 85,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<ui5-dialog #dialog>
<ui5-bar slot="header" design="Header">
<ui5-title slot="startContent">
<ui5-icon name="alert" design="Critical"></ui5-icon>
Delete {{ innerResource()?.metadata?.name?.toLowerCase() }}
</ui5-title>
</ui5-bar>
<section class="content" [formGroup]="form">
<div class="inputs">
<p>Are you sure you want to delete {{ context()?.resourceDefinition?.singular }}
<b>{{ innerResource()?.metadata?.name?.toLowerCase() }}</b>?</p>
<ui5-text style="color: var(--sapCriticalElementColor)">
This action <b>cannot</b> be undone.
</ui5-text>
<p>
Please type <b>{{ innerResource()?.metadata?.name.toLowerCase() }}</b> to confirm:
</p>
<ui5-input
class="input"
placeholder="Type name"
[value]="form.controls.resource.value"
(blur)="onFieldBlur('resource')"
(change)="setFormControlValue($event, 'resource')"
(input)="setFormControlValue($event, 'resource')"
[valueState]="getValueState('resource')"
required
></ui5-input>
</div>
</section>
<ui5-toolbar class="ui5-content-density-compact" slot="footer">
<ui5-toolbar-button
class="dialogCloser"
[disabled]="form.invalid || form.controls.resource.value !== innerResource()?.metadata?.name?.toLowerCase()"
design="Emphasized"
text="Delete"
(click)="delete()"
>
</ui5-toolbar-button>
<ui5-toolbar-button
class="dialogCloser"
design="Transparent"
text="Cancel"
(click)="close()"
>
</ui5-toolbar-button>
</ui5-toolbar>
</ui5-dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.content {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: flex-start;
margin-bottom: 0.5rem;
width: 100%;
}

.input {
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { DeleteResourceModalComponent } from './delete-resource-modal.component';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';

describe('DeleteResourceModalComponent', () => {
let component: DeleteResourceModalComponent;
let fixture: ComponentFixture<DeleteResourceModalComponent>;
let mockDialog: any;

const resource: any = { metadata: { name: 'TestName' } };

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CommonModule, ReactiveFormsModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.overrideComponent(DeleteResourceModalComponent, {
set: {
imports: [CommonModule, ReactiveFormsModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
},
})
.compileComponents();

fixture = TestBed.createComponent(DeleteResourceModalComponent);
component = fixture.componentInstance;

mockDialog = { open: false };
(component as any).dialog = () => mockDialog;

component.ngOnInit();
fixture.detectChanges();
});

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

it('should initialize the form with "resource" control', () => {
expect(component.form).toBeDefined();
expect(component.form.controls['resource']).toBeDefined();
});

it('should set dialog open and store innerResource', () => {
component.open(resource);
expect(mockDialog.open).toBeTruthy();
expect(component.innerResource()).toBe(resource);
});

it('should set dialog closed when closing', () => {
mockDialog.open = true;
component.close();
expect(mockDialog.open).toBeFalsy();
});

it('should be invalid when empty or mismatched; valid when matches innerResource.name', () => {
component.open(resource);
const control = component.form.controls['resource'];

control.setValue('');
control.markAsTouched();
fixture.detectChanges();
expect(control.invalid).toBeTruthy();
expect(control.hasError('invalidResource')).toBeTruthy();

control.setValue('WrongName');
fixture.detectChanges();
expect(control.invalid).toBeTruthy();
expect(control.hasError('invalidResource')).toBeTruthy();

control.setValue('TestName');
fixture.detectChanges();
expect(control.valid).toBeTruthy();
expect(control.errors).toBeNull();
});

it('should emit the resource and close the dialog when deleting resource', () => {
component.open(resource);
spyOn(component.resource, 'emit');
component.delete();
expect(component.resource.emit).toHaveBeenCalledWith(resource);
expect(mockDialog.open).toBeFalsy();
});

it('should set value and marks touched/dirty', () => {
const control = component.form.controls['resource'];
spyOn(control, 'setValue');
spyOn(control, 'markAsTouched');
spyOn(control, 'markAsDirty');

component.setFormControlValue(
{ target: { value: 'SomeValue' } } as any,
'resource',
);

expect(control.setValue).toHaveBeenCalledWith('SomeValue');
expect(control.markAsTouched).toHaveBeenCalled();
expect(control.markAsDirty).toHaveBeenCalled();
});

it('should return "Negative" for invalid+touched, else "None"', () => {
const control = component.form.controls['resource'];

control.setValue('');
control.markAsTouched();
fixture.detectChanges();
expect(component.getValueState('resource')).toBe('Negative');

component.open(resource);
control.setValue('TestName');
fixture.detectChanges();
expect(component.getValueState('resource')).toBe('None');

control.setValue('');
control.markAsUntouched();
fixture.detectChanges();
expect(component.getValueState('resource')).toBe('None');
});

it('should mark the control as touched', () => {
const control = component.form.controls['resource'];
spyOn(control, 'markAsTouched');
component.onFieldBlur('resource');
expect(control.markAsTouched).toHaveBeenCalled();
});

it('should render title with resource name in lowercase in the header', () => {
component.open(resource);
fixture.detectChanges();
const title = fixture.nativeElement.querySelector('ui5-title');
expect(title?.textContent?.toLowerCase()).toContain('delete testname');
});

it('should render prompt text with resource name and cannot be undone note', () => {
component.open(resource);
(component as any).context = () => ({
resourceDefinition: { singular: 'resource' },
});
fixture.detectChanges();
const content = fixture.nativeElement.querySelector('section.content');
const text = content?.textContent?.toLowerCase() || '';
expect(text).toContain('are you sure you want to delete');
expect(text).toContain('testname');
expect(text).toContain('cannot');
});

it('should bind input value to form control and show Negative valueState when invalid and touched', () => {
component.open(resource);
fixture.detectChanges();

const inputEl: HTMLElement & {
value?: string;
valueState?: string;
dispatchEvent?: any;
} = fixture.nativeElement.querySelector('ui5-input');
expect(inputEl).toBeTruthy();

component.setFormControlValue(
{ target: { value: 'wrong' } } as any,
'resource',
);
component.onFieldBlur('resource');
fixture.detectChanges();

expect(component.form.controls['resource'].invalid).toBeTruthy();
expect(component.getValueState('resource')).toBe('Negative');
});

it('should close dialog when Cancel button clicked', () => {
component.open(resource);
mockDialog.open = true;
fixture.detectChanges();

const cancelBtn: HTMLElement = fixture.nativeElement.querySelector(
'ui5-toolbar-button[design="Transparent"]',
);
expect(cancelBtn).toBeTruthy();

cancelBtn.dispatchEvent(new Event('click'));
fixture.detectChanges();
expect(mockDialog.open).toBeFalsy();
});

it('should reset control state on close (pristine, untouched, revalidated)', () => {
component.open(resource);
const control = component.form.controls['resource'];
control.setValue('wrong');
control.markAsTouched();
control.markAsDirty();
fixture.detectChanges();
expect(control.invalid).toBeTruthy();

component.close();
fixture.detectChanges();

expect(control.value).toBeNull();
expect(control.pristine).toBeTruthy();
expect(control.touched).toBeFalsy();
expect(control.invalid).toBeTruthy();
expect(control.hasError('invalidResource')).toBeTruthy();
});
});
Loading