Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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,185 @@
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();
});
});
Loading