Skip to content

Commit 321fe6f

Browse files
makdenissmakdenissSobyt483makdeniss
authored
feat: add delete resource confirmation dialog (#339) (#23)
* feat: add delete resource confirmation dialog (#339) * feat: fix failing test (#339) * feat: fix failing test (#339) * fix: fix tests * fix: fix test * fix: fix tests * fix: fix code coverage (#339) * fix: fixes per PR comments (#339) --------- Co-authored-by: makdeniss <[email protected]> Co-authored-by: Sobyt483 <[email protected]> Co-authored-by: makdeniss <[email protected]>
1 parent 118ca86 commit 321fe6f

File tree

9 files changed

+450
-23
lines changed

9 files changed

+450
-23
lines changed

projects/wc/_mocks_/ui5-mock.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component } from '@angular/core';
22

3+
34
@Component({ selector: 'ui5-component', template: '', standalone: true })
45
export class MockComponent {}
56

@@ -27,6 +28,6 @@ jest.mock('@ui5/webcomponents-ngx', () => {
2728
TitleComponent: MockComponent,
2829
ToolbarButtonComponent: MockComponent,
2930
ToolbarComponent: MockComponent,
31+
BarComponent: MockComponent,
3032
};
31-
});
32-
33+
});

projects/wc/jest.config.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
const path = require('path');
22

33
module.exports = {
4+
preset: 'jest-preset-angular',
5+
testRunner: 'jest-jasmine2',
46
displayName: 'wc',
57
roots: [__dirname],
68
testMatch: ['**/*.spec.ts'],
9+
module: 'NodeNext',
10+
moduleResolution: 'NodeNext',
11+
target: 'ES2022',
12+
types: ['jest', 'node'],
13+
testEnvironment: 'jsdom',
714
coverageDirectory: path.resolve(__dirname, '../../coverage/wc'),
815
collectCoverageFrom: ['!<rootDir>/projects/wc/**/*.spec.ts'],
916
coveragePathIgnorePatterns: [
@@ -12,8 +19,11 @@ module.exports = {
1219
'<rootDir>/projects/wc/src/app/app.config.ts',
1320
'<rootDir>/projects/wc/jest.config.js',
1421
],
15-
setupFilesAfterEnv: [`${__dirname}/jest.setup.ts`],
16-
modulePathIgnorePatterns: ['<rootDir>/projects/wc/_mocks_/'],
22+
// Ensure mocks are applied before modules are loaded
23+
setupFiles: [`${__dirname}/jest.setup.ts`],
24+
setupFilesAfterEnv: [],
25+
// Do not ignore mocks; they are loaded via setupFiles
26+
modulePathIgnorePatterns: [],
1727
coverageThreshold: {
1828
global: {
1929
branches: 85,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<ui5-dialog #dialog>
2+
<ui5-bar slot="header" design="Header">
3+
<ui5-title slot="startContent">
4+
<ui5-icon name="alert" design="Critical"></ui5-icon>
5+
Delete {{ innerResource()?.metadata?.name?.toLowerCase() }}
6+
</ui5-title>
7+
</ui5-bar>
8+
<section class="content" [formGroup]="form">
9+
<div class="inputs">
10+
<p>Are you sure you want to delete {{ context()?.resourceDefinition?.singular }}
11+
<b>{{ innerResource()?.metadata?.name?.toLowerCase() }}</b>?</p>
12+
<ui5-text style="color: var(--sapCriticalElementColor)">
13+
This action <b>cannot</b> be undone.
14+
</ui5-text>
15+
<p>
16+
Please type <b>{{ innerResource()?.metadata?.name.toLowerCase() }}</b> to confirm:
17+
</p>
18+
<ui5-input
19+
class="input"
20+
placeholder="Type name"
21+
[value]="form.controls.resource.value"
22+
(blur)="onFieldBlur('resource')"
23+
(change)="setFormControlValue($event, 'resource')"
24+
(input)="setFormControlValue($event, 'resource')"
25+
[valueState]="getValueState('resource')"
26+
required
27+
></ui5-input>
28+
</div>
29+
</section>
30+
<ui5-toolbar class="ui5-content-density-compact" slot="footer">
31+
<ui5-toolbar-button
32+
class="dialogCloser"
33+
[disabled]="form.invalid || form.controls.resource.value !== innerResource()?.metadata?.name?.toLowerCase()"
34+
design="Emphasized"
35+
text="Delete"
36+
(click)="delete()"
37+
>
38+
</ui5-toolbar-button>
39+
<ui5-toolbar-button
40+
class="dialogCloser"
41+
design="Transparent"
42+
text="Cancel"
43+
(click)="close()"
44+
>
45+
</ui5-toolbar-button>
46+
</ui5-toolbar>
47+
</ui5-dialog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.content {
2+
display: flex;
3+
flex-direction: column;
4+
justify-content: space-evenly;
5+
align-items: flex-start;
6+
margin-bottom: 0.5rem;
7+
width: 100%;
8+
}
9+
10+
.input {
11+
width: 100%;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { DeleteResourceModalComponent } from './delete-resource-modal.component';
2+
import { CommonModule } from '@angular/common';
3+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
4+
import { ComponentFixture, TestBed } from '@angular/core/testing';
5+
import { ReactiveFormsModule } from '@angular/forms';
6+
7+
describe('DeleteResourceModalComponent', () => {
8+
let component: DeleteResourceModalComponent;
9+
let fixture: ComponentFixture<DeleteResourceModalComponent>;
10+
let mockDialog: any;
11+
12+
const resource: any = { metadata: { name: 'TestName' } };
13+
14+
beforeEach(async () => {
15+
await TestBed.configureTestingModule({
16+
imports: [CommonModule, ReactiveFormsModule],
17+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
18+
})
19+
.overrideComponent(DeleteResourceModalComponent, {
20+
set: {
21+
imports: [CommonModule, ReactiveFormsModule],
22+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
23+
},
24+
})
25+
.compileComponents();
26+
27+
fixture = TestBed.createComponent(DeleteResourceModalComponent);
28+
component = fixture.componentInstance;
29+
30+
mockDialog = { open: false };
31+
(component as any).dialog = () => mockDialog;
32+
33+
component.ngOnInit();
34+
fixture.detectChanges();
35+
});
36+
37+
it('should create the component', () => {
38+
expect(component).toBeTruthy();
39+
});
40+
41+
it('should initialize the form with "resource" control', () => {
42+
expect(component.form).toBeDefined();
43+
expect(component.form.controls['resource']).toBeDefined();
44+
});
45+
46+
it('should set dialog open and store innerResource', () => {
47+
component.open(resource);
48+
expect(mockDialog.open).toBeTruthy();
49+
expect(component.innerResource()).toBe(resource);
50+
});
51+
52+
it('should set dialog closed when closing', () => {
53+
mockDialog.open = true;
54+
component.close();
55+
expect(mockDialog.open).toBeFalsy();
56+
});
57+
58+
it('should be invalid when empty or mismatched; valid when matches innerResource.name', () => {
59+
component.open(resource);
60+
const control = component.form.controls['resource'];
61+
62+
control.setValue('');
63+
control.markAsTouched();
64+
fixture.detectChanges();
65+
expect(control.invalid).toBeTruthy();
66+
expect(control.hasError('invalidResource')).toBeTruthy();
67+
68+
control.setValue('WrongName');
69+
fixture.detectChanges();
70+
expect(control.invalid).toBeTruthy();
71+
expect(control.hasError('invalidResource')).toBeTruthy();
72+
73+
control.setValue('TestName');
74+
fixture.detectChanges();
75+
expect(control.valid).toBeTruthy();
76+
expect(control.errors).toBeNull();
77+
});
78+
79+
it('should emit the resource and close the dialog when deleting resource', () => {
80+
component.open(resource);
81+
spyOn(component.resource, 'emit');
82+
component.delete();
83+
expect(component.resource.emit).toHaveBeenCalledWith(resource);
84+
expect(mockDialog.open).toBeFalsy();
85+
});
86+
87+
it('should set value and marks touched/dirty', () => {
88+
const control = component.form.controls['resource'];
89+
spyOn(control, 'setValue');
90+
spyOn(control, 'markAsTouched');
91+
spyOn(control, 'markAsDirty');
92+
93+
component.setFormControlValue(
94+
{ target: { value: 'SomeValue' } } as any,
95+
'resource',
96+
);
97+
98+
expect(control.setValue).toHaveBeenCalledWith('SomeValue');
99+
expect(control.markAsTouched).toHaveBeenCalled();
100+
expect(control.markAsDirty).toHaveBeenCalled();
101+
});
102+
103+
it('should return "Negative" for invalid+touched, else "None"', () => {
104+
const control = component.form.controls['resource'];
105+
106+
control.setValue('');
107+
control.markAsTouched();
108+
fixture.detectChanges();
109+
expect(component.getValueState('resource')).toBe('Negative');
110+
111+
component.open(resource);
112+
control.setValue('TestName');
113+
fixture.detectChanges();
114+
expect(component.getValueState('resource')).toBe('None');
115+
116+
control.setValue('');
117+
control.markAsUntouched();
118+
fixture.detectChanges();
119+
expect(component.getValueState('resource')).toBe('None');
120+
});
121+
122+
it('should mark the control as touched', () => {
123+
const control = component.form.controls['resource'];
124+
spyOn(control, 'markAsTouched');
125+
component.onFieldBlur('resource');
126+
expect(control.markAsTouched).toHaveBeenCalled();
127+
});
128+
129+
it('should render title with resource name in lowercase in the header', () => {
130+
component.open(resource);
131+
fixture.detectChanges();
132+
const title = fixture.nativeElement.querySelector('ui5-title');
133+
expect(title?.textContent?.toLowerCase()).toContain('delete testname');
134+
});
135+
136+
it('should render prompt text with resource name and cannot be undone note', () => {
137+
component.open(resource);
138+
(component as any).context = () => ({
139+
resourceDefinition: { singular: 'resource' },
140+
});
141+
fixture.detectChanges();
142+
const content = fixture.nativeElement.querySelector('section.content');
143+
const text = content?.textContent?.toLowerCase() || '';
144+
expect(text).toContain('are you sure you want to delete');
145+
expect(text).toContain('testname');
146+
expect(text).toContain('cannot');
147+
});
148+
149+
it('should bind input value to form control and show Negative valueState when invalid and touched', () => {
150+
component.open(resource);
151+
fixture.detectChanges();
152+
153+
const inputEl: HTMLElement & {
154+
value?: string;
155+
valueState?: string;
156+
dispatchEvent?: any;
157+
} = fixture.nativeElement.querySelector('ui5-input');
158+
expect(inputEl).toBeTruthy();
159+
160+
component.setFormControlValue(
161+
{ target: { value: 'wrong' } } as any,
162+
'resource',
163+
);
164+
component.onFieldBlur('resource');
165+
fixture.detectChanges();
166+
167+
expect(component.form.controls['resource'].invalid).toBeTruthy();
168+
expect(component.getValueState('resource')).toBe('Negative');
169+
});
170+
171+
it('should close dialog when Cancel button clicked', () => {
172+
component.open(resource);
173+
mockDialog.open = true;
174+
fixture.detectChanges();
175+
176+
const cancelBtn: HTMLElement = fixture.nativeElement.querySelector(
177+
'ui5-toolbar-button[design="Transparent"]',
178+
);
179+
expect(cancelBtn).toBeTruthy();
180+
181+
cancelBtn.dispatchEvent(new Event('click'));
182+
fixture.detectChanges();
183+
expect(mockDialog.open).toBeFalsy();
184+
});
185+
186+
it('should reset control state on close (pristine, untouched, revalidated)', () => {
187+
component.open(resource);
188+
const control = component.form.controls['resource'];
189+
control.setValue('wrong');
190+
control.markAsTouched();
191+
control.markAsDirty();
192+
fixture.detectChanges();
193+
expect(control.invalid).toBeTruthy();
194+
195+
component.close();
196+
fixture.detectChanges();
197+
198+
expect(control.value).toBeNull();
199+
expect(control.pristine).toBeTruthy();
200+
expect(control.touched).toBeFalsy();
201+
expect(control.invalid).toBeTruthy();
202+
expect(control.hasError('invalidResource')).toBeTruthy();
203+
});
204+
});

0 commit comments

Comments
 (0)