diff --git a/apps/metadata-editor-e2e/src/e2e/delete.cy.ts b/apps/metadata-editor-e2e/src/e2e/delete.cy.ts new file mode 100644 index 0000000000..63dfb4932c --- /dev/null +++ b/apps/metadata-editor-e2e/src/e2e/delete.cy.ts @@ -0,0 +1,65 @@ +describe('delete', () => { + beforeEach(() => { + cy.login('admin', 'admin', false) + cy.visit('/catalog/search') + }) + + describe('record with draft', () => { + it('should delete the record, delete its associated draft and refresh the interface', () => { + // First create a record and its draft + cy.get('[data-cy="create-record"]').click() + cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea').type( + 'record abstract' + ) + cy.intercept({ + method: 'PUT', + pathname: '**/records', + }).as('insertRecord') + cy.get('md-editor-publish-button').click() + cy.wait('@insertRecord') + cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea').type( + 'draft abstract' + ) + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(1000) // waiting for draft saving to kick in + cy.visit('/catalog/search?_sort=-changeDate') + cy.get('[data-cy="table-row"]') + .contains('My new record') + .should('have.length', 1) + cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '1') + // Delete the record + cy.get('[data-test="record-menu-button"]').first().click() + cy.get('[data-test="record-menu-delete-button"]').click() + cy.get('[data-cy="confirm-button"]').click() + cy.get('[data-cy="table-row"]') + .contains('My new record') + .should('have.length', 0) + cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '0') + }) + }) + + describe('draft without record', () => { + it('should delete the draft and refresh the interface', () => { + // First create a draft + cy.get('[data-cy="create-record"]').click() + cy.get('gn-ui-form-field[ng-reflect-model=abstract] textarea').type( + 'draft abstract' + ) + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(1000) // waiting for draft saving to kick in + cy.visit('/my-space/my-draft') + cy.get('[data-cy="table-row"]') + .contains('My new record') + .should('have.length', 1) + cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '1') + // Delete the draft + cy.get('[data-test="record-menu-button"]').click() + cy.get('[data-test="record-menu-delete-button"]').click() + cy.get('[data-cy="confirm-button"]').click() + cy.get('[data-cy="table-row"]') + .contains('New record') + .should('have.length', 0) + cy.get('[data-cy="dashboard-drafts-count"]').should('contain', '0') + }) + }) +}) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html index ed67fe8b72..b57c19c048 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.html @@ -67,9 +67,11 @@ > edit_note dashboard.records.myDraft - {{ - draftsCount$ | async - }} + {{ draftsCount$ | async }} { let component: DashboardMenuComponent let fixture: ComponentFixture + let recordsRepository: RecordsRepositoryInterface + + beforeEach(() => { + return MockBuilder(DashboardMenuComponent) + }) beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DashboardMenuComponent, TranslateModule.forRoot()], - providers: [ - { - provide: ActivatedRoute, - useValue: { params: of({ id: 1 }) }, - }, - { - provide: RecordsRepositoryInterface, - useClass: RecordsRepositoryMock, - }, - ], - schemas: [NO_ERRORS_SCHEMA], + providers: [MockProviders(ActivatedRoute, RecordsRepositoryInterface)], }).compileComponents() + recordsRepository = TestBed.inject(RecordsRepositoryInterface) fixture = TestBed.createComponent(DashboardMenuComponent) component = fixture.componentInstance fixture.detectChanges() @@ -38,4 +29,20 @@ describe('DashboardMenuComponent', () => { it('should create', () => { expect(component).toBeTruthy() }) + + it('should emit draftsCount$ immediately and then on drafts change', () => { + // Mock the source observable that draftsCount$ depends on + recordsRepository.draftsChanged$ = hot('-a-|', { + a: void 0, + }) + recordsRepository.getAllDrafts = jest + .fn() + .mockReturnValue(hot('ab-|', { a: [], b: [{}] })) + + // Define the expected marble diagram + const expected = cold('ab-|', { a: 0, b: 1 }) + + // Assert that draftsCount$ behaves as expected + expect(component.draftsCount$).toBeObservable(expected) + }) }) diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts index 4845040432..e2bb95762b 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts +++ b/apps/metadata-editor/src/app/dashboard/dashboard-menu/dashboard-menu.component.ts @@ -4,7 +4,7 @@ import { MatIconModule } from '@angular/material/icon' import { RouterModule } from '@angular/router' import { TranslateModule } from '@ngx-translate/core' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' -import { map } from 'rxjs/operators' +import { map, startWith, switchMap } from 'rxjs/operators' import { BadgeComponent } from '@geonetwork-ui/ui/inputs' @Component({ @@ -22,9 +22,11 @@ import { BadgeComponent } from '@geonetwork-ui/ui/inputs' ], }) export class DashboardMenuComponent { - draftsCount$ = this.recordsRepository - .getAllDrafts() - .pipe(map((drafts) => drafts.length)) + draftsCount$ = this.recordsRepository.draftsChanged$.pipe( + startWith(void 0), + switchMap(() => this.recordsRepository.getAllDrafts()), + map((drafts) => drafts.length) + ) constructor(private recordsRepository: RecordsRepositoryInterface) {} } diff --git a/apps/metadata-editor/src/app/dashboard/dashboard-page.component.spec.ts b/apps/metadata-editor/src/app/dashboard/dashboard-page.component.spec.ts index 036765507b..6f3395deb0 100644 --- a/apps/metadata-editor/src/app/dashboard/dashboard-page.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/dashboard-page.component.spec.ts @@ -1,21 +1,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MockBuilder } from 'ng-mocks' import { DashboardPageComponent } from './dashboard-page.component' -import { CommonModule } from '@angular/common' describe('DashboardPageComponent', () => { let component: DashboardPageComponent let fixture: ComponentFixture - beforeEach(async () => { - await TestBed.configureTestingModule({}) - .overrideComponent(DashboardPageComponent, { - set: { - imports: [CommonModule], - providers: [], - }, - }) - .compileComponents() + beforeEach(() => { + return MockBuilder(DashboardPageComponent) + }) + beforeEach(() => { fixture = TestBed.createComponent(DashboardPageComponent) component = fixture.componentInstance fixture.detectChanges() diff --git a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts index e0337e836a..8e897166be 100644 --- a/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/search-header/search-header.component.spec.ts @@ -1,8 +1,4 @@ -import { - ChangeDetectionStrategy, - CUSTOM_ELEMENTS_SCHEMA, - NO_ERRORS_SCHEMA, -} from '@angular/core' +import { ChangeDetectionStrategy, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' import { SearchHeaderComponent } from './search-header.component' @@ -37,7 +33,6 @@ describe('SearchHeaderComponent', () => { StoreModule.forRoot({}), TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), ], - schemas: [NO_ERRORS_SCHEMA], providers: [ { provide: AvatarServiceInterface, diff --git a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts index 362fb48249..50982f3777 100644 --- a/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts +++ b/apps/metadata-editor/src/app/dashboard/sidebar/sidebar.component.spec.ts @@ -1,66 +1,30 @@ -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' -import { of } from 'rxjs' -import { SidebarComponent } from './sidebar.component' -import { ActivatedRoute } from '@angular/router' -import { TranslateModule } from '@ngx-translate/core' -import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' -import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' -import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' - -class RecordsRepositoryMock { - getAllDrafts = jest.fn().mockReturnValue(of(DATASET_RECORDS)) -} - -class PlatformServiceMock { - getMe = jest.fn().mockReturnValue(of({ organisation: 'organisation' })) -} - -class AvatarServiceInterfaceMock { - getPlaceholder = () => of('http://placeholder.com') -} - -class OrganisationsServiceMock { - organisations$ = of([{ name: 'organisation' }]) -} +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { TranslateModule } from '@ngx-translate/core' +import { MockBuilder, MockProviders } from 'ng-mocks' +import { SidebarComponent } from './sidebar.component' describe('SidebarComponent', () => { let component: SidebarComponent let fixture: ComponentFixture + beforeEach(() => { + return MockBuilder(SidebarComponent) + }) + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SidebarComponent, TranslateModule.forRoot()], providers: [ - { - provide: ActivatedRoute, - useValue: { params: of({ id: 1 }) }, - }, - { - provide: RecordsRepositoryInterface, - useClass: RecordsRepositoryMock, - }, - { - provide: PlatformServiceInterface, - useClass: PlatformServiceMock, - }, - { - provide: AvatarServiceInterface, - useClass: AvatarServiceInterfaceMock, - }, - { - provide: OrganizationsServiceInterface, - useClass: OrganisationsServiceMock, - }, + MockProviders( + PlatformServiceInterface, + AvatarServiceInterface, + OrganizationsServiceInterface + ), ], - schemas: [NO_ERRORS_SCHEMA], - }) - .overrideComponent(SidebarComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() + }).compileComponents() fixture = TestBed.createComponent(SidebarComponent) component = fixture.componentInstance diff --git a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts index 4889c0822c..6830ace538 100644 --- a/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts +++ b/apps/metadata-editor/src/app/edit/edit-page.component.spec.ts @@ -1,7 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { EditPageComponent } from './edit-page.component' import { ActivatedRoute, Router } from '@angular/router' -import { NO_ERRORS_SCHEMA } from '@angular/core' import { DATASET_RECORDS, EDITOR_CONFIG } from '@geonetwork-ui/common/fixtures' import { BehaviorSubject, Subject } from 'rxjs' import { NotificationsService } from '@geonetwork-ui/feature/notifications' @@ -49,7 +48,6 @@ describe('EditPageComponent', () => { TranslateModule.forRoot(), PageSelectorComponent, ], - schemas: [NO_ERRORS_SCHEMA], providers: [ { provide: ActivatedRoute, diff --git a/apps/metadata-editor/src/app/new-record.resolver.spec.ts b/apps/metadata-editor/src/app/new-record.resolver.spec.ts index 6f88be9b7a..e77e545fcd 100644 --- a/apps/metadata-editor/src/app/new-record.resolver.spec.ts +++ b/apps/metadata-editor/src/app/new-record.resolver.spec.ts @@ -1,14 +1,20 @@ import { TestBed } from '@angular/core/testing' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { MockProvider } from 'ng-mocks' import { NewRecordResolver } from './new-record.resolver' describe('NewRecordResolver', () => { let resolver: NewRecordResolver let resolvedData: [CatalogRecord, string, boolean] + let recordsRepository: RecordsRepositoryInterface beforeEach(() => { - TestBed.configureTestingModule({}) + TestBed.configureTestingModule({ + providers: [MockProvider(RecordsRepositoryInterface)], + }) resolver = TestBed.inject(NewRecordResolver) + recordsRepository = TestBed.inject(RecordsRepositoryInterface) }) it('should be created', () => { @@ -17,8 +23,10 @@ describe('NewRecordResolver', () => { describe('new record', () => { beforeEach(() => { + recordsRepository.generateTemporaryId = jest.fn(() => 'TEMP-ID-123') resolvedData = undefined resolver.resolve().subscribe((r) => (resolvedData = r)) + recordsRepository.generateTemporaryId = jest.fn(() => 'TEMP-ID-123') }) it('creates a new empty record with a pregenerated id', () => { expect(resolvedData).toMatchObject([ @@ -29,7 +37,7 @@ describe('NewRecordResolver', () => { status: 'ongoing', temporalExtents: [], title: expect.stringMatching(/^My new record/), - uniqueIdentifier: expect.stringMatching(/^TEMP-ID-/), + uniqueIdentifier: 'TEMP-ID-123', }, null, false, diff --git a/apps/metadata-editor/src/app/new-record.resolver.ts b/apps/metadata-editor/src/app/new-record.resolver.ts index ec206eeb99..bfd2cc6a8d 100644 --- a/apps/metadata-editor/src/app/new-record.resolver.ts +++ b/apps/metadata-editor/src/app/new-record.resolver.ts @@ -1,15 +1,18 @@ import { Injectable } from '@angular/core' -import { Observable, of } from 'rxjs' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { Observable, of } from 'rxjs' @Injectable({ providedIn: 'root', }) export class NewRecordResolver { + constructor(private recordsRepository: RecordsRepositoryInterface) {} + resolve(): Observable<[CatalogRecord, string, boolean]> { return of([ { - uniqueIdentifier: `TEMP-ID-${Date.now()}`, + uniqueIdentifier: this.recordsRepository.generateTemporaryId(), title: `My new record (${new Date().toISOString()})`, abstract: '', ownerOrganization: {}, diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html index de8be72269..c9d8dbb4f2 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.html @@ -10,8 +10,10 @@

> diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts index 4c42919839..9abff1f58d 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.spec.ts @@ -1,44 +1,25 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MyDraftComponent } from './my-draft.component' -import { Component, importProvidersFrom } from '@angular/core' -import { TranslateModule } from '@ngx-translate/core' -import { RecordsListComponent } from '../records-list.component' -import { of } from 'rxjs' -import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' - -@Component({ - selector: 'md-editor-records-list', - template: '', - standalone: true, -}) -export class MockRecordsListComponent {} - -class RecordsRepositoryMock { - getAllDrafts = jest.fn().mockReturnValue(of(DATASET_RECORDS)) -} +import { TranslateModule } from '@ngx-translate/core' +import { cold, hot } from 'jasmine-marbles' +import { MockBuilder, MockProvider } from 'ng-mocks' +import { MyDraftComponent } from './my-draft.component' describe('MyDraftComponent', () => { let component: MyDraftComponent let fixture: ComponentFixture + let recordsRepository: RecordsRepositoryInterface + + beforeEach(() => { + return MockBuilder(MyDraftComponent) + }) beforeEach(() => { TestBed.configureTestingModule({ - providers: [ - importProvidersFrom(TranslateModule.forRoot()), - { - provide: RecordsRepositoryInterface, - useClass: RecordsRepositoryMock, - }, - ], - }).overrideComponent(MyDraftComponent, { - remove: { - imports: [RecordsListComponent], - }, - add: { - imports: [MockRecordsListComponent], - }, + imports: [TranslateModule.forRoot()], + providers: [MockProvider(RecordsRepositoryInterface)], }) + recordsRepository = TestBed.inject(RecordsRepositoryInterface) fixture = TestBed.createComponent(MyDraftComponent) component = fixture.componentInstance fixture.detectChanges() @@ -48,9 +29,19 @@ describe('MyDraftComponent', () => { expect(component).toBeTruthy() }) - it('gets all drafts on init', () => { - expect( - TestBed.inject(RecordsRepositoryInterface).getAllDrafts - ).toHaveBeenCalled() + it('should emit records$ immediately and then on drafts change', () => { + // Mock the source observable that records$ depends on + recordsRepository.draftsChanged$ = hot('--a-|', { + a: void 0, + }) + recordsRepository.getAllDrafts = jest + .fn() + .mockReturnValue(hot('-ab-|', { a: [], b: [{}] })) + + // Define the expected marble diagram + const expected = cold('abc-|', { a: [], b: [], c: [{}] }) + + // Assert that records$ behaves as expected + expect(component.records$).toBeObservable(expected) }) }) diff --git a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts index cd9d312037..be61640ea4 100644 --- a/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts +++ b/apps/metadata-editor/src/app/records/my-draft/my-draft.component.ts @@ -1,17 +1,17 @@ -import { Component } from '@angular/core' import { CommonModule } from '@angular/common' -import { TranslateModule } from '@ngx-translate/core' -import { RecordsListComponent } from '../records-list.component' -import { ResultsTableContainerComponent } from '@geonetwork-ui/feature/search' -import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { Component } from '@angular/core' import { MatIconModule } from '@angular/material/icon' -import { RecordsCountComponent } from '../records-count/records-count.component' -import { UiElementsModule } from '@geonetwork-ui/ui/elements' -import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { Router } from '@angular/router' +import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { ResultsTableContainerComponent } from '@geonetwork-ui/feature/search' +import { UiElementsModule } from '@geonetwork-ui/ui/elements' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' import { ResultsTableComponent } from '@geonetwork-ui/ui/search' -import { startWith } from 'rxjs' +import { TranslateModule } from '@ngx-translate/core' +import { startWith, switchMap } from 'rxjs' +import { RecordsCountComponent } from '../records-count/records-count.component' +import { RecordsListComponent } from '../records-list.component' @Component({ selector: 'md-editor-my-my-draft', @@ -31,8 +31,15 @@ import { startWith } from 'rxjs' ], }) export class MyDraftComponent { - records$ = this.recordsRepository.getAllDrafts().pipe(startWith([])) + records$ = this.recordsRepository.draftsChanged$.pipe( + startWith(void 0), + switchMap(() => this.recordsRepository.getAllDrafts()), + startWith([]) + ) hasDraft = () => true + canDuplicate = (): boolean => false + canDelete = (record: CatalogRecord): boolean => + this.recordsRepository.isRecordNotYetSaved(record.uniqueIdentifier) constructor( private router: Router, @@ -42,4 +49,8 @@ export class MyDraftComponent { editRecord(record: CatalogRecord) { this.router.navigate(['/edit', record.uniqueIdentifier]) } + + deleteDraft(record: CatalogRecord) { + this.recordsRepository.clearRecordDraft(record.uniqueIdentifier) + } } diff --git a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html index d07c2c50ef..becee4f003 100644 --- a/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html +++ b/apps/metadata-editor/src/app/records/search-records/search-records-list.component.html @@ -31,7 +31,11 @@

dashboard.results.listResources
- + edit_document dashboard.createRecord diff --git a/apps/metadata-editor/src/test-setup.ts b/apps/metadata-editor/src/test-setup.ts index 1261469e19..37ff1f2a88 100644 --- a/apps/metadata-editor/src/test-setup.ts +++ b/apps/metadata-editor/src/test-setup.ts @@ -1,11 +1,23 @@ import 'jest-preset-angular/setup-jest' import '../../../jest.setup' +import { CommonModule } from '@angular/common' import { getTestBed } from '@angular/core/testing' +import { MatDialogModule } from '@angular/material/dialog' +import { MatIconModule } from '@angular/material/icon' +import { BrowserModule } from '@angular/platform-browser' import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing' +import { RouterModule } from '@angular/router' +import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { SearchFacade } from '@geonetwork-ui/feature/search' +import { TranslateModule } from '@ngx-translate/core' +import { ngMocks } from 'ng-mocks' +import { BehaviorSubject, EMPTY } from 'rxjs' getTestBed().resetTestEnvironment() getTestBed().initTestEnvironment( @@ -13,3 +25,29 @@ getTestBed().initTestEnvironment( platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: false } } ) + +// ng-mocks global configuration +ngMocks.autoSpy('jest') + +ngMocks.globalKeep(CommonModule, true) +ngMocks.globalKeep(BrowserModule, true) +ngMocks.globalKeep(RouterModule, true) +ngMocks.globalKeep(TranslateModule, true) +ngMocks.globalKeep(MatIconModule, true) + +ngMocks.defaultMock(AvatarServiceInterface, () => ({ + getPlaceholder: jest.fn(), +})) + +ngMocks.defaultMock(PlatformServiceInterface, () => ({ + getMe: jest.fn(() => EMPTY), +})) + +ngMocks.defaultMock(RecordsRepositoryInterface, () => ({ + deleteRecord: jest.fn(() => EMPTY), + draftsChanged$: EMPTY, +})) + +ngMocks.defaultMock(SearchFacade, () => ({ + results$: new BehaviorSubject([]), +})) diff --git a/docs/guide/best-practices.md b/docs/guide/code-guide.md similarity index 90% rename from docs/guide/best-practices.md rename to docs/guide/code-guide.md index 1c792ce806..87e92bd800 100644 --- a/docs/guide/best-practices.md +++ b/docs/guide/code-guide.md @@ -2,7 +2,7 @@ outline: deep --- -# Best practices +# Code guide ## Standalone components @@ -13,11 +13,24 @@ Standalone components have the following differences with legacy "non-standalone - Standalone components are _not_ declared in Angular Modules; instead, they act as their own module, declaring their own dependencies and providers - Standalone components can import either Angular modules or other standalone components +Existing components are migrated progressively to become standalone as continuous improvement. +All new components must be created as standalone. + ## Testing ### Unit tests -#### Mocking services +#### ng-mocks + +Unit tests are meant to be shallow. We should only test the behaviour of a component or a service by controlling the inputs and dependencies, and checking the outputs and results. + +Angular provides many tools to clearly split the responsabilities between each components and services. The testing library `ng-mocks` relies on those tools, to provide an easy way to mock dependencies. + +See commit `01dfc84d5e127bd426238dd00395faa1697f0eaa` for a few examples. + +Unit tests are migrated progressively to use `ng-mocks`. + +#### Mocking services (legacy) One of the advantages of the Angular [dependency injection](https://angular.io/guide/dependency-injection-overview) system is the ability to use mock classes when testing a component or services. diff --git a/libs/api/metadata-converter/src/index.ts b/libs/api/metadata-converter/src/index.ts index a5e5f3d06e..169d8dfcb6 100644 --- a/libs/api/metadata-converter/src/index.ts +++ b/libs/api/metadata-converter/src/index.ts @@ -1,4 +1,3 @@ -export * from './lib/base.converter' export * from './lib/iso19139' export * from './lib/iso19115-3' export * from './lib/find-converter' diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index d16e72c14d..a31688b218 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -9,6 +9,7 @@ import { from, Observable, of, + Subject, switchMap, throwError, } from 'rxjs' @@ -24,7 +25,6 @@ import { } from '@geonetwork-ui/common/domain/model/search' import { catchError, map, tap } from 'rxjs/operators' import { - BaseConverter, findConverterForDocument, Gn4Converter, Gn4SearchResults, @@ -33,8 +33,13 @@ import { import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { HttpErrorResponse } from '@angular/common/http' +const TEMPORARY_ID_PREFIX = 'TEMP-ID-' + @Injectable() export class Gn4Repository implements RecordsRepositoryInterface { + _draftsChanged = new Subject() + draftsChanged$ = this._draftsChanged.asObservable() + constructor( private gn4SearchApi: SearchApiService, private gn4SearchHelper: ElasticsearchService, @@ -238,13 +243,14 @@ export class Gn4Repository implements RecordsRepositoryInterface { switchMap(async (recordAsXml) => { const converter = findConverterForDocument(recordAsXml) const record = await converter.readRecord(recordAsXml) - record.uniqueIdentifier = `TEMP-ID-${Date.now()}` + record.uniqueIdentifier = `${TEMPORARY_ID_PREFIX}${Date.now()}` record.title = `${record.title} (Copy)` const xml = await converter.writeRecord(record, recordAsXml) window.localStorage.setItem( this.getLocalStorageKeyForRecord(record.uniqueIdentifier), xml ) + this._draftsChanged.next() return [record, xml, false] as [CatalogRecord, string, false] }) ) @@ -295,17 +301,26 @@ export class Gn4Repository implements RecordsRepositoryInterface { ) } + deleteRecord(uniqueIdentifier: string): Observable { + return this.gn4RecordsApi.deleteRecord(uniqueIdentifier) + } + + generateTemporaryId(): string { + return `${TEMPORARY_ID_PREFIX}${Date.now()}` + } + saveRecordAsDraft( record: CatalogRecord, referenceRecordSource?: string ): Observable { return this.serializeRecordToXml(record, referenceRecordSource).pipe( - tap((recordXml) => + tap((recordXml) => { window.localStorage.setItem( this.getLocalStorageKeyForRecord(record.uniqueIdentifier), recordXml ) - ) + this._draftsChanged.next() + }) ) } @@ -313,6 +328,7 @@ export class Gn4Repository implements RecordsRepositoryInterface { window.localStorage.removeItem( this.getLocalStorageKeyForRecord(uniqueIdentifier) ) + this._draftsChanged.next() } recordHasDraft(uniqueIdentifier: string): boolean { @@ -323,6 +339,10 @@ export class Gn4Repository implements RecordsRepositoryInterface { ) } + isRecordNotYetSaved(uniqueIdentifier: string): boolean { + return uniqueIdentifier.startsWith(TEMPORARY_ID_PREFIX) + } + // generated by copilot getAllDrafts(): Observable { const items = { ...window.localStorage } diff --git a/libs/common/domain/src/lib/repository/records-repository.interface.ts b/libs/common/domain/src/lib/repository/records-repository.interface.ts index 039eac3812..e254465afe 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -52,6 +52,14 @@ export abstract class RecordsRepositoryInterface { referenceRecordSource?: string ): Observable + /** + * @param uniqueIdentifier + * @returns Observable Returns when record is deleted + */ + abstract deleteRecord(uniqueIdentifier: string): Observable + + abstract generateTemporaryId(): string + /** * @param record * @param referenceRecordSource @@ -64,7 +72,9 @@ export abstract class RecordsRepositoryInterface { abstract clearRecordDraft(uniqueIdentifier: string): void abstract recordHasDraft(uniqueIdentifier: string): boolean + abstract isRecordNotYetSaved(uniqueIdentifier: string): boolean /** will return all pending drafts, both published and not published */ abstract getAllDrafts(): Observable + abstract draftsChanged$: Observable } diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.html b/libs/feature/search/src/lib/results-table/results-table-container.component.html index b0aeb07004..a2be70d0af 100644 --- a/libs/feature/search/src/lib/results-table/results-table-container.component.html +++ b/libs/feature/search/src/lib/results-table/results-table-container.component.html @@ -1,10 +1,11 @@ diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.spec.ts b/libs/feature/search/src/lib/results-table/results-table-container.component.spec.ts index eb843061a3..0ececc40ce 100644 --- a/libs/feature/search/src/lib/results-table/results-table-container.component.spec.ts +++ b/libs/feature/search/src/lib/results-table/results-table-container.component.spec.ts @@ -1,71 +1,49 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { By } from '@angular/platform-browser' -import { NoopAnimationsModule } from '@angular/platform-browser/animations' import { SelectionService } from '@geonetwork-ui/api/repository' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures' +import { NotificationsService } from '@geonetwork-ui/feature/notifications' import { TranslateModule } from '@ngx-translate/core' -import { BehaviorSubject } from 'rxjs' +import { MockBuilder, MockProviders } from 'ng-mocks' +import { of, Subject, throwError } from 'rxjs' import { SearchFacade } from '../state/search.facade' import { SearchService } from '../utils/service/search.service' import { ResultsTableContainerComponent } from './results-table-container.component' -class SearchFacadeMock { - results$ = new BehaviorSubject(DATASET_RECORDS) - resultsHits$ = new BehaviorSubject(1000) - setConfigRequestFields = jest.fn(() => this) - setSortBy = jest.fn(() => this) - sortBy$ = new BehaviorSubject(['asc', 'updateDate']) -} -class SearchServiceMock { - setPage = jest.fn() - setSortBy = jest.fn() -} -class SelectionServiceMock { - selectRecords = jest.fn() - deselectRecords = jest.fn() - clearSelection = jest.fn() - selectedRecordsIdentifiers$ = new BehaviorSubject([]) -} -class RecordsRepositoryMock { - recordHasDraft = jest.fn(() => false) -} - describe('ResultsTableContainerComponent', () => { let component: ResultsTableContainerComponent - let searchFacade: SearchFacadeMock - let searchService: SearchServiceMock - let selectionService: SelectionServiceMock + let searchFacade: SearchFacade + let searchService: SearchService + let selectionService: SelectionService + let recordsRepository: RecordsRepositoryInterface + let notificationsService: NotificationsService let fixture: ComponentFixture + beforeEach(() => { + return MockBuilder(ResultsTableContainerComponent) + }) + beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NoopAnimationsModule], + imports: [TranslateModule.forRoot()], providers: [ - { - provide: SearchFacade, - useClass: SearchFacadeMock, - }, - { - provide: SearchService, - useClass: SearchServiceMock, - }, - { - provide: SelectionService, - useClass: SelectionServiceMock, - }, - { - provide: RecordsRepositoryInterface, - useClass: RecordsRepositoryMock, - }, + MockProviders( + SearchFacade, + SearchService, + SelectionService, + RecordsRepositoryInterface, + NotificationsService + ), ], }).compileComponents() fixture = TestBed.createComponent(ResultsTableContainerComponent) - searchFacade = TestBed.inject(SearchFacade) as any - searchService = TestBed.inject(SearchService) as any - selectionService = TestBed.inject(SelectionService) as any + searchFacade = TestBed.inject(SearchFacade) + searchService = TestBed.inject(SearchService) + selectionService = TestBed.inject(SelectionService) + recordsRepository = TestBed.inject(RecordsRepositoryInterface) + notificationsService = TestBed.inject(NotificationsService) component = fixture.componentInstance fixture.detectChanges() }) @@ -85,7 +63,7 @@ describe('ResultsTableContainerComponent', () => { describe('selection', () => { beforeEach(() => { - searchFacade.results$.next([ + ;(searchFacade.results$ as Subject).next([ { uniqueIdentifier: '1', }, @@ -116,10 +94,7 @@ describe('ResultsTableContainerComponent', () => { }) it('emits a recordClick event', () => { - const tableRow = fixture.debugElement.queryAll( - By.css('.table-row-cell') - )[1].nativeElement as HTMLDivElement - tableRow.parentElement.click() + component.handleRecordClick(DATASET_RECORDS[0]) expect(clickedRecord).toEqual(DATASET_RECORDS[0]) }) }) @@ -133,19 +108,38 @@ describe('ResultsTableContainerComponent', () => { }) it('emits a duplicateRecord event', () => { - const menuButton = fixture.debugElement.query( - By.css('[data-test="record-menu-button"]') - ).nativeElement as HTMLButtonElement - menuButton.click() - fixture.detectChanges() - const duplicateButton = fixture.debugElement.query( - By.css('[data-test="record-menu-duplicate-button"]') - ).nativeElement as HTMLButtonElement - duplicateButton.click() + component.handleDuplicateRecord(DATASET_RECORDS[0]) expect(recordToBeDuplicated).toEqual(DATASET_RECORDS[0]) }) }) + describe('deleting a dataset', () => { + describe('delete error', () => { + it('shows notification', () => { + recordsRepository.deleteRecord = jest.fn(() => + throwError(() => 'oopsie') + ) + component.handleDeleteRecord(DATASET_RECORDS[0]) + expect(notificationsService.showNotification).toHaveBeenCalledWith({ + type: 'error', + title: 'editor.record.deleteError.title', + text: 'editor.record.deleteError.body oopsie', + closeMessage: 'editor.record.deleteError.closeMessage', + }) + }) + }) + + describe('delete success', () => { + it('shows notification', () => { + recordsRepository.deleteRecord = jest.fn(() => of(void 0)) + component.handleDeleteRecord(DATASET_RECORDS[0]) + expect(recordsRepository.deleteRecord).toHaveBeenCalled() + expect(recordsRepository.clearRecordDraft).toHaveBeenCalled() + expect(searchFacade.requestNewResults).toHaveBeenCalled() + }) + }) + }) + describe('#hasDraft', () => { it('calls the repository service', () => { const record = DATASET_RECORDS[0] diff --git a/libs/feature/search/src/lib/results-table/results-table-container.component.ts b/libs/feature/search/src/lib/results-table/results-table-container.component.ts index 6b1cf8ebce..7e15aed314 100644 --- a/libs/feature/search/src/lib/results-table/results-table-container.component.ts +++ b/libs/feature/search/src/lib/results-table/results-table-container.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Output } from '@angular/core' +import { Component, EventEmitter, OnDestroy, Output } from '@angular/core' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' import { SearchFacade } from '../state/search.facade' import { SelectionService } from '@geonetwork-ui/api/repository' @@ -6,6 +6,9 @@ import { SearchService } from '../utils/service/search.service' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { ResultsTableComponent } from '@geonetwork-ui/ui/search' import { CommonModule } from '@angular/common' +import { Subscription } from 'rxjs' +import { NotificationsService } from '@geonetwork-ui/feature/notifications' +import { TranslateService } from '@ngx-translate/core' @Component({ selector: 'gn-ui-results-table-container', @@ -14,10 +17,12 @@ import { CommonModule } from '@angular/common' standalone: true, imports: [CommonModule, ResultsTableComponent], }) -export class ResultsTableContainerComponent { +export class ResultsTableContainerComponent implements OnDestroy { @Output() recordClick = new EventEmitter() @Output() duplicateRecord = new EventEmitter() + subscription = new Subscription() + records$ = this.searchFacade.results$ selectedRecords$ = this.selectionService.selectedRecordsIdentifiers$ sortBy$ = this.searchFacade.sortBy$ @@ -29,7 +34,9 @@ export class ResultsTableContainerComponent { private searchFacade: SearchFacade, private searchService: SearchService, private selectionService: SelectionService, - private recordsRepository: RecordsRepositoryInterface + private recordsRepository: RecordsRepositoryInterface, + private notificationsService: NotificationsService, + private translateService: TranslateService ) {} handleRecordClick(item: unknown) { @@ -40,6 +47,32 @@ export class ResultsTableContainerComponent { this.duplicateRecord.emit(item as CatalogRecord) } + async handleDeleteRecord(item: unknown) { + const uniqueIdentifier = (item as CatalogRecord).uniqueIdentifier + this.subscription.add( + this.recordsRepository.deleteRecord(uniqueIdentifier).subscribe({ + next: () => { + this.recordsRepository.clearRecordDraft(uniqueIdentifier) + this.searchFacade.requestNewResults() + }, + error: (error) => { + this.notificationsService.showNotification({ + type: 'error', + title: this.translateService.instant( + 'editor.record.deleteError.title' + ), + text: `${this.translateService.instant( + 'editor.record.deleteError.body' + )} ${error}`, + closeMessage: this.translateService.instant( + 'editor.record.deleteError.closeMessage' + ), + }) + }, + }) + ) + } + handleSortByChange(col: string, order: 'asc' | 'desc') { this.searchService.setSortBy([order, col]) } @@ -51,4 +84,8 @@ export class ResultsTableContainerComponent { this.selectionService.selectRecords(records) } } + + ngOnDestroy() { + this.subscription.unsubscribe() + } } diff --git a/libs/feature/search/src/lib/state/search.facade.ts b/libs/feature/search/src/lib/state/search.facade.ts index 1f9388a2b1..44c9c34e1e 100644 --- a/libs/feature/search/src/lib/state/search.facade.ts +++ b/libs/feature/search/src/lib/state/search.facade.ts @@ -8,6 +8,7 @@ import { Paginate, RequestMoreOnAggregation, RequestMoreResults, + RequestNewResults, SetConfigAggregations, SetConfigFilters, SetConfigRequestFields, @@ -151,6 +152,11 @@ export class SearchFacade { return this } + requestNewResults(): SearchFacade { + this.store.dispatch(new RequestNewResults(this.searchId)) + return this + } + requestMoreOnAggregation(key: string, increment: number): SearchFacade { this.store.dispatch( new RequestMoreOnAggregation(key, increment, this.searchId) diff --git a/libs/feature/search/src/test-setup.ts b/libs/feature/search/src/test-setup.ts index 70e41af1c8..027b08590d 100644 --- a/libs/feature/search/src/test-setup.ts +++ b/libs/feature/search/src/test-setup.ts @@ -1,11 +1,21 @@ import 'jest-preset-angular/setup-jest' import '../../../../jest.setup' +import { CommonModule } from '@angular/common' import { getTestBed } from '@angular/core/testing' +import { MatIconModule } from '@angular/material/icon' +import { BrowserModule } from '@angular/platform-browser' import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing' +import { RouterModule } from '@angular/router' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { TranslateModule } from '@ngx-translate/core' +import { ngMocks } from 'ng-mocks' +import { BehaviorSubject } from 'rxjs' +import { SearchFacade } from './lib/state/search.facade' +import { SearchService } from './lib/utils/service/search.service' getTestBed().resetTestEnvironment() getTestBed().initTestEnvironment( @@ -13,3 +23,25 @@ getTestBed().initTestEnvironment( platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: false } } ) + +// ng-mocks global configuration +ngMocks.autoSpy('jest') + +ngMocks.globalKeep(CommonModule, true) +ngMocks.globalKeep(BrowserModule, true) +ngMocks.globalKeep(RouterModule, true) +ngMocks.globalKeep(TranslateModule, true) +ngMocks.globalKeep(MatIconModule, true) + +ngMocks.defaultMock(RecordsRepositoryInterface, () => ({ + clearRecordDraft: jest.fn(), + recordHasDraft: jest.fn(), +})) + +ngMocks.defaultMock(SearchFacade, () => ({ + results$: new BehaviorSubject([]), +})) + +ngMocks.defaultMock(SearchService, () => ({ + setSortBy: jest.fn(), +})) diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 578e08b8ed..327b9d5c81 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -1,5 +1,6 @@ export * from './lib/api-card/api-card.component' export * from './lib/avatar/avatar.component' +export * from './lib/confirmation-dialog/confirmation-dialog.component' export * from './lib/content-ghost/content-ghost.component' export * from './lib/download-item/download-item.component' export * from './lib/downloads-list/downloads-list.component' diff --git a/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.css b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.html b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.html new file mode 100644 index 0000000000..684437dc99 --- /dev/null +++ b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.html @@ -0,0 +1,12 @@ +

{{ data.title }}

+
{{ data.message }}
+
+ {{ data.cancelText }} + {{ data.confirmText }} +
diff --git a/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.spec.ts b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..0bc6508618 --- /dev/null +++ b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog' +import { MockBuilder, MockProvider } from 'ng-mocks' +import { ConfirmationDialogComponent } from './confirmation-dialog.component' + +describe('ConfirmationDialogComponent', () => { + let component: ConfirmationDialogComponent + let fixture: ComponentFixture + + beforeEach(() => { + return MockBuilder(ConfirmationDialogComponent) + }) + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfirmationDialogComponent], + providers: [ + MockProvider(MatDialogRef), + MockProvider(MAT_DIALOG_DATA, { + title: 'title', + message: 'message', + confirmText: 'confirm', + cancelText: 'cancel', + }), + ], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(ConfirmationDialogComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + fixture.detectChanges() + expect(component).toBeTruthy() + }) +}) diff --git a/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.stories.ts b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.stories.ts new file mode 100644 index 0000000000..5cda484c54 --- /dev/null +++ b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.stories.ts @@ -0,0 +1,59 @@ +import { Component, Input } from '@angular/core' +import { MatDialog } from '@angular/material/dialog' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { ConfirmationDialogComponent } from './confirmation-dialog.component' + +@Component({ + selector: 'gn-ui-launcher', + template: ` + Open + Waiting for a result + Confirmed + Canceled + `, +}) +class LaunchDialogComponent { + @Input() title = '' + @Input() message = '' + @Input() confirmText = '' + @Input() cancelText = '' + + confirmed: boolean + + constructor(private _dialog: MatDialog) {} + + launch(): void { + const dialogRef = this._dialog.open(ConfirmationDialogComponent, { + data: { + title: this.title, + message: this.message, + confirmText: this.confirmText, + cancelText: this.cancelText, + }, + }) + + dialogRef.afterClosed().subscribe((confirmed) => { + this.confirmed = confirmed + }) + } +} + +export default { + title: 'Elements/ConfirmationDialogComponent', + component: LaunchDialogComponent, + decorators: [ + moduleMetadata({ + imports: [ButtonComponent, ConfirmationDialogComponent], + }), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + title: 'Some title', + message: 'Some message to confirm', + confirmText: 'OK', + cancelText: 'KO', + }, +} diff --git a/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.ts b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.ts new file mode 100644 index 0000000000..8eac932175 --- /dev/null +++ b/libs/ui/elements/src/lib/confirmation-dialog/confirmation-dialog.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog' +import { ButtonComponent } from '@geonetwork-ui/ui/inputs' + +export interface ConfirmationDialogData { + title: string + message: string + confirmText: string + cancelText: string +} + +@Component({ + selector: 'gn-ui-confirmation-dialog', + templateUrl: './confirmation-dialog.component.html', + styleUrls: ['./confirmation-dialog.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [MatDialogModule, ButtonComponent], +}) +export class ConfirmationDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData + ) {} + + onConfirm() { + this.dialogRef.close(true) + } + + onCancel() { + this.dialogRef.close(false) + } +} diff --git a/libs/ui/elements/src/test-setup.ts b/libs/ui/elements/src/test-setup.ts index 19b403b6c7..73bc92687d 100644 --- a/libs/ui/elements/src/test-setup.ts +++ b/libs/ui/elements/src/test-setup.ts @@ -6,6 +6,12 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing' +import { ngMocks } from 'ng-mocks' +import { CommonModule } from '@angular/common' +import { BrowserModule } from '@angular/platform-browser' +import { TranslateModule } from '@ngx-translate/core' +import { MatIconModule } from '@angular/material/icon' +import { MatTooltipModule } from '@angular/material/tooltip' getTestBed().resetTestEnvironment() getTestBed().initTestEnvironment( @@ -20,3 +26,12 @@ class ResizeObserverMock { } ;(window as any).ResizeObserver = ResizeObserverMock + +// ng-mocks global configuration +ngMocks.autoSpy('jest') + +ngMocks.globalKeep(CommonModule, true) +ngMocks.globalKeep(BrowserModule, true) +ngMocks.globalKeep(TranslateModule, true) +ngMocks.globalKeep(MatIconModule, true) +ngMocks.globalKeep(MatTooltipModule, true) diff --git a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html index 81c2e88dec..143497cfbb 100644 --- a/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html +++ b/libs/ui/layout/src/lib/interactive-table/interactive-table.component.html @@ -30,6 +30,7 @@ class="contents text-gray-900 cursor-pointer group" *ngFor="let item of items" (click)="handleRowClick(item)" + data-cy="table-row" >