diff --git a/Jenkinsfile b/Jenkinsfile index ebf30e8097..2a8d7df9d6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -48,7 +48,7 @@ spec: topologyKey: kubernetes.io/hostname containers: - name: build - image: docker.gentics.com/cms-oss/build-container:6.4 + image: docker.gentics.com/cms-oss/build-container:6.6 resources: requests: cpu: '0' @@ -103,8 +103,6 @@ spec: options { withCredentials([usernamePassword(credentialsId: 'docker.gentics.com', usernameVariable: 'repoUsername', passwordVariable: 'repoPassword')]) - gitLabConnection('git.gentics.com') - gitlabBuilds(builds: ['Jenkins build']) timestamps() timeout(time: 4, unit: 'HOURS') ansiColor('xterm') @@ -130,19 +128,88 @@ spec: } } - stage("Build, Deploy") { + // Build the UI in preparation to be used in the CMS + stage("Build UI") { when { expression { return env.BUILD_SKIPPED != "true" } } + + environment { + // Disable colors/special characters, if projects do properly check it + TERM="dumb" + // Disable node based color libraries + FORCE_COLOR="0" + } + steps { - updateGitlabCommitStatus name: 'Jenkins build', state: "running" + script { + dir(path: 'cms-ui') { + // Get the correct version + version = params.forceVersion + + if ( + // If the version isn't semver, we can't set it, as NX just explodes + !(version ==~ /[\d]+\.[\d]+\.[\d]+(?:\.[a-zA-Z0-9-]+)?/) + // Or for release-builds, we can't override it + || (!version && params.runReleaseBuild) + ) { + version = MavenHelper.getVersion() + } + + // Install the dependencies + sh "npm ci --no-audit --no-fund" + + if (version && (version ==~ /[\d]+\.[\d]+\.[\d]+(?:\.[a-zA-Z0-9-]+)?/)) { + // Setup the packages to use the correct version for publishing + sh "npm run nx -- release version $version" + } + + // Build everything + sh "npm run many -- --target=build --configuration=ci --output-style=static" + + // Run the tests + if (params.runTests) { + sh "npm run many -- --targets=test,component-test --configuration=ci --output-style=static" + } + + // Create the report-files + sh "mkdir target" + sh "npm run report:list" + sh "npm run report:outdated" + } + } + } + + post { + always { + script { + // Ignore missing test results if we only run one test + boolean allowEmptyResults = (params.singleTest ? true : false) + if (params.runTests) { + junit testResults: "cms-ui/.reports/**/VITEST-report.xml", allowEmptyResults: allowEmptyResults + junit testResults: "cms-ui/.reports/**/KARMA-report.xml", allowEmptyResults: allowEmptyResults + junit testResults: "cms-ui/.reports/**/CYPRESS-component-report.xml", allowEmptyResults: allowEmptyResults + } + } + } + } + } + + stage("Build, Deploy") { + when { + expression { + return env.BUILD_SKIPPED != "true" + } + } + + steps { script { def mvnGoal = "package" def mvnProjects = "" - def mvnArguments = "-Dnodejs.npm.bin=/opt/node/bin/npm " + def mvnArguments = " " version = params.forceVersion branchName = GitHelper.fetchCurrentBranchName() @@ -192,35 +259,16 @@ spec: if (params.singleTest) { mvnGoal = "test" mvnProjects = " -am -pl 'cms-core,cms-oss-server'" - mvnArguments += " -Dui.skip.build -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest=" + params.singleTest - } - - // Check if triggered by a Gitlab merge request - if (env.gitlabTargetBranch) { - runJUnitTests = GitHelper.checkForChangesInPaths("origin/" + env.gitlabTargetBranch,(String[])[ - "base-api/", - "base-lib/", - "cms-api/", - "cms-cache/", - "cms-cache/", - "cms-core/", - "cms-oss-server/", - "cms-restapi/" - ]) - - if (!runJUnitTests) { - mvnArguments += " -Dskip.unit.tests" - } + mvnArguments += " -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false -Dtest=" + params.singleTest } } else { - mvnArguments += " -DskipTests=true -Dskip.unit.tests=true -Dui.skip.test=true" + mvnArguments += " -DskipTests=true -Dskip.unit.tests=true" runJUnitTests = false } // when deploying for the test systems, we do not build the changelog or doc if (params.deployTesting) { mvnProjects = " -am -pl '!cms-oss-changelog,!cms-oss-doc'" - mvnArguments += " -Dui.skip.publish" } // Update chrome to the latest version @@ -261,7 +309,7 @@ spec: // Install mvnGoal = "install" mvnProjects = " -am -pl 'cms-oss-bom,cms-core,cms-oss-server,cms-ui'" - mvnArguments = " -DskipTests=true -Dskip.unit.tests -Dui.skip.test=true -Dnodejs.npm.bin=/opt/node/bin/npm -Dui.skip.publish" + mvnArguments = " -DskipTests=true -Dskip.unit.tests " } } @@ -271,11 +319,6 @@ spec: sh "find . -maxdepth 3 -type f -name 'pom.xml' -print0 | xargs -0 sed -i -r 's/<\\?m2e[[:blank:]]+[[:alnum:]]+[[:blank:]]*\\?>//g'" } - // Add private repository credentials and scopes - withCredentials([string(credentialsId: 'nexus-npm', variable: 'NPM_TOKEN')]) { - sh "echo //repo.gentics.com/repository/npm-products/:_auth=${env.NPM_TOKEN} >> ~/.npmrc" - } - // Set custom mesh version (if configured) if (params.meshVersion?.trim() != "") { echo "Setting Mesh version to " + params.meshVersion.trim() @@ -292,7 +335,7 @@ spec: authDockerRegistry("docker.gentics.com", "docker.gentics.com") authDockerRegistry("docker.gentics.com", "push.docker.gentics.com") withEnv(["TESTMANAGER_HOSTNAME=" + testDbManagerHost, "TESTMANAGER_PORT=" + testDbManagerPort, "TESTCONTAINERS_RYUK_DISABLED=true"]) { - sh "mvn -B -Dstyle.color=always -U -Dskip.integration.tests -Dui.skip.integrationTest=true " + + sh "mvn -B -Dstyle.color=always -U -Dskip.integration.tests " + " -fae -Dmaven.test.failure.ignore=true " + mvnArguments + mvnProjects + " clean " + mvnGoal } @@ -328,10 +371,6 @@ spec: junit testResults: "cms-core/target/surefire-reports/TEST-*.xml", allowEmptyResults: allowEmptyResults junit testResults: "cms-oss-server/target/surefire-reports/TEST-*.xml", allowEmptyResults: allowEmptyResults } - - junit testResults: "cms-ui/.reports/**/JEST-report.xml", allowEmptyResults: allowEmptyResults - junit testResults: "cms-ui/.reports/**/KARMA-report.xml", allowEmptyResults: allowEmptyResults - junit testResults: "cms-ui/.reports/**/CYPRESS-component-report.xml", allowEmptyResults: allowEmptyResults } } } @@ -343,7 +382,7 @@ spec: expression { // Build the docker image only if the parameter runDockerBuild is enabled and return env.BUILD_SKIPPED != "true" && params.runDockerBuild && - (!env.gitlabTargetBranch || qaDeployBranchList.contains(branchName)) + (qaDeployBranchList.contains(branchName)) } } @@ -377,7 +416,7 @@ spec: expression { // Build the docker image only if the parameter runDockerBuild is enabled and return env.BUILD_SKIPPED != "true" && params.runDockerBuild && - (!env.gitlabTargetBranch || qaDeployBranchList.contains(branchName)) + (qaDeployBranchList.contains(branchName)) } } @@ -484,10 +523,22 @@ spec: } } + environment { + // Disable colors/special characters, if projects do properly check it + TERM="dumb" + // Disable node based color libraries + FORCE_COLOR="0" + } + steps { script { dir(path: 'cms-ui') { - // Publish the pacakges to npm repository + // Add private repository credentials and scopes + withCredentials([string(credentialsId: 'nexus-npm', variable: 'NPM_TOKEN')]) { + sh "echo //repo.gentics.com/repository/npm-products/:_auth=${env.NPM_TOKEN} >> ~/.npmrc" + } + + // Publish the packages to npm repository sh "npm run nx -- release publish --output-style=static" } } diff --git a/build/Dockerfile b/build/Dockerfile index 4f90f9b2e4..fdb4d4b1f9 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -5,10 +5,10 @@ LABEL maintainer="Peter Nyari " ARG GIT_USER_NAME="Maven Release Manager" ARG GIT_USER_EMAIL="entwicklung@gentics.com" ARG USER_NAME="jenkins" -ARG NODE_VERSION=22.15.0 +ARG NODE_VERSION=26.4.0 # Version makes it possible to force rebuild the image and bypass caching by changing this variable (eg.: when ca-certificates needs update) -ARG VERSION="20251201.0" +ARG VERSION="20260728.0" ARG DEBIAN_RELEASE="trixie" # Setup debian channels @@ -40,28 +40,15 @@ Suites: $(. /etc/os-release && echo "$DEBIAN_RELEASE") Components: stable Signed-By: /etc/apt/keyrings/docker.asc EOF -RUN apt update -RUN apt install --assume-yes docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - -# Google chrome -RUN wget -q -O /etc/apt/keyrings/linux_signing_key.pub https://dl-ssl.google.com/linux/linux_signing_key.pub -RUN tee /etc/apt/sources.list.d/google-chrome.sources > /dev/null < /etc/ssh/ssh_config \ && mv /usr/share/maven/conf/settings.xml /usr/share/maven/conf/settings-original.xml ADD build/settings.xml /usr/share/maven/conf/settings.xml -# Allow updating Google Chrome -RUN echo "jenkins ALL=NOPASSWD: /usr/bin/apt-get update" > /etc/sudoers.d/jenkins \ - && echo "jenkins ALL=NOPASSWD: /usr/bin/apt-get install --assume-yes --allow-unauthenticated google-chrome-beta" >> /etc/sudoers.d/jenkins - # Set the XDG cache to a directory which can be written to (In our CI, .cache isn't mounted/writable) -ENV XDG_CACHE_HOME "/tmp" +ENV XDG_CACHE_HOME="/tmp" # Disable mesa cache entirely -ENV MESA_SHADER_CACHE_DISABLE "true" +ENV MESA_SHADER_CACHE_DISABLE="true" # Switch user USER ${USER_NAME} # Workaround for Maven not outputting colors and silence download progress messages -ENV MAVEN_OPTS "-Djansi.passthrough=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -Xmx2g -XX:MaxMetaspaceSize=256m" +ENV MAVEN_OPTS="-Djansi.passthrough=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -Xmx2g -XX:MaxMetaspaceSize=256m" RUN mkdir -p ~/workspace \ && mkdir -p ~/.m2/repository # TestDB Manager -ENV TESTMANAGER_HOSTNAME "gcn-testdb-manager.gtx-dev.svc" -ENV TESTMANAGER_PORT "8080" +ENV TESTMANAGER_HOSTNAME="gcn-testdb-manager.gtx-dev.svc" +ENV TESTMANAGER_PORT="8080" # path to the docker.sock file to be mounted in the ryuk container (for testcontainers) -ENV TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE "/run/user/1000/docker.sock" -ENV JAVA_HOME "/usr/lib/jvm/java-17-amazon-corretto/" +ENV TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE="/run/user/1000/docker.sock" +ENV JAVA_HOME="/usr/lib/jvm/java-17-amazon-corretto/" EXPOSE 80 diff --git a/cms-ui/.gitignore b/cms-ui/.gitignore index e1e415c5e1..cc031a356e 100644 --- a/cms-ui/.gitignore +++ b/cms-ui/.gitignore @@ -13,6 +13,8 @@ /apps/ui-core-docs/compile-docs.js /apps/ui-core-docs/docs.output.json migrations.json +prepared_dependencies.json +outdated_dependencies.json # profiling files chrome-profiler-events.json @@ -31,3 +33,5 @@ report.*.json .angular test-output + +vitest.config.*.timestamp* diff --git a/cms-ui/.nvmrc b/cms-ui/.nvmrc index 9158a6fb3f..f7a56c9ec2 100644 --- a/cms-ui/.nvmrc +++ b/cms-ui/.nvmrc @@ -1 +1 @@ -v22.15.0 +v26.4.0 diff --git a/cms-ui/apps/admin-ui/cypress/tsconfig.json b/cms-ui/apps/admin-ui/cypress/tsconfig.json index 12f174438e..c36c03f3ff 100644 --- a/cms-ui/apps/admin-ui/cypress/tsconfig.json +++ b/cms-ui/apps/admin-ui/cypress/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "../../dist/out-tsc", "module": "commonjs", "types": ["cypress", "node"], - "sourceMap": false + "sourceMap": false, + "ignoreDeprecations": "6.0" }, "include": [ "**/*.ts", diff --git a/cms-ui/apps/admin-ui/e2e/templates.spec.ts b/cms-ui/apps/admin-ui/e2e/templates.spec.ts index 47ad1a6d66..4cb9f5460f 100644 --- a/cms-ui/apps/admin-ui/e2e/templates.spec.ts +++ b/cms-ui/apps/admin-ui/e2e/templates.spec.ts @@ -1,4 +1,4 @@ -import { Node, Template, TemplateResponse, TemplateSaveRequest } from '@gentics/cms-models'; +import { Node, Template, TemplateResponse, TemplateSaveRequest, NodeUrlMode, NodePageLanguageCode } from '@gentics/cms-models'; import { BASIC_TEMPLATE_ID, clickModalAction, @@ -15,6 +15,11 @@ import { selectTrableRow, TestSize, waitForResponseFrom, + IMPORT_ID, + IMPORT_TYPE, + IMPORT_TYPE_NODE, + LANGUAGE_DE, + NodeImportData } from '@gentics/e2e-utils'; import { expect, Locator, Response, test } from '@playwright/test'; import { AUTH } from './common'; @@ -114,6 +119,46 @@ test.describe('Templates Module', () => { let nodeTable: Locator; let nodeRow: Locator; + // todo + const EXAMPLE_NODE_ONE: NodeImportData = { + [IMPORT_TYPE]: IMPORT_TYPE_NODE, + [IMPORT_ID]: 'templateExampleNodeOne', + + node: { + name: 'Templates Example Node #1', + host: 'http://template01.localhost', + hostProperty: '', + publishDir: '', + binaryPublishDir: '', + pubDirSegment: true, + publishImageVariants: false, + publishFs: false, + publishFsPages: false, + publishFsFiles: false, + publishContentMap: false, + publishContentMapPages: false, + publishContentMapFiles: false, + publishContentMapFolders: false, + urlRenderWayPages: NodeUrlMode.AUTOMATIC, + urlRenderWayFiles: NodeUrlMode.AUTOMATIC, + omitPageExtension: false, + pageLanguageCode: NodePageLanguageCode.FILENAME, + meshPreviewUrlProperty: '', + }, + description: 'Test Node', + languages: [LANGUAGE_DE], + templates: [], + }; + + await IMPORTER.importData([ + EXAMPLE_NODE_ONE + ]); + + const tmp_node = IMPORTER.get(EXAMPLE_NODE_ONE); + + await IMPORTER.client.node.assignTemplate(tmp_node.id, testTemplate.id) + .send(); + await test.step('Unassign from node', async () => { const nodeLoad = waitForResponseFrom(page, 'GET', '/rest/node'); const tplNodesLoad = waitForResponseFrom(page, 'GET', `/rest/template/${testTemplate.id}/nodes`); diff --git a/cms-ui/apps/admin-ui/project.json b/cms-ui/apps/admin-ui/project.json index 9ae7a9beb8..85feea4e5e 100644 --- a/cms-ui/apps/admin-ui/project.json +++ b/cms-ui/apps/admin-ui/project.json @@ -4,11 +4,7 @@ "projectType": "application", "prefix": "gtx", "sourceRoot": "apps/admin-ui/src", - "tags": [ - "app", - "ui", - "angular" - ], + "tags": ["app", "ui", "angular", "test:karma"], "targets": { "build": { "executor": "@nx/angular:application", diff --git a/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.spec.ts b/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.spec.ts index 84f488f945..338e6f4f5a 100644 --- a/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.spec.ts +++ b/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.spec.ts @@ -121,6 +121,11 @@ const ROUTES: GcmsAdminUiRoute[] = [ path: SUBMODULE_BA, component: TestComponent, // Intentionally no breadcrumbs for MODULE_BA. + // For *whatever* reason, these data elements are inherited if not applied on the route + // manually. So set a dummy element here and it doesn't inherit the parent breadcrumb data. + data: { + breadcrumb: {} as any, + }, children: [ { path: SUBMODULE_BAA, @@ -365,7 +370,7 @@ describe('BreadcrumbsService', () => { .subscribe((links) => breadcrumbLinks = links); navigateByUrl(`/${MODULE_B}/${SUBMODULE_BA}/${SUBMODULE_BAA}`); - tick(); + tick(1_000); expect(breadcrumbLinks).toEqual([ EXPECTED_BREADCRUMBS[DASHBOARD], EXPECTED_BREADCRUMBS[MODULE_B], @@ -380,7 +385,7 @@ describe('BreadcrumbsService', () => { .subscribe((links) => breadcrumbLinks = links); navigateByUrl(`/${MODULE_B}/${SUBMODULE_BA}`); - tick(); + tick(1_000); expect(breadcrumbLinks).toEqual([ EXPECTED_BREADCRUMBS[DASHBOARD], EXPECTED_BREADCRUMBS[MODULE_B], diff --git a/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.ts b/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.ts index 753d72a1d4..03a84f0bff 100644 --- a/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.ts +++ b/cms-ui/apps/admin-ui/src/app/core/providers/breadcrumbs/breadcrumbs.service.ts @@ -6,13 +6,13 @@ import { ROUTE_SKIP_BREADCRUMB, RouteData, } from '@admin-ui/common'; -import { InitializableServiceBase } from '@admin-ui/shared/providers/initializable-service-base'; +import { InitializableServiceBase } from '../../../shared/providers/initializable-service-base/initializable-service.base'; import { SelectState } from '@admin-ui/state'; import { Injectable } from '@angular/core'; import { ActivatedRoute, NavigationEnd, PRIMARY_OUTLET, Router, UrlSegment } from '@angular/router'; +import { I18nService } from '@gentics/cms-components'; import { GcmsUiLanguage } from '@gentics/cms-integration-api-models'; import { IBreadcrumbRouterLink } from '@gentics/ui-core'; -import { I18nService } from '@gentics/cms-components'; import { has as _has, isEqual as _isEqual } from 'lodash-es'; import { BehaviorSubject, Observable, combineLatest, of as observableOf } from 'rxjs'; import { filter, map, switchMap, takeUntil } from 'rxjs/operators'; @@ -170,7 +170,7 @@ export class BreadcrumbsService extends InitializableServiceBase { let ret: RouteSegment; - if (data[ROUTE_BREADCRUMB_KEY]) { + if (data[ROUTE_BREADCRUMB_KEY]?.title) { ret = { routerCommands: routerLink, breadcrumb: data[ROUTE_BREADCRUMB_KEY], diff --git a/cms-ui/apps/admin-ui/src/app/mesh/components/management/management.component.cy.ts b/cms-ui/apps/admin-ui/src/app/mesh/components/management/management.component.cy.ts deleted file mode 100644 index 2201874ae2..0000000000 --- a/cms-ui/apps/admin-ui/src/app/mesh/components/management/management.component.cy.ts +++ /dev/null @@ -1,72 +0,0 @@ -// import { CoreModule } from '@admin-ui/core/core.module'; -// import { I18nNotificationService } from '@admin-ui/core/providers/i18n-notification/i18n-notification.service'; -// import { AppStateService } from '@admin-ui/state/providers/app-state/app-state.service'; -// import { TestAppState } from '@admin-ui/state/utils/test-app-state/test-app-state.mock'; -// import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -// import { RouterTestingModule } from '@angular/router/testing'; -// import { ContentRepository, ContentRepositoryType } from '@gentics/cms-models'; -// import { GCMSRestClientService } from '@gentics/cms-rest-client-angular'; -// import { GCMSTestRestClientService } from '@gentics/cms-rest-client-angular/testing'; -// import { MeshRestClientService } from '@gentics/mesh-rest-client-angular'; -// import { GenticsUICoreModule } from '@gentics/ui-core'; -// import { MeshRestClientConfig } from '@gentics/mesh-rest-client'; -// import { LoginGateComponent } from '../login-gate/login-gate.component'; -// import { ManagementComponent } from './management.component'; -// import { CmsComponentsModule } from '@gentics/cms-components'; - -// TODO: Create a test client service like the cms-client in the mesh package -// class MockMeshRestClientService implements Partial { -// init(config: MeshRestClientConfig, apiKey?: string): void { -// // nothing to do -// } - -// auth = { -// me: () => ({ -// send: () => Promise.resolve({ -// username: 'anonymous', -// }), -// }), -// } as any; -// } - -// class MockI18nNotificationService {} - -// Disabled until all cyclic imports are fixed. -// This needs a major rework of the admin-ui module system however. -xdescribe('ManagementComponent', () => { - - // it('should display the login when not logged in', () => { - // const REPO: ContentRepository = { - // id: 1, - // name: 'text', - // crType: ContentRepositoryType.MESH, - // } as any; - - // cy.mount(ManagementComponent, { - // imports: [ - // GenticsUICoreModule.forRoot(), - // CoreModule, - // RouterTestingModule, - // CmsComponentsModule, - // ], - // declarations: [ - // LoginGateComponent, - // ], - // providers: [ - // { provide: MeshRestClientService, useClass: MockMeshRestClientService }, - // { provide: AppStateService, useClass: TestAppState }, - // { provide: GCMSRestClientService, useClass: GCMSTestRestClientService }, - // { provide: I18nNotificationService, useClass: MockI18nNotificationService }, - // ], - // schemas: [CUSTOM_ELEMENTS_SCHEMA], - // componentProperties: { - // repository: REPO, - // }, - // }) - - // cy.get('.login') - // .should('exist') - // .and('be.visible'); - // }); - -}); diff --git a/cms-ui/apps/admin-ui/src/app/shared/components/wizard-modal/wizard-modal.component.ts b/cms-ui/apps/admin-ui/src/app/shared/components/wizard-modal/wizard-modal.component.ts index 3abca97ad4..23c1fbc8e0 100644 --- a/cms-ui/apps/admin-ui/src/app/shared/components/wizard-modal/wizard-modal.component.ts +++ b/cms-ui/apps/admin-ui/src/app/shared/components/wizard-modal/wizard-modal.component.ts @@ -2,7 +2,6 @@ import { ObservableStopper } from '@admin-ui/common'; import { ChangeDetectionStrategy, Component, - ComponentFactoryResolver, OnDestroy, OnInit, Type, @@ -43,11 +42,9 @@ export class WizardModalComponent, R> implements OnInit, OnD private stopper = new ObservableStopper(); - constructor( - private componentFactoryResolver: ComponentFactoryResolver, - ) { } + constructor() { } - ngOnInit(): void { + /*ngOnInit(): void { const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.wizardType); const wizardHost = this.wizardContainer.createComponent(componentFactory); this.applyProperties(wizardHost.instance); @@ -59,6 +56,22 @@ export class WizardModalComponent, R> implements OnInit, OnD takeUntil(this.stopper.stopper$), ).subscribe(output => this.closeFn(output)); + wizardHost.instance.wizard.wizardCancel.pipe( + takeUntil(this.stopper.stopper$), + ).subscribe(() => this.cancelFn()); + }*/ + + ngOnInit(): void { + const wizardHost = this.wizardContainer.createComponent(this.wizardType); + + this.applyProperties(wizardHost.instance); + + wizardHost.changeDetectorRef.detectChanges(); + + wizardHost.instance.wizard.wizardFinish.pipe( + takeUntil(this.stopper.stopper$), + ).subscribe(output => this.closeFn(output)); + wizardHost.instance.wizard.wizardCancel.pipe( takeUntil(this.stopper.stopper$), ).subscribe(() => this.cancelFn()); diff --git a/cms-ui/apps/admin-ui/src/app/shared/directives/action-allowed/action-allowed.directive.spec.ts b/cms-ui/apps/admin-ui/src/app/shared/directives/action-allowed/action-allowed.directive.spec.ts index 1e807c49ec..30d176059a 100644 --- a/cms-ui/apps/admin-ui/src/app/shared/directives/action-allowed/action-allowed.directive.spec.ts +++ b/cms-ui/apps/admin-ui/src/app/shared/directives/action-allowed/action-allowed.directive.spec.ts @@ -1,6 +1,8 @@ import { componentTest } from '@admin-ui/testing'; -import { AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, model, NO_ERRORS_SCHEMA, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { I18nService } from '@gentics/cms-components'; +import { MockI18nService } from '@gentics/cms-components/testing'; import { AccessControlledType, GcmsPermission } from '@gentics/cms-models'; import { ButtonComponent, GenticsUICoreModule, InputComponent } from '@gentics/ui-core'; import { cloneDeep } from 'lodash-es'; @@ -14,11 +16,9 @@ import { } from '../../../common'; import { PermissionsService, RequiredInstancePermissions, RequiredPermissions } from '../../../core'; import { ACTION_HIDDEN_CSS_CLASS, ActionAllowedDirective, DEFAULT_DISABLED_TOOLTIP } from './action-allowed.directive'; -import { I18nService } from '@gentics/cms-components'; -import { MockI18nService } from '@gentics/cms-components/testing'; function assembleTestTemplate(additionalAttributes: string): string { - return `Action Button`; + return `Action Button`; } @Component({ @@ -26,7 +26,7 @@ function assembleTestTemplate(additionalAttributes: string): string { standalone: false, }) class TestComponent { - userAction = 'typeTests.testAction'; + public readonly userAction = model('typeTests.testAction'); @ViewChild('actionButton') actionButton: ButtonComponent; @@ -42,12 +42,12 @@ class TestComponent { } @Component({ - template: '', + template: '', standalone: false, }) class InputTestComponent implements AfterViewInit { - userAction = 'typeTests.testAction'; + public readonly userAction = model('typeTests.testAction'); @ViewChild('inputField') inputField: InputComponent; @@ -301,7 +301,7 @@ describe('ActionAllowedDirective', () => { expect(permissions.checkPermissions).toHaveBeenCalledWith(MOCK_USER_ACTIONS.typeTests.testAction.typePermissions); assertActionState(instance, { disabled: false, hidden: false }); - instance.userAction = 'typeTests.testAction2'; + instance.userAction.set('typeTests.testAction2'); runDoubleChangeDetection(fixture); expect(permissions.checkPermissions).toHaveBeenCalledTimes(2); expect(permissions.checkPermissions).toHaveBeenCalledWith(MOCK_USER_ACTIONS.typeTests.testAction2.typePermissions); @@ -311,7 +311,7 @@ describe('ActionAllowedDirective', () => { it('leaves the control enabled if actionId is null', componentTest(() => TestComponent, (fixture, instance) => { - instance.userAction = null; + instance.userAction.set(null); runDoubleChangeDetection(fixture); expect(permissions.checkPermissions).not.toHaveBeenCalled(); @@ -321,7 +321,7 @@ describe('ActionAllowedDirective', () => { it('leaves the control enabled if actionId is an empty string', componentTest(() => TestComponent, (fixture, instance) => { - instance.userAction = ''; + instance.userAction.set(''); runDoubleChangeDetection(fixture); expect(permissions.checkPermissions).not.toHaveBeenCalled(); @@ -338,7 +338,7 @@ describe('ActionAllowedDirective', () => { assembleTestTemplate('[aaInstanceId]="instanceId"'), (fixture, instance) => { initServices(); - instance.userAction = 'instanceTests.instancePermsOnly'; + instance.userAction.set('instanceTests.instancePermsOnly'); runDoubleChangeDetection(fixture); // Since we haven't provided an instanceId yet, the component should be disabled. @@ -364,7 +364,7 @@ describe('ActionAllowedDirective', () => { assembleTestTemplate('[aaInstanceId]="instanceId" [aaNodeId]="nodeId"'), (fixture, instance) => { initServices(); - instance.userAction = 'instanceTests.typeAndInstancePerms'; + instance.userAction.set('instanceTests.typeAndInstancePerms'); instance.instanceId = INSTANCE_ID_A; instance.nodeId = NODE_ID_A; runDoubleChangeDetection(fixture); @@ -387,7 +387,7 @@ describe('ActionAllowedDirective', () => { assembleTestTemplate('[aaInstanceId]="instanceId" [aaNodeId]="nodeId"'), (fixture, instance) => { initServices(); - instance.userAction = 'instanceTests.instancePermsOnly'; + instance.userAction.set('instanceTests.instancePermsOnly'); instance.instanceId = INSTANCE_ID_A; instance.nodeId = NODE_ID_A; runDoubleChangeDetection(fixture); @@ -415,7 +415,7 @@ describe('ActionAllowedDirective', () => { assembleTestTemplate('[aaInstanceId]="instanceId" [aaNodeId]="nodeId"'), (fixture, instance) => { initServices(); - instance.userAction = 'instanceTests.instancePermsOnly'; + instance.userAction.set('instanceTests.instancePermsOnly'); instance.instanceId = INSTANCE_ID_A; instance.nodeId = NODE_ID_A; runDoubleChangeDetection(fixture); @@ -591,7 +591,7 @@ describe('ActionAllowedDirective', () => { it('uses the default tooltip if none is provided', componentTest(() => TestComponent, (fixture, instance) => { - instance.userAction = 'tooltipTests.noTooltip'; + instance.userAction.set('tooltipTests.noTooltip'); runDoubleChangeDetection(fixture); assertActionState(instance, { disabled: true, tooltip: DEFAULT_DISABLED_TOOLTIP, hidden: false }); @@ -600,7 +600,7 @@ describe('ActionAllowedDirective', () => { it('tooltip params work, if provided', componentTest(() => TestComponent, (fixture, instance) => { - instance.userAction = 'tooltipTests.tooltipParams'; + instance.userAction.set('tooltipTests.tooltipParams'); runDoubleChangeDetection(fixture); assertActionState(instance, { disabled: true, tooltip: TEST_TOOLTIP, hidden: false }); diff --git a/cms-ui/apps/admin-ui/src/testing/dynamic-components.ts b/cms-ui/apps/admin-ui/src/testing/dynamic-components.ts deleted file mode 100644 index 8ac92764ff..0000000000 --- a/cms-ui/apps/admin-ui/src/testing/dynamic-components.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Type, ComponentRef, ComponentFactoryResolver, ComponentFactory} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; - -/** - * Allows to create a spy on a dynamically created component. - * @param componentTypes The component types on which a spy should be created. - * @param setUpSpyFn The callback function that will set up the spy. - */ -export function spyOnDynamicallyCreatedComponent( - componentTypes: Type[], - setUpSpyFn: (componentType: Type, componentInstance: ComponentRef) => void -): void { - const componentFactoryResolver: ComponentFactoryResolver = TestBed.inject(ComponentFactoryResolver); - const origResolveFn = componentFactoryResolver.resolveComponentFactory.bind(componentFactoryResolver); - - spyOn(componentFactoryResolver, 'resolveComponentFactory').and.callFake((componentType: Type) => { - const componentFactory: ComponentFactory = origResolveFn(componentType); - - // Only spy on component types we are interested in. - if (componentTypes.findIndex(type => type === componentType) !== -1) { - const origCreateFn = componentFactory.create.bind(componentFactory); - spyOn(componentFactory, 'create').and.callFake((...args: any[]) => { - const component: ComponentRef = origCreateFn(...args); - setUpSpyFn(componentType, component); - return component; - }); - } - - return componentFactory; - }); -} diff --git a/cms-ui/apps/admin-ui/src/testing/index.ts b/cms-ui/apps/admin-ui/src/testing/index.ts index 17680a55f5..6a384af787 100644 --- a/cms-ui/apps/admin-ui/src/testing/index.ts +++ b/cms-ui/apps/admin-ui/src/testing/index.ts @@ -1,6 +1,5 @@ export * from './component-test'; export * from './configure-component-test'; -export * from './dynamic-components'; export * from './elastic-search-index'; export * from './mocks'; export * from './must-fail'; diff --git a/cms-ui/apps/admin-ui/tsconfig.json b/cms-ui/apps/admin-ui/tsconfig.json index 85ada22df9..765eea88df 100644 --- a/cms-ui/apps/admin-ui/tsconfig.json +++ b/cms-ui/apps/admin-ui/tsconfig.json @@ -6,10 +6,8 @@ "isolatedModules": true, "emitDecoratorMetadata": false, "module": "preserve", - "types": [ - "node", - "jquery" - ] + "types": ["node", "jquery"], + "ignoreDeprecations": "6.0" }, "files": [], "include": [], @@ -31,5 +29,5 @@ { "path": "./cypress/tsconfig.json" } - ], + ] } diff --git a/cms-ui/apps/ct-form-translations/tsconfig.json b/cms-ui/apps/ct-form-translations/tsconfig.json index 1ab8873e19..cd3be46c41 100644 --- a/cms-ui/apps/ct-form-translations/tsconfig.json +++ b/cms-ui/apps/ct-form-translations/tsconfig.json @@ -4,13 +4,14 @@ "target": "es2022", "moduleResolution": "bundler", "module": "preserve", + "ignoreDeprecations": "6.0" }, "files": [], "include": [], "angularCompilerOptions": { "annotateForClosureCompiler": true, "strictTemplates": false, - "strictInjectionParameters": true, + "strictInjectionParameters": true }, "references": [ { diff --git a/cms-ui/apps/ct-link-checker/cypress/tsconfig.json b/cms-ui/apps/ct-link-checker/cypress/tsconfig.json index f9adc82caf..1253a7ca70 100644 --- a/cms-ui/apps/ct-link-checker/cypress/tsconfig.json +++ b/cms-ui/apps/ct-link-checker/cypress/tsconfig.json @@ -1,17 +1,18 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "allowJs": true, - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["cypress", "node"], - "sourceMap": false - }, - "include": [ - "**/*.ts", - "**/*.js", - "../cypress.config.ts", - "../**/*.cy.ts", - "../**/*.d.ts" - ] + "extends": "../tsconfig.json", + "compilerOptions": { + "allowJs": true, + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["cypress", "node"], + "sourceMap": false, + "ignoreDeprecations": "6.0" + }, + "include": [ + "**/*.ts", + "**/*.js", + "../cypress.config.ts", + "../**/*.cy.ts", + "../**/*.d.ts" + ] } diff --git a/cms-ui/apps/ct-link-checker/project.json b/cms-ui/apps/ct-link-checker/project.json index 0f21ce90e0..8aafdab30a 100644 --- a/cms-ui/apps/ct-link-checker/project.json +++ b/cms-ui/apps/ct-link-checker/project.json @@ -4,7 +4,7 @@ "projectType": "application", "prefix": "gtx", "sourceRoot": "apps/ct-link-checker/src", - "tags": ["app", "ct", "angular"], + "tags": ["app", "ct", "angular", "test:karma"], "targets": { "build": { "executor": "@nx/angular:application", diff --git a/cms-ui/apps/ct-link-checker/tsconfig.json b/cms-ui/apps/ct-link-checker/tsconfig.json index 74d82be4bf..7348001d62 100644 --- a/cms-ui/apps/ct-link-checker/tsconfig.json +++ b/cms-ui/apps/ct-link-checker/tsconfig.json @@ -6,7 +6,8 @@ "isolatedModules": true, "emitDecoratorMetadata": false, "module": "preserve", - "types": ["node", "jquery"] + "types": ["node", "jquery"], + "ignoreDeprecations": "6.0" }, "files": [], "include": [], diff --git a/cms-ui/apps/editor-ui/cypress/tsconfig.json b/cms-ui/apps/editor-ui/cypress/tsconfig.json index 12f174438e..c36c03f3ff 100644 --- a/cms-ui/apps/editor-ui/cypress/tsconfig.json +++ b/cms-ui/apps/editor-ui/cypress/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "../../dist/out-tsc", "module": "commonjs", "types": ["cypress", "node"], - "sourceMap": false + "sourceMap": false, + "ignoreDeprecations": "6.0" }, "include": [ "**/*.ts", diff --git a/cms-ui/apps/editor-ui/project.json b/cms-ui/apps/editor-ui/project.json index 02b7bc58ee..4f135a591d 100644 --- a/cms-ui/apps/editor-ui/project.json +++ b/cms-ui/apps/editor-ui/project.json @@ -4,11 +4,7 @@ "projectType": "application", "prefix": "gtx", "sourceRoot": "apps/editor-ui/src", - "tags": [ - "app", - "ui", - "angular" - ], + "tags": ["app", "ui", "angular", "test:karma"], "targets": { "build": { "executor": "@nx/angular:application", diff --git a/cms-ui/apps/editor-ui/src/app/content-frame/components/combined-properties-editor/combined-properties-editor.component.spec.ts b/cms-ui/apps/editor-ui/src/app/content-frame/components/combined-properties-editor/combined-properties-editor.component.spec.ts index 128db467ed..c108492af7 100644 --- a/cms-ui/apps/editor-ui/src/app/content-frame/components/combined-properties-editor/combined-properties-editor.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/content-frame/components/combined-properties-editor/combined-properties-editor.component.spec.ts @@ -1,4 +1,4 @@ -import { Component, Input, ViewChild } from '@angular/core'; +import { Component, Input, model, ViewChild } from '@angular/core'; import { ComponentFixture, flush, TestBed, tick } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; @@ -18,13 +18,12 @@ import { ObjectTag, OverviewTagPartProperty, Page, - Raw, StringTagPartProperty, Tag, TagPartType, TagPropertyType, Tags, - Template, + Template } from '@gentics/cms-models'; import { getExampleFileData, @@ -204,10 +203,11 @@ describe('CombinedPropertiesEditorComponent', () => { describe('item properties', () => { it('loads additional item properties data and displays the item\'s properties', - componentTest(() => TestComponent, (fixture, testComponent) => { + componentTest(() => TestComponent, async (fixture, testComponent) => { fixture.detectChanges(); - testComponent.item = mockPage; - multiDetectChanges(fixture, 3); + + testComponent.item.set(mockPage); + await multiDetectChanges(fixture, 3, 1000); const expectedLanguages = state.now.folder.activeNodeLanguages.list.map((id) => mockLanguage(id)); const expectedTemplates = state.now.folder.templates.list.map((id) => mockTemplate(id)); @@ -236,7 +236,7 @@ describe('CombinedPropertiesEditorComponent', () => { contentModified: true, }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 3); const changes = { change: 'some change' }; @@ -346,7 +346,7 @@ describe('CombinedPropertiesEditorComponent', () => { it('displays one tab for the item properties and one tab for each object property', componentTest(() => TestComponent, (fixture, testComponent) => { - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); // Check if the correct sequence of tabs is displayed (one for the item properties, @@ -376,7 +376,7 @@ describe('CombinedPropertiesEditorComponent', () => { withDelete: false, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); // Navigate to the last object property. @@ -435,7 +435,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); // Make sure that the TagEditor has been initialized correctly. @@ -478,7 +478,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); // Make sure that the TagEditor has been initialized correctly. @@ -511,7 +511,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); const tagEditorHost: MockTagEditorHost = testComponent.combinedPropertiesEditor.tagEditorHostList.first as any; @@ -571,7 +571,7 @@ describe('CombinedPropertiesEditorComponent', () => { }); validateTagSpy.and.returnValue({ allPropertiesValid: false }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); const tagEditorHost: MockTagEditorHost = testComponent.combinedPropertiesEditor.tagEditorHostList.first as any; @@ -598,7 +598,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 3); const tagEditorHost: MockTagEditorHost = testComponent.combinedPropertiesEditor.tagEditorHostList.first as any; @@ -623,7 +623,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); const tagEditorHost: MockTagEditorHost = testComponent.combinedPropertiesEditor.tagEditorHostList.first as any; @@ -712,7 +712,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockFolder; + testComponent.item.set(mockFolder); multiDetectChanges(fixture, 2); const tagEditorHost: MockTagEditorHost = testComponent.combinedPropertiesEditor.tagEditorHostList.first as any; @@ -787,7 +787,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); const tagEditorHost: MockTagEditorHost = testComponent.combinedPropertiesEditor.tagEditorHostList.first as any; @@ -898,7 +898,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); expect(fixture.debugElement.query(By.css(ITEM_PROPERTIES_EDITOR_SELECTOR))).toBeFalsy(); @@ -928,7 +928,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); expect(fixture.debugElement.query(By.css(ITEM_PROPERTIES_EDITOR_SELECTOR))).toBeFalsy(); @@ -971,7 +971,7 @@ describe('CombinedPropertiesEditorComponent', () => { updatedItem.tags[editedObjProp.name].id = tagId; folderActions.updateItemObjectProperties.and.returnValue(Promise.resolve(updatedItem)); - testComponent.item = mockFolder; + testComponent.item.set(mockFolder); multiDetectChanges(fixture, 2); const iFrameWrapper = fixture.debugElement.query(By.directive(MockIFrameWrapper)); @@ -1008,7 +1008,7 @@ describe('CombinedPropertiesEditorComponent', () => { // Simulate that the tag has not been added to the DB yet. delete mockFolder.tags[editedObjProp.name].id; - testComponent.item = mockFolder; + testComponent.item.set(mockFolder); multiDetectChanges(fixture, 2); const iFrameWrapper = fixture.debugElement.query(By.directive(MockIFrameWrapper)); @@ -1037,7 +1037,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); }), ); @@ -1053,7 +1053,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); }), ); @@ -1069,7 +1069,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); expect(fixture.debugElement.query(By.css(ITEM_PROPERTIES_EDITOR_SELECTOR))).toBeFalsy(); @@ -1100,7 +1100,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); expect(fixture.debugElement.query(By.directive(MockIFrameWrapper))).toBeTruthy(); @@ -1157,7 +1157,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockFolder; + testComponent.item.set(mockFolder); multiDetectChanges(fixture, 2); expect(fixture.debugElement.query(By.directive(MockIFrameWrapper))).toBeTruthy(); @@ -1233,7 +1233,7 @@ describe('CombinedPropertiesEditorComponent', () => { }, }); - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); expect(fixture.debugElement.query(By.directive(MockIFrameWrapper))).toBeTruthy(); @@ -1301,7 +1301,7 @@ describe('CombinedPropertiesEditorComponent', () => { it('does not display the active/inactive checkbox', componentTest(() => TestComponent, (fixture, testComponent) => { - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); // Navigate to the last object property. @@ -1329,7 +1329,7 @@ describe('CombinedPropertiesEditorComponent', () => { it('displays the active/inactive checkbox', componentTest(() => TestComponent, (fixture, testComponent) => { - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); // Navigate to the last object property. @@ -1395,7 +1395,7 @@ describe('CombinedPropertiesEditorComponent', () => { describe('tag list', () => { it('displays one tab for the tag list for page items', componentTest(() => TestComponent, (fixture, testComponent) => { - testComponent.item = mockPage; + testComponent.item.set(mockPage); multiDetectChanges(fixture, 2); // Check if tab list tag is present. @@ -1413,7 +1413,7 @@ describe('CombinedPropertiesEditorComponent', () => { numItems: 0, responseInfo: null, })); - testComponent.item = mockFolder; + testComponent.item.set(mockFolder); multiDetectChanges(fixture, 2); // Check if no tab list tag is present. @@ -1435,7 +1435,7 @@ describe('CombinedPropertiesEditorComponent', () => { numItems: 0, responseInfo: null, })); - testComponent.item = mockFile; + testComponent.item.set(mockFile); multiDetectChanges(fixture, 2); // Check if no tab list tag is present. @@ -1450,7 +1450,7 @@ describe('CombinedPropertiesEditorComponent', () => { it('displays no tab for the tag list for image items', componentTest(() => TestComponent, (fixture, testComponent) => { - testComponent.item = mockImage; + testComponent.item.set(mockImage); multiDetectChanges(fixture, 2); // Check if no tab list tag is present. @@ -1465,7 +1465,7 @@ describe('CombinedPropertiesEditorComponent', () => { it('with a page as item parses content tags accordingly', componentTest(() => TestComponent, (fixture, testComponent) => { - testComponent.item = mockPage; + testComponent.item.set(mockPage); fixture.detectChanges(); const contentTags: Tag[] = generateContentTagList(mockPage); tick(1000); @@ -1485,7 +1485,7 @@ describe('CombinedPropertiesEditorComponent', () => { numItems: 0, responseInfo: null, })); - testComponent.item = mockPage; + testComponent.item.set(mockPage); fixture.detectChanges(); let contentTags: Tag[] = generateContentTagList(mockPage); tick(100); @@ -1495,7 +1495,7 @@ describe('CombinedPropertiesEditorComponent', () => { expect(testComponent.combinedPropertiesEditor.contentTagRows.map((row) => row.item)).toEqual(contentTags); // switch to folder item - testComponent.item = mockFolder; + testComponent.item.set(mockFolder); fixture.detectChanges(); contentTags = generateContentTagList(mockFolder as any); tick(100); @@ -1514,7 +1514,7 @@ describe('CombinedPropertiesEditorComponent', () => { numItems: 0, responseInfo: null, })); - testComponent.item = mockFolder; + testComponent.item.set(mockFolder); fixture.detectChanges(); let contentTags: Tag[] = generateContentTagList(mockFolder as any); tick(100); @@ -1522,7 +1522,7 @@ describe('CombinedPropertiesEditorComponent', () => { expect(testComponent.combinedPropertiesEditor.contentTagRows).toEqual([]); // switch to page item - testComponent.item = mockPage; + testComponent.item.set(mockPage); fixture.detectChanges(); contentTags = generateContentTagList(mockPage); tick(100); @@ -1545,7 +1545,7 @@ describe('CombinedPropertiesEditorComponent', () => { tagType: editTagInfo.tagType, }, readOnly: editTagInfo.readOnly, - node: editTagInfo.node as Node, + node: editTagInfo.node, validator: { validateAllTagProperties: validateTagSpy, } as any, @@ -1675,11 +1675,12 @@ function mockTemplate(id: number): Template { return ret as Template; } -function multiDetectChanges(fixture: ComponentFixture, count: number, delay: number = 100): void { +async function multiDetectChanges(fixture: ComponentFixture, count: number, delay: number = 100): Promise { for (let i = 0; i < count; ++i) { fixture.detectChanges(); flush(); tick(delay); + await fixture.whenStable(); } } @@ -1706,7 +1707,7 @@ class MockI18nNotification { @Component({ selector: 'test-component', template: ` - + `, standalone: false, @@ -1715,7 +1716,7 @@ class TestComponent { @ViewChild(CombinedPropertiesEditorComponent, { static: true }) combinedPropertiesEditor: CombinedPropertiesEditorComponent; - item: ItemWithObjectTags | Node; + readonly item = model(); } @Component({ diff --git a/cms-ui/apps/editor-ui/src/app/core/components/users-list/users-list.component.spec.ts b/cms-ui/apps/editor-ui/src/app/core/components/users-list/users-list.component.spec.ts index cdf8d00819..04e3de38ae 100644 --- a/cms-ui/apps/editor-ui/src/app/core/components/users-list/users-list.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/core/components/users-list/users-list.component.spec.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, model } from '@angular/core'; import { ComponentFixture, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CheckboxComponent, GenticsUICoreModule } from '@gentics/ui-core'; @@ -28,18 +28,18 @@ describe('UsersList', () => { })); it('checks those with selected ids', componentTest(() => TestComponent, (fixture, instance) => { - instance.selected = [1, 3]; + instance.selected.set([1, 3]); fixture.detectChanges(); const checkboxes = getUserCheckboxes(fixture); expect(checkboxes.map((checkbox) => checkbox.value)).toEqual([true, false, true, false]); - instance.selected = []; + instance.selected.set([]); fixture.detectChanges(); expect(checkboxes.map((checkbox) => checkbox.value)).toEqual([false, false, false, false]); - instance.selected = [1, 2, 3, 4]; + instance.selected.set([1, 2, 3, 4]); fixture.detectChanges(); expect(checkboxes.map((checkbox) => checkbox.value)).toEqual([true, true, true, true]); @@ -110,27 +110,29 @@ function clickCheckbox(fixture: ComponentFixture, index: number): selector: 'test-component', template: ` `, standalone: false, }) class TestComponent { - users: any[] = []; - selected: number[] = []; + readonly users = model([]); + readonly selected = model([]); constructor() { for (let i = 1; i < 5; i++) { - this.users.push({ - id: i, - firstName: `firstName_${i}`, - lastName: `lastName_${i}`, + this.users.update((arr) => { + return [...arr, { + id: i, + firstName: `firstName_${i}`, + lastName: `lastName_${i}`, + }]; }); } } selectedChange(newSelection: number[]): void { - this.selected = newSelection; + this.selected.set(newSelection); } } diff --git a/cms-ui/apps/editor-ui/src/app/list-view/components/item-list/item-list.component.spec.ts b/cms-ui/apps/editor-ui/src/app/list-view/components/item-list/item-list.component.spec.ts index f063f3758f..a307014ac6 100644 --- a/cms-ui/apps/editor-ui/src/app/list-view/components/item-list/item-list.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/list-view/components/item-list/item-list.component.spec.ts @@ -3,6 +3,7 @@ import { Component, EventEmitter, Input, + model, NO_ERRORS_SCHEMA, OnInit, Pipe, @@ -103,18 +104,18 @@ const allPermissions = (): EditorPermissions => // Sorry, but it works. @Component({ template: ` `, standalone: false, }) @@ -122,31 +123,32 @@ class TestComponent implements OnInit { @ViewChild('itemList', { static: true }) itemList: ItemListComponent; - itemType = 'folder'; - items: Array | Partial | Partial | Partial> = [ + readonly itemType = model<'folder' | 'page' | 'image' | 'file'>('folder'); + readonly items = model | Partial | Partial | Partial>>([ { id: 1, name: 'item1', path: 'root/item1', type: 'folder' }, { id: 2, name: 'item2', path: 'root/item2', type: 'folder' }, { id: 3, name: 'item3', path: 'root/item3', type: 'folder' }, - ]; + ]); - activeNode: any = { + readonly activeNode = model<{ name: string; id: number }>({ name: '', id: 1, - }; + }); - filterTerm = ''; - itemsInfo$: Observable; - currentFolderId$: Observable; - selectedItems: number[] = []; - startPageId: number = Number.NaN; - activeItemId: number; - permissions = allPermissions(); - isSearching = true; - itemsInfoPipe$: Observable; - activeNodeLanguages: Language[] = [ + readonly filterTerm = model(''); + readonly selectedItems = model([]); + readonly startPageId = model(Number.NaN); + readonly activeItemId = model(); + readonly permissions = model(allPermissions()); + readonly isSearching = model(true); + readonly activeNodeLanguages = model([ { id: 1, code: 'en', name: 'English' }, { id: 2, code: 'de', name: 'Deutsch (German)' }, - ]; + ]); + + itemsInfoPipe$: Observable; + itemsInfo$: Observable; + currentFolderId$: Observable; constructor(public appState: ApplicationStateService) { } @@ -424,13 +426,13 @@ describe('ItemListComponent', () => { it('calls getTotalUsage correct times with the correct parameters', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; + instance.itemType.set('page'); instance.itemsInfo$ = state.select((state) => state.folder.pages); - instance.items = [ + instance.items.set([ { id: 1, name: 'item1', path: 'root/item1', type: 'page' }, { id: 2, name: 'item2', path: 'root/item2', type: 'page' }, { id: 3, name: 'item3', path: 'root/item3', type: 'page' }, - ]; + ]); instance.itemList.itemsInfo = { list: [1, 2] } as ItemsInfo; instance.itemList.activeNode = { id: 11 } as NodeModel; instance.itemList.itemType = 'page'; @@ -478,11 +480,11 @@ describe('ItemListComponent', () => { it('checks if getTotalUsage gets called with the correct parameters when usage display field is active and the saving state is false', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.activeNode = { + instance.itemType.set('page'); + instance.activeNode.set({ name: '', id: 33, - }; + }); const usageActions = TestBed.inject(UsageActionsService); state.mockState({ folder: { @@ -524,7 +526,7 @@ describe('ItemListComponent', () => { it('checks if getTotalUsage gets called when usage display field is active', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; + instance.itemType.set('page'); const usageActions = TestBed.inject(UsageActionsService); state.mockState({ ...state.now, @@ -565,7 +567,7 @@ describe('ItemListComponent', () => { it('displays the live URL for images', componentTest(() => TestComponent, (fixture, instance) => { const testImage: Partial = { name: 'item1', path: 'root/item1', globalId: 'itemA', type: 'image' }; - instance.items = [testImage]; + instance.items.set([testImage]); fixture.detectChanges(); tick(); const getImageLiveURL = (el: Element) => (el.querySelector('image-thumbnail .liveurl-icon')); @@ -575,12 +577,12 @@ describe('ItemListComponent', () => { it('displays the live URL for files', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'file'; - instance.items = [ + instance.itemType.set('file'); + instance.items.set([ { type: 'file', name: 'file1', id: 1, path: 'root/file1' }, { type: 'file', name: 'file2', id: 2, path: 'root/file2' }, { type: 'file', name: 'file3', id: 3, path: 'root/file3' }, - ]; + ]); fixture.detectChanges(); tick(); const getFileLiveURL = (el: Element) => (el.querySelector('.liveurl-icon')); @@ -590,10 +592,10 @@ describe('ItemListComponent', () => { it('displays the correct path for pages if showPath is true', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.items = [ + instance.itemType.set('page'); + instance.items.set([ { type: 'page', name: 'page1', id: 1, path: 'root/page1', publishPath: '/root/page1' }, - ]; + ]); updateItemsInfoState({ showPath: true, }); @@ -608,10 +610,10 @@ describe('ItemListComponent', () => { it('does not display the path for pages if showPath is false', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.items = [ + instance.itemType.set('page'); + instance.items.set([ { type: 'page', name: 'page1', id: 1, path: 'root/page1' }, - ]; + ]); updateItemsInfoState({ showPath: false, }); @@ -627,7 +629,7 @@ describe('ItemListComponent', () => { it('filters based on filterTerm', componentTest(() => TestComponent, (fixture, instance) => { - instance.filterTerm = '2'; + instance.filterTerm.set('2'); fixture.detectChanges(); tick(); const listItems: HTMLElement[] = getListItems(fixture); @@ -640,14 +642,14 @@ describe('ItemListComponent', () => { componentTest(() => TestComponent, (fixture, instance) => { let listItems: HTMLElement[]; - instance.filterTerm = '2'; + instance.filterTerm.set('2'); fixture.detectChanges(); tick(); listItems = getListItems(fixture); expect(listItems.length).toBe(1); expect(getItemName(listItems[0])).toContain('item2'); - instance.filterTerm = ''; + instance.filterTerm.set(''); fixture.detectChanges(); listItems = getListItems(fixture); expect(listItems.length).toBe(3); @@ -660,13 +662,13 @@ describe('ItemListComponent', () => { it('handles unexpected filterTerm values', componentTest(() => TestComponent, (fixture, instance) => { const detectChanges = () => fixture.detectChanges(); - instance.filterTerm = 2; + instance.filterTerm.set(2 as any); // why? expect(detectChanges).not.toThrow(); - instance.filterTerm = null; + instance.filterTerm.set(null); expect(detectChanges).not.toThrow(); - instance.filterTerm = undefined; + instance.filterTerm.set(undefined); expect(detectChanges).not.toThrow(); tick(); }), @@ -674,13 +676,13 @@ describe('ItemListComponent', () => { it('copies only selected items that match filterTerm', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; + instance.itemType.set('page'); instance.itemsInfo$ = state.select((state) => state.folder.pages); - instance.items = [ + instance.items.set([ { id: 1, name: 'item1', path: 'root/item1', type: 'page' }, { id: 2, name: 'item2', path: 'root/item2', type: 'page' }, { id: 3, name: 'item3', path: 'root/item3', type: 'page' }, - ]; + ]); const contextMenuService = TestBed.inject(ContextMenuOperationsService); spyOn(contextMenuService, 'copyItems').and.stub(); @@ -697,7 +699,7 @@ describe('ItemListComponent', () => { fixture.detectChanges(); expect(state.now.folder.pages.selected).toEqual([1, 2]); - instance.filterTerm = '2'; + instance.filterTerm.set('2'); fixture.detectChanges(); tick(); const copyButton: HTMLElement = fixture.debugElement @@ -707,7 +709,7 @@ describe('ItemListComponent', () => { fixture.detectChanges(); tick(); - expect(contextMenuService.copyItems).toHaveBeenCalledWith('page', [instance.items[1]] as any, state.now.folder.activeNode); + expect(contextMenuService.copyItems).toHaveBeenCalledWith('page', [instance.items()[1]] as any, state.now.folder.activeNode); }), ); @@ -851,7 +853,7 @@ describe('ItemListComponent', () => { it('emits with all items when toggleAll is clicked and a filterTerm is being applied', componentTest(() => TestComponent, (fixture, instance) => { - instance.filterTerm = '2'; + instance.filterTerm.set('2'); fixture.detectChanges(); const toggleAll: HTMLElement = fixture.nativeElement.querySelector('.list-header input[type="checkbox"] + label'); @@ -968,11 +970,11 @@ describe('ItemListComponent', () => { it(`focuses the editor when a ${itemType} item is clicked`, componentTest(() => TestComponent, (fixture, instance) => { - instance.items = [ + instance.items.set([ { id: 1, name: 'item1', path: 'root/item1', type: itemType }, { id: 2, name: 'item2', path: 'root/item2', type: itemType }, { id: 3, name: 'item3', path: 'root/item3', type: itemType }, - ]; + ]); // editorIsFocused won't change unless the editor is open state.mockState({ editor: { @@ -995,11 +997,11 @@ describe('ItemListComponent', () => { it('does not focus the editor when a folder item is clicked', componentTest(() => TestComponent, (fixture, instance) => { - instance.items = [ + instance.items.set([ { id: 1, name: 'item1', path: 'root/item1', type: 'folder' }, { id: 2, name: 'item2', path: 'root/item2', type: 'folder' }, { id: 3, name: 'item3', path: 'root/item3', type: 'folder' }, - ]; + ]); fixture.detectChanges(); tick(); const listItems = getListItems(fixture); @@ -1029,8 +1031,8 @@ describe('ItemListComponent', () => { }, }); - instance.isSearching = true; - instance.items = [1, 2, 3].map((id) => state.now.entities.page[id]); + instance.isSearching.set(true); + instance.items.set([1, 2, 3].map((id) => state.now.entities.page[id])); updateItemsInfoState({ list: [1, 2, 3], total: 3, @@ -1056,8 +1058,8 @@ describe('ItemListComponent', () => { function setupTestCase(fixture: ComponentFixture): void { testPage = { type: 'page', id: 1, name: 'page1', path: 'root/page1', globalId: 'pageA' } as Page; const instance = (currentFixture = fixture).componentRef.instance; - instance.itemType = 'page'; - instance.items = [testPage]; + instance.itemType.set('page'); + instance.items.set([testPage]); updateItemsInfoState({ list: [1], total: 1, @@ -1065,7 +1067,7 @@ describe('ItemListComponent', () => { } function mockIsStartPage(startPage: boolean): void { - currentFixture.componentRef.instance.startPageId = startPage ? 1 : 99999; + currentFixture.componentRef.instance.startPageId.set(startPage ? 1 : 99999); currentFixture.detectChanges(); } @@ -1113,10 +1115,10 @@ describe('ItemListComponent', () => { it('shows a language indicator for pages with language variants ', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.items = [ + instance.itemType.set('page'); + instance.items.set([ { ...getExamplePageData({ id: 1 }), languageVariants: [1, 324423], deleted: { at: 0, by: null } }, - ]; + ]); updateItemsInfoState({ list: [66], total: 1, @@ -1125,6 +1127,7 @@ describe('ItemListComponent', () => { const links: HTMLElement[] = Array.from(fixture.nativeElement.querySelectorAll('page-language-indicator gtx-language-state')); expect(links.length).toBe(1); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(links[0].querySelector('.language-button').classList.contains('available')).toBeTrue; expect(links[0].querySelector('.language-code').textContent).toMatch(new RegExp(/(en)/, 'i')); @@ -1132,12 +1135,12 @@ describe('ItemListComponent', () => { }), ); - it('shows a language indicator for pages with no language variants but an assigned language if displayAllLangauges is enabled', + it('shows a language indicator for pages with no language variants but an assigned language if displayAllLanguages is enabled', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.items = [ + instance.itemType.set('page'); + instance.items.set([ { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], deleted: { at: 0, by: null } }, - ]; + ]); updateItemsInfoState({ list: [66], total: 1, @@ -1149,9 +1152,11 @@ describe('ItemListComponent', () => { const links: HTMLElement[] = Array.from(fixture.nativeElement.querySelectorAll('page-language-indicator gtx-language-state')); expect(links.length).toBe(2); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(links[0].querySelector('.language-button').classList.contains('available')).toBeTrue; expect(links[0].querySelector('.language-code').textContent).toMatch(new RegExp(/(en)/, 'i')); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(links[1].querySelector('.language-button').classList.contains('available')).toBeTrue; expect(links[1].querySelector('.language-code').textContent).toMatch(new RegExp(/(de)/, 'i')); @@ -1163,12 +1168,12 @@ describe('ItemListComponent', () => { it('highlights the item currently opened in the editor (folders, pages, files, and images in list view)', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.items = [ + instance.itemType.set('page'); + instance.items.set([ { type: 'page', name: 'page1', id: 1, path: 'root/page1' }, { type: 'page', name: 'page2', id: 2, path: 'root/page2' }, { type: 'page', name: 'page3', id: 3, path: 'root/page3' }, - ]; + ]); updateItemsInfoState({ list: [1, 2, 3], total: 3, @@ -1181,7 +1186,7 @@ describe('ItemListComponent', () => { itemId: 3, }, }); - instance.activeItemId = instance.items[2].id; + instance.activeItemId.set(instance.items()[2].id); fixture.detectChanges(); const backgroundColors = Array.from(fixture.nativeElement.querySelectorAll('gtx-contents-list-item')) @@ -1198,12 +1203,12 @@ describe('ItemListComponent', () => { it('highlights the item currently opened in the editor (images)', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'image'; - instance.items = [ + instance.itemType.set('image'); + instance.items.set([ { type: 'image', name: 'image1', id: 1, path: 'root/image1' }, { type: 'image', name: 'image2', id: 2, path: 'root/image2' }, { type: 'image', name: 'image3', id: 3, path: 'root/image3' }, - ]; + ]); updateItemsInfoState({ list: [1, 2, 3], total: 3, @@ -1221,7 +1226,7 @@ describe('ItemListComponent', () => { itemId: 3, }, }); - instance.activeItemId = instance.items[2].id; + instance.activeItemId.set(instance.items()[2].id); fixture.detectChanges(); const boxShadows = Array.from(fixture.nativeElement.querySelectorAll('image-thumbnail')) diff --git a/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.html b/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.html index a07ba8caf0..c6c32c9829 100644 --- a/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.html +++ b/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.html @@ -24,7 +24,7 @@ formControlName="chipProperty" [options]="searchableProperties" [noInput]="true" - [style.width]="getChipValue(chip)?.length < 4 ? '4ch' : getChipValue(chip).length + 1 + 'ch'" + [style.width]="getChipWidth(chip) + 'ch'" > @@ -74,7 +74,7 @@ type="text" class="gtx-chip-input-value-inner gtx-chip-input-value-inner-string" formControlName="chipValue" - [style.width]="getChipValue(chip)?.length < 4 ? '4ch' : getChipValue(chip).length + 1 + 'ch'" + [style.width]="getChipWidth(chip) + 'ch'" /> } diff --git a/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.ts b/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.ts index 4c6b7159e2..f97fe0336d 100644 --- a/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.ts +++ b/cms-ui/apps/editor-ui/src/app/shared/components/chip-search-bar/chip-search-bar.component.ts @@ -458,6 +458,11 @@ export class ChipSearchBarComponent implements OnInit, OnChanges, AfterViewInit, return this.getFormControlValue(c); } + getChipWidth(control: AbstractControl): number { + const chipValue = String(this.getChipValue(control)); + return Math.max(4, (chipValue?.length ?? 0) + 1); + } + setChipValue(control: AbstractControl, newValue: GtxChipValue): void { const c = this.getChipValueControl(control); this.setFormControlValue(c, newValue); diff --git a/cms-ui/apps/editor-ui/src/app/shared/components/item-list-row/item-list-row.component.spec.ts b/cms-ui/apps/editor-ui/src/app/shared/components/item-list-row/item-list-row.component.spec.ts index 807fc35ac0..58299c2bc8 100644 --- a/cms-ui/apps/editor-ui/src/app/shared/components/item-list-row/item-list-row.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/shared/components/item-list-row/item-list-row.component.spec.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, + model, NO_ERRORS_SCHEMA, Pipe, PipeTransform, @@ -71,11 +72,11 @@ const getItemName = (listItem: Element): string => (listItem.querySelector('.ite template: ` (listItem.querySelector('.ite standalone: false, }) class TestComponent { - itemType = 'file'; - item: Partial | Partial | Partial | Partial = { + readonly itemType = model<'file' | 'page' | 'folder' | 'image' | 'form'>('file'); + readonly item = model | Partial | Partial | Partial>({ id: 1, name: 'item1', path: 'root/item1', publishPath: '/root/item1', type: 'file', deleted: { at: 0, by: null }, - }; + }); activeNode: any = { name: '', @@ -312,7 +313,7 @@ describe('ItemListRow', () => { it('shows online status for images that are online', componentTest(() => TestComponent, (fixture, instance) => { const testImage: Partial = { name: 'item1', path: 'root/item1', globalId: 'itemA', type: 'image', online: true }; - instance.item = testImage; + instance.item.set(testImage); fixture.detectChanges(); tick(); @@ -324,7 +325,7 @@ describe('ItemListRow', () => { it('shows offline status for images that are offline', componentTest(() => TestComponent, (fixture, instance) => { const testImage: Partial = { name: 'item1', path: 'root/item1', globalId: 'itemA', type: 'image', online: false }; - instance.item = testImage; + instance.item.set(testImage); fixture.detectChanges(); tick(); @@ -347,8 +348,8 @@ describe('ItemListRow', () => { it('does not show a language indicator for pages when less than 2 node languages', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.item = { ...getExamplePageData({ id: 1 }), languageVariants: [], deleted: { at: 0, by: null } }; + instance.itemType.set('page'); + instance.item.set({ ...getExamplePageData({ id: 1 }), languageVariants: [], deleted: { at: 0, by: null } }); instance.nodeLanguages = [ { id: 1, code: 'en', name: 'English' }, ]; @@ -361,13 +362,13 @@ describe('ItemListRow', () => { ); it('shows a language indicator for translated pages without additional status icons and without all untranslated languages visible', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); expect(fixture.nativeElement.querySelector('page-language-indicator')).toBe(null); - instance.itemType = 'page'; - instance.item = { + instance.itemType.set('page'); + const testItem = { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], online: true, @@ -375,20 +376,27 @@ describe('ItemListRow', () => { at: 0, by: null, }, - }; + } as Page; + instance.item.set(testItem); state.mockState({ entities: { page: { - [instance.item.id]: instance.item as any, + [testItem.id]: testItem, }, - }, + } as any, }); + tick(); fixture.detectChanges(); + tick(); + await fixture.whenRenderingDone(); state.dispatch(new SetDisplayAllLanguagesAction(false)); tick(); + fixture.detectChanges(); + tick(); + await fixture.whenRenderingDone(); const langStateBtn = fixture.nativeElement.querySelector('page-language-indicator gtx-language-state .language-button'); expect(langStateBtn).toBeTruthy(); @@ -398,8 +406,8 @@ describe('ItemListRow', () => { it('does show an offline language indicator for English page without additional status icons and without all untranslated languages visible', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.item = { + instance.itemType.set('page'); + const testItem = { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], online: false, @@ -411,13 +419,14 @@ describe('ItemListRow', () => { at: 0, by: null, }, - }; + } as Page; + instance.item.set(testItem); state.mockState({ entities: { page: { - [instance.item.id]: instance.item as any, + [testItem.id]: testItem, }, - }, + } as any, }); state.dispatch(new SetDisplayAllLanguagesAction(false)); @@ -433,8 +442,8 @@ describe('ItemListRow', () => { it('does show an published language indicator for English page without additional status icons and without all untranslated languages visible', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.item = { + instance.itemType.set('page'); + const testItem = { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], online: true, @@ -446,13 +455,14 @@ describe('ItemListRow', () => { at: 0, by: null, }, - }; + } as Page; + instance.item.set(testItem); state.mockState({ entities: { page: { - [instance.item.id]: instance.item as any, + [testItem.id]: testItem, }, - }, + } as any, }); state.dispatch(new SetDisplayAllLanguagesAction(false)); @@ -468,8 +478,8 @@ describe('ItemListRow', () => { it('does show a language indicator for English page and with additional status icon "modified" and without all untranslated languages visible', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.item = { + instance.itemType.set('page'); + const testItem = { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], online: true, @@ -481,13 +491,14 @@ describe('ItemListRow', () => { at: 0, by: null, }, - }; + } as Page; + instance.item.set(testItem); state.mockState({ entities: { page: { - [instance.item.id]: instance.item as any, + [testItem.id]: testItem, }, - }, + } as any, }); state.dispatch(new SetDisplayAllLanguagesAction(false)); @@ -508,8 +519,8 @@ describe('ItemListRow', () => { it('does show a language indicator for English page and with additional status icon "queued" and without all untranslated languages visible', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.item = { + instance.itemType.set('page'); + const testItem = { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], online: true, @@ -521,13 +532,14 @@ describe('ItemListRow', () => { at: 0, by: null, }, - }; + } as Page; + instance.item.set(testItem); state.mockState({ entities: { page: { - [instance.item.id]: instance.item as any, + [testItem.id]: testItem, }, - }, + } as any, }); state.dispatch(new SetDisplayAllLanguagesAction(false)); @@ -548,8 +560,8 @@ describe('ItemListRow', () => { it('does show a language indicator for English page and with additional status icon "planned" and without all untranslated languages visible', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.item = { + instance.itemType.set('page'); + const testItem = { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], online: true, @@ -561,13 +573,14 @@ describe('ItemListRow', () => { at: 0, by: null, }, - }; + } as Page; + instance.item.set(testItem); state.mockState({ entities: { page: { - [instance.item.id]: instance.item as any, + [testItem.id]: testItem, }, - }, + } as any, }); state.dispatch(new SetDisplayAllLanguagesAction(false)); @@ -588,8 +601,8 @@ describe('ItemListRow', () => { it('does show a language indicator for English page and with additional status icon "inherited" and without all untranslated languages visible', componentTest(() => TestComponent, (fixture, instance) => { - instance.itemType = 'page'; - instance.item = { + instance.itemType.set('page'); + const testItem = { ...getExamplePageData({ id: 1 }), languageVariants: [1, 2], online: true, @@ -601,13 +614,14 @@ describe('ItemListRow', () => { at: 0, by: null, }, - }; + } as Page; + instance.item.set(testItem); state.mockState({ entities: { page: { - [instance.item.id]: instance.item as any, + [testItem.id]: testItem, }, - }, + } as any, }); state.dispatch(new SetDisplayAllLanguagesAction(false)); @@ -648,8 +662,8 @@ describe('ItemListRow', () => { by: null, }, }; - instance.itemType = 'page'; - instance.item = pageEN; + instance.itemType.set('page'); + instance.item.set(pageEN); instance.nodeLanguages = [ { id: 1, code: 'en', name: 'English' }, { id: 2, code: 'de', name: 'Deutsch (German)' }, @@ -730,8 +744,8 @@ describe('ItemListRow', () => { by: null, }, }; - instance.itemType = 'page'; - instance.item = pageEN; + instance.itemType.set('page'); + instance.item.set(pageEN); instance.itemsInfo.total = 1; instance.nodeLanguages = [ { id: 1, code: 'en', name: 'English' }, @@ -816,8 +830,8 @@ describe('ItemListRow', () => { by: null, }, }; - instance.itemType = 'page'; - instance.item = pageEN; + instance.itemType.set('page'); + instance.item.set(pageEN); instance.itemsInfo.total = 1; instance.nodeLanguages = [ { id: 1, code: 'en', name: 'English' }, @@ -902,8 +916,8 @@ describe('ItemListRow', () => { by: null, }, }; - instance.itemType = 'page'; - instance.item = pageEN; + instance.itemType.set('page'); + instance.item.set(pageEN); instance.itemsInfo.total = 1; instance.nodeLanguages = [ { id: 1, code: 'en', name: 'English' }, @@ -979,7 +993,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = undefined; fixture.detectChanges(); tick(); @@ -999,7 +1013,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = 4; fixture.detectChanges(); tick(); @@ -1019,7 +1033,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = 1; fixture.detectChanges(); tick(); @@ -1039,7 +1053,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = 3; fixture.detectChanges(); tick(); @@ -1066,7 +1080,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = undefined; fixture.detectChanges(); tick(); @@ -1086,7 +1100,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = 4; fixture.detectChanges(); tick(); @@ -1106,7 +1120,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = 1; fixture.detectChanges(); tick(); @@ -1126,7 +1140,7 @@ describe('ItemListRow', () => { inherited: false, language: 'de', }; - instance.item = item; + instance.item.set(item); instance.startPageId = 3; fixture.detectChanges(); tick(); @@ -1157,7 +1171,7 @@ describe('ItemListRow', () => { it('adds to favourites when favourite star is clicked', componentTest(() => TestComponent, (fixture, instance) => { const testFolder: Partial = { name: 'item1', path: 'root/item1', globalId: 'itemA', type: 'folder' }; - instance.item = testFolder; + instance.item.set(testFolder); instance.itemsInfo.list = [1]; instance.itemsInfo.total = 1; state.mockState({ favourites: { list: [] } }); @@ -1176,7 +1190,7 @@ describe('ItemListRow', () => { it('removes from favourites when unfavourite star is clicked', componentTest(() => TestComponent, (fixture, instance) => { const testFolder: Partial = { name: 'item1', path: 'root/item1', globalId: 'itemA', type: 'folder' }; - instance.item = testFolder; + instance.item.set(testFolder); instance.itemsInfo.list = [1]; instance.itemsInfo.total = 1; state.mockState({ diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.spec.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.spec.ts index 04897c5385..9e81e8b23e 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.spec.ts @@ -23,7 +23,6 @@ import { mockPipes } from '@gentics/ui-core/testing'; import { cloneDeep } from 'lodash-es'; import { TagEditorHostComponent } from '../..'; import { componentTest, configureComponentTest } from '../../../../testing'; -import { spyOnDynamicallyCreatedComponent } from '../../../../testing/dynamic-components'; import { getExampleEditableTag, getMockedTagEditorContext } from '../../../../testing/test-tag-editor-data.mock'; import { ErrorHandler } from '../../../core/providers/error-handler/error-handler.service'; import { ApplicationStateService } from '../../../state'; @@ -36,9 +35,12 @@ import { ValidationErrorInfoComponent } from '../shared/validation-error-info/va import { TagPropertyEditorHostComponent } from '../tag-property-editor-host/tag-property-editor-host.component'; import { TextTagPropertyEditor } from '../tag-property-editors/text-tag-property-editor/text-tag-property-editor.component'; import { GenticsTagEditorComponent } from './gentics-tag-editor.component'; +import { spyWithOriginalFn } from '../../../../testing/spy-with-original'; describe('GenticsTagEditorComponent', () => { + let resolver: TagPropertyEditorResolverService; + beforeEach(() => { configureComponentTest({ imports: [ @@ -64,10 +66,12 @@ describe('GenticsTagEditorComponent', () => { mockPipes('objTagName'), ], }); + + resolver = TestBed.inject(TagPropertyEditorResolverService); }); it('creates the correct number of tag property editors and initializes them correctly', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); // Create the TagPropertyEditor spies and make sure that the spied methods are called in the right order. @@ -76,41 +80,53 @@ describe('GenticsTagEditorComponent', () => { const writeChangedValuesSpies: jasmine.Spy[] = []; let validatorSpy: jasmine.Spy; let currTagPropertyEditorIndex = 0; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent, TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - if (componentType === TextTagPropertyEditor) { - const index = currTagPropertyEditorIndex; - const origInit = componentInstance.instance.initTagPropertyEditor.bind(componentInstance.instance); - initTagPropEditorSpies.push( - spyOn(componentInstance.instance, 'initTagPropertyEditor').and.callFake((...args: any[]) => { - expect(registerOnChangeSpies[index].calls.count()).toBe(0); - expect(writeChangedValuesSpies[index].calls.count()).toBe(0); - origInit(...args); - }), - ); - const origRegister = componentInstance.instance.registerOnChange.bind(componentInstance.instance); - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callFake((...args: any[]) => { - expect(initTagPropEditorSpies[index].calls.count()).toBe(1); - expect(writeChangedValuesSpies[index].calls.count()).toBe(0); - origRegister(...args); - }), - ); - writeChangedValuesSpies.push( - spyOn(componentInstance.instance, 'writeChangedValues').and.stub(), - ); - ++currTagPropertyEditorIndex; - } - if (componentType === GenticsTagEditorComponent) { - const tagEditor = componentInstance.instance; - const origEditTag = tagEditor.editTag.bind(tagEditor); - spyOn(tagEditor, 'editTag').and.callFake((tag: EditableTag, context: TagEditorContext) => { - validatorSpy = spyOn(context.validator, 'validateTagProperty').and.callThrough(); - return origEditTag(tag, context); - }); - } + + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + const tagEditor = ref.instance; + const origEditTag = tagEditor.editTag.bind(tagEditor); + spyOn(tagEditor, 'editTag').and.callFake((tag: EditableTag, context: TagEditorContext) => { + validatorSpy = spyOn(context.validator, 'validateTagProperty').and.callThrough(); + return origEditTag(tag, context); }); + return ref; + }); + + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + const index = currTagPropertyEditorIndex; + const origInit = ref.instance.initTagPropertyEditor.bind(ref.instance); + initTagPropEditorSpies.push( + spyOn(ref.instance, 'initTagPropertyEditor').and.callFake((...args: any[]) => { + expect(registerOnChangeSpies[index].calls.count()).toBe(0); + expect(writeChangedValuesSpies[index].calls.count()).toBe(0); + origInit(...args); + }), + ); + const origRegister = ref.instance.registerOnChange.bind(ref.instance); + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callFake((...args: any[]) => { + expect(initTagPropEditorSpies[index].calls.count()).toBe(1); + expect(writeChangedValuesSpies[index].calls.count()).toBe(0); + origRegister(...args); + }), + ); + writeChangedValuesSpies.push( + spyOn(ref.instance, 'writeChangedValues').and.stub(), + ); + ++currTagPropertyEditorIndex; + + return ref; + }); + const tag = getMockedTag(); const tagPart0Key = tag.tagType.parts[0].keyword; const tagPart1Key = tag.tagType.parts[1].keyword; @@ -227,16 +243,24 @@ describe('GenticsTagEditorComponent', () => { ); it('editTag() enables the OK button when all mandatory TagProperties have been filled and disables it again when that property is emptied', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const registerOnChangeSpies: jasmine.Spy[] = []; - spyOnDynamicallyCreatedComponent([TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - }); + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + + return ref; + }); const tag = getMockedTag(); const context = getMockedTagEditorContext(tag); @@ -283,16 +307,24 @@ describe('GenticsTagEditorComponent', () => { ); it('throws an error when a TagPropertyEditor tries to edit a non-editable TagProperty', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const registerOnChangeSpies: jasmine.Spy[] = []; - spyOnDynamicallyCreatedComponent([TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - }); + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + + return ref; + }); const tag = getMockedTag(); const context = getMockedTagEditorContext(tag); @@ -315,32 +347,43 @@ describe('GenticsTagEditorComponent', () => { it('correctly validates and communicates changes by one TagPropertyEditor to all TagPropertyEditors ' + 'and editTag() resolves the promise correctly on OK click', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const registerOnChangeSpies: jasmine.Spy[] = []; const writeChangedValuesSpies: jasmine.Spy[] = []; let validatorSpy: jasmine.Spy; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent, TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - if (componentType === TextTagPropertyEditor) { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - writeChangedValuesSpies.push( - spyOn(componentInstance.instance, 'writeChangedValues').and.callThrough(), - ); - } - if (componentType === GenticsTagEditorComponent) { - const tagEditor = componentInstance.instance; - const origEditTag = tagEditor.editTag.bind(tagEditor); - spyOn(tagEditor, 'editTag').and.callFake((tag: EditableTag, context: TagEditorContext) => { - validatorSpy = spyOn(context.validator, 'validateTagProperty').and.callThrough(); - return origEditTag(tag, context); - }); - } + + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + writeChangedValuesSpies.push( + spyOn(ref.instance, 'writeChangedValues').and.callThrough(), + ); + + return ref; + }); + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + const tagEditor = ref.instance; + const origEditTag = tagEditor.editTag.bind(tagEditor); + spyOn(tagEditor, 'editTag').and.callFake((tag: EditableTag, context: TagEditorContext) => { + validatorSpy = spyOn(context.validator, 'validateTagProperty').and.callThrough(); + return origEditTag(tag, context); }); + return ref; + }); + const origTag = getMockedTag(); const context = getMockedTagEditorContext(origTag); const origTagClone = cloneDeep(origTag); @@ -438,32 +481,43 @@ describe('GenticsTagEditorComponent', () => { ); it('does not communicate changes that fail validation, editTag() disables the OK button, and re-enables it when all properties are valid', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const registerOnChangeSpies: jasmine.Spy[] = []; const writeChangedValuesSpies: jasmine.Spy[] = []; let validatorSpy: jasmine.Spy; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent, TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - if (componentType === TextTagPropertyEditor) { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - writeChangedValuesSpies.push( - spyOn(componentInstance.instance, 'writeChangedValues').and.callThrough(), - ); - } - if (componentType === GenticsTagEditorComponent) { - const tagEditor = componentInstance.instance; - const origEditTag = tagEditor.editTag.bind(tagEditor); - spyOn(tagEditor, 'editTag').and.callFake((tag: EditableTag, context: TagEditorContext) => { - validatorSpy = spyOn(context.validator, 'validateTagProperty').and.callThrough(); - return origEditTag(tag, context); - }); - } + + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + writeChangedValuesSpies.push( + spyOn(ref.instance, 'writeChangedValues').and.callThrough(), + ); + + return ref; + }); + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + const tagEditor = ref.instance; + const origEditTag = tagEditor.editTag.bind(tagEditor); + spyOn(tagEditor, 'editTag').and.callFake((tag: EditableTag, context: TagEditorContext) => { + validatorSpy = spyOn(context.validator, 'validateTagProperty').and.callThrough(); + return origEditTag(tag, context); }); + return ref; + }); + const origTag = getMockedTag(); const tagPart0Key = origTag.tagType.parts[0].keyword; const tagPart1Key = origTag.tagType.parts[1].keyword; @@ -622,20 +676,28 @@ describe('GenticsTagEditorComponent', () => { ); it('editTag() rejects the promise correctly when Cancel is clicked', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const registerOnChangeSpies: jasmine.Spy[] = []; const writeChangedValuesSpies: jasmine.Spy[] = []; - spyOnDynamicallyCreatedComponent([TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - writeChangedValuesSpies.push( - spyOn(componentInstance.instance, 'writeChangedValues').and.callThrough(), - ); - }); + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + writeChangedValuesSpies.push( + spyOn(ref.instance, 'writeChangedValues').and.callThrough(), + ); + + return ref; + }); const origTag = getMockedTag(); const context = getMockedTagEditorContext(origTag); @@ -687,7 +749,7 @@ describe('GenticsTagEditorComponent', () => { ); it('prevents an onTagPropertyChanged() infinite loop', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const errorHandler: ErrorHandler = TestBed.inject(ErrorHandler); @@ -695,15 +757,23 @@ describe('GenticsTagEditorComponent', () => { const registerOnChangeSpies: jasmine.Spy[] = []; const writeChangedValuesSpies: jasmine.Spy[] = []; - spyOnDynamicallyCreatedComponent([TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - writeChangedValuesSpies.push( - spyOn(componentInstance.instance, 'writeChangedValues').and.callThrough(), - ); - }); + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + writeChangedValuesSpies.push( + spyOn(ref.instance, 'writeChangedValues').and.callThrough(), + ); + + return ref; + }); const origTag = getMockedTag(); const context = getMockedTagEditorContext(origTag); @@ -751,7 +821,7 @@ describe('GenticsTagEditorComponent', () => { ); it('prevents an onTagPropertyChanged() call during initialization', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const errorHandler: ErrorHandler = TestBed.inject(ErrorHandler); @@ -762,32 +832,40 @@ describe('GenticsTagEditorComponent', () => { // The registerOnChange() method of TagPropertyEditor 2 should trigger onTagPropertyChanged() let currPropEditorIndex = 0; - spyOnDynamicallyCreatedComponent([TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - const index = currPropEditorIndex; - let onChangeSpy = spyOn(componentInstance.instance, 'registerOnChange'); - - const writeChangedValuesSpy = spyOn(componentInstance.instance, 'writeChangedValues').and.callFake(() => { - if (writeChangedValuesSpy.calls.count() > 1) { - fail('onTagPropertyChanged() call should not be allowed during initialization'); - } - }); + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); - if (index !== 2) { - onChangeSpy = onChangeSpy.and.callThrough(); - } else { - onChangeSpy = onChangeSpy.and.callFake((onChangeFn: TagPropertiesChangedFn) => { - const change: Partial = { }; - change[tag.tagType.parts[index].keyword] = { - ...tag.properties[tag.tagType.parts[index].keyword], - stringValue: 'Changed from registerOnChange()', - } as StringTagPartProperty; - onChangeFn(change); - }); + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + const index = currPropEditorIndex; + let onChangeSpy = spyOn(ref.instance, 'registerOnChange'); + + const writeChangedValuesSpy = spyOn(ref.instance, 'writeChangedValues').and.callFake(() => { + if (writeChangedValuesSpy.calls.count() > 1) { + fail('onTagPropertyChanged() call should not be allowed during initialization'); } - ++currPropEditorIndex; }); + if (index !== 2) { + onChangeSpy = onChangeSpy.and.callThrough(); + } else { + onChangeSpy = onChangeSpy.and.callFake((onChangeFn: TagPropertiesChangedFn) => { + const change: Partial = { }; + change[tag.tagType.parts[index].keyword] = { + ...tag.properties[tag.tagType.parts[index].keyword], + stringValue: 'Changed from registerOnChange()', + } as StringTagPartProperty; + onChangeFn(change); + }); + } + ++currPropEditorIndex; + + return ref; + }); + instance.tagEditorHost.editTag(tag, context); fixture.detectChanges(); tick(); @@ -796,7 +874,7 @@ describe('GenticsTagEditorComponent', () => { ); it('catches errors thrown by a TagPropertyEditor during initialization', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { fixture.detectChanges(); const errorHandler: ErrorHandler = TestBed.inject(ErrorHandler); @@ -804,13 +882,21 @@ describe('GenticsTagEditorComponent', () => { const expectedError = new Error('error during init'); let initSpy: jasmine.Spy; - spyOnDynamicallyCreatedComponent([TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - if (!initSpy) { - initSpy = spyOn(componentInstance.instance, 'initTagPropertyEditor').and - .callFake(() => { throw expectedError; }); - } - }); + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + if (!initSpy) { + initSpy = spyOn(ref.instance, 'initTagPropertyEditor').and + .callFake(() => { throw expectedError; }); + } + + return ref; + }); const origTag = getMockedTag(); const context = getMockedTagEditorContext(origTag); @@ -824,7 +910,7 @@ describe('GenticsTagEditorComponent', () => { }), ); - it('editTagLive() calls onTagChangeFn with the current properties after every valid change', componentTest(() => TestComponent, (fixture, instance) => { + it('editTagLive() calls onTagChangeFn with the current properties after every valid change', componentTest(() => TestComponent, async (fixture, instance) => { const origTag = getMockedTag(); const context = getMockedTagEditorContext(origTag); const tagPart0Key = origTag.tagType.parts[0].keyword; @@ -839,23 +925,33 @@ describe('GenticsTagEditorComponent', () => { (propertiesAfterChange1[tagPart2Key] as StringTagPartProperty).stringValue = 'Changed value of tagProperty2'; const registerOnChangeSpies: jasmine.Spy[] = []; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent, TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - if (componentType === TextTagPropertyEditor) { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - } - if (componentType === GenticsTagEditorComponent) { - const tagEditor = componentInstance.instance; - const origEditTagLive = tagEditor.editTagLive.bind(tagEditor); - spyOn(tagEditor, 'editTagLive').and.callFake((tag: EditableTag, context: TagEditorContext, onTagChangeFn: TagChangedFn) => { - spyOn(context.validator, 'validateTagProperty').and.returnValue(getValidationSuccess()); - return origEditTagLive(tag, context, onTagChangeFn); - }); - } + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + + return ref; + }); + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + const tagEditor = ref.instance; + const origEditTagLive = tagEditor.editTagLive.bind(tagEditor); + spyOn(tagEditor, 'editTagLive').and.callFake((tag: EditableTag, context: TagEditorContext, onTagChangeFn: TagChangedFn) => { + spyOn(context.validator, 'validateTagProperty').and.returnValue(getValidationSuccess()); + return origEditTagLive(tag, context, onTagChangeFn); }); + return ref; + }); + const reportedChangedStates: TagPropertyMap[] = []; const onTagChangeHandler: TagChangedFn = (tagProperties) => reportedChangedStates.push(tagProperties); instance.tagEditorHost.editTagLive(origTag, context, onTagChangeHandler); @@ -891,7 +987,7 @@ describe('GenticsTagEditorComponent', () => { expect(reportedChangedStates[1]).toEqual(propertiesAfterChange1); })); - it('editTagLive() calls onTagChangeFn with null after an invalid change', componentTest(() => TestComponent, (fixture, instance) => { + it('editTagLive() calls onTagChangeFn with null after an invalid change', componentTest(() => TestComponent, async (fixture, instance) => { const origTag = getMockedTag(); const context = getMockedTagEditorContext(origTag); const tagPart0Key = origTag.tagType.parts[0].keyword; @@ -907,23 +1003,33 @@ describe('GenticsTagEditorComponent', () => { const registerOnChangeSpies: jasmine.Spy[] = []; let validatorSpy: jasmine.Spy; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent, TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - if (componentType === TextTagPropertyEditor) { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - } - if (componentType === GenticsTagEditorComponent) { - const tagEditor = componentInstance.instance; - const origEditTagLive = tagEditor.editTagLive.bind(tagEditor); - spyOn(tagEditor, 'editTagLive').and.callFake((tag: EditableTag, context: TagEditorContext, onTagChangeFn: TagChangedFn) => { - validatorSpy = spyOn(context.validator, 'validateTagProperty').and.returnValue(getValidationSuccess()); - return origEditTagLive(tag, context, onTagChangeFn); - }); - } + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + + return ref; + }); + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + const tagEditor = ref.instance; + const origEditTagLive = tagEditor.editTagLive.bind(tagEditor); + spyOn(tagEditor, 'editTagLive').and.callFake((tag: EditableTag, context: TagEditorContext, onTagChangeFn: TagChangedFn) => { + validatorSpy = spyOn(context.validator, 'validateTagProperty').and.returnValue(getValidationSuccess()); + return origEditTagLive(tag, context, onTagChangeFn); }); + return ref; + }); + const reportedChangedStates: TagPropertyMap[] = []; const onChangeHandler: TagChangedFn = (tagProperties) => reportedChangedStates.push(tagProperties); instance.tagEditorHost.editTagLive(origTag, context, onChangeHandler); @@ -970,7 +1076,7 @@ describe('GenticsTagEditorComponent', () => { expect(reportedChangedStates[2]).toEqual(propertiesAfterValidChange1); })); - it('editTagLive() does not report changes that did not modify anything', componentTest(() => TestComponent, (fixture, instance) => { + it('editTagLive() does not report changes that did not modify anything', componentTest(() => TestComponent, async (fixture, instance) => { const origTag = getMockedTag(); const context = getMockedTagEditorContext(origTag); const tagPart0Key = origTag.tagType.parts[0].keyword; @@ -983,23 +1089,33 @@ describe('GenticsTagEditorComponent', () => { (propertiesAfterRealChange[tagPart2Key] as StringTagPartProperty).stringValue = 'Changed value of tagProperty2'; const registerOnChangeSpies: jasmine.Spy[] = []; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent, TextTagPropertyEditor], - (componentType, componentInstance: ComponentRef) => { - if (componentType === TextTagPropertyEditor) { - registerOnChangeSpies.push( - spyOn(componentInstance.instance, 'registerOnChange').and.callThrough(), - ); - } - if (componentType === GenticsTagEditorComponent) { - const tagEditor = componentInstance.instance; - const origEditTagLive = tagEditor.editTagLive.bind(tagEditor); - spyOn(tagEditor, 'editTagLive').and.callFake((tag: EditableTag, context: TagEditorContext, onTagChangeFn: TagChangedFn) => { - spyOn(context.validator, 'validateTagProperty').and.returnValue(getValidationSuccess()); - return origEditTagLive(tag, context, onTagChangeFn); - }); - } + spyWithOriginalFn(resolver, 'createPropertyEditor', (original, container, part) => { + const ref = original(container, part); + + // Not the type we look for + if (!(ref.instance instanceof TextTagPropertyEditor)) { + return ref; + } + + registerOnChangeSpies.push( + spyOn(ref.instance, 'registerOnChange').and.callThrough(), + ); + + return ref; + }); + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + const tagEditor = ref.instance; + const origEditTagLive = tagEditor.editTagLive.bind(tagEditor); + spyOn(tagEditor, 'editTagLive').and.callFake((tag: EditableTag, context: TagEditorContext, onTagChangeFn: TagChangedFn) => { + spyOn(context.validator, 'validateTagProperty').and.returnValue(getValidationSuccess()); + return origEditTagLive(tag, context, onTagChangeFn); }); + return ref; + }); + const reportedChangedStates: TagPropertyMap[] = []; const onTagChangeHandler: TagChangedFn = (tagProperties) => reportedChangedStates.push(tagProperties); instance.tagEditorHost.editTagLive(origTag, context, onTagChangeHandler); diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.ts index 55ceea58dd..1399fc28e7 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/components/gentics-tag-editor/gentics-tag-editor.component.ts @@ -33,7 +33,7 @@ import { TagPropertyEditorHostComponent } from '../tag-property-editor-host/tag- templateUrl: './gentics-tag-editor.component.html', styleUrls: ['./gentics-tag-editor.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false + standalone: false, }) export class GenticsTagEditorComponent implements CompleteTagEditor, AfterViewInit, OnDestroy { @@ -108,7 +108,7 @@ export class GenticsTagEditorComponent implements CompleteTagEditor, AfterViewIn ngAfterViewInit(): void { this.subscriptions.add( this.propertyEditorHosts.changes.subscribe((newPropEditorHosts: QueryList) => { - this.tagPropertyEditors = newPropEditorHosts.map(propEditorHost => propEditorHost.tagPropertyEditor); + this.tagPropertyEditors = newPropEditorHosts.map((propEditorHost) => propEditorHost.tagPropertyEditor); this.executeSafely(() => this.setUpPropertyEditors()); }), ); @@ -153,8 +153,8 @@ export class GenticsTagEditorComponent implements CompleteTagEditor, AfterViewIn }, ], }) - .then(modal => modal.open()) - .then(shouldContinue => { + .then((modal) => modal.open()) + .then((shouldContinue) => { if (!shouldContinue) { return; } @@ -185,7 +185,7 @@ export class GenticsTagEditorComponent implements CompleteTagEditor, AfterViewIn this.currentTagState = cloneDeep(tag.properties); this.context = context; this.editableTagParts = this.originalTag.tagType.parts - .filter(tagPart => tagPart.editable && !tagPart.hideInEditor); + .filter((tagPart) => tagPart.editable && !tagPart.hideInEditor); this.changeDetector.markForCheck(); } @@ -218,7 +218,7 @@ export class GenticsTagEditorComponent implements CompleteTagEditor, AfterViewIn const context = this.context.clone(); editor.initTagPropertyEditor(tagPart, tag, tagProperty, context); - editor.registerOnChange(changes => this.onTagPropertyChange(changes, editor)); + editor.registerOnChange((changes) => this.onTagPropertyChange(changes, editor)); } /** @@ -260,7 +260,6 @@ export class GenticsTagEditorComponent implements CompleteTagEditor, AfterViewIn /** * Validates the specified changes, deleting invalid changes, and updates * the currentTagState with the valid changes and latestValidationResults with all validation results. - * * @param changes The changes to be validated. Invalid changes will be deleted from this object. */ private validateChangesAndUpdateTagState(changes: Partial): MultiValidationResult { @@ -346,7 +345,6 @@ export class GenticsTagEditorComponent implements CompleteTagEditor, AfterViewIn /** * Executes the specified function, catching all exceptions * thrown during its execution and passing them to the errorHandler. - * * @returns true if the function was executed successfully, false if it threw an exception */ private executeSafely(fn: () => void): boolean { diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/components/iframe-wrapper/iframe-wrapper.component.spec.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/components/iframe-wrapper/iframe-wrapper.component.spec.ts index 0758dd486d..0744c6248a 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/components/iframe-wrapper/iframe-wrapper.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/components/iframe-wrapper/iframe-wrapper.component.spec.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, QueryList, ViewChild } from '@angular/core'; +import { Component, ElementRef, model, QueryList, ViewChild } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { GenticsUICoreModule } from '@gentics/ui-core'; @@ -54,7 +54,7 @@ describe('IFrameWrapperComponent', () => { const instance = fixture.componentInstance; const iFrameLoadedSpy = spyOn(instance, 'onIFrameLoad').and.stub(); - instance.srcUrl = SRC_URL1; + instance.srcUrl.set(SRC_URL1); fixture.detectChanges(); const iFrame = fixture.debugElement.query(By.css('iframe')); @@ -74,7 +74,7 @@ describe('IFrameWrapperComponent', () => { const instance = fixture.componentInstance; const iFrameLoadedSpy = spyOn(instance, 'onIFrameLoad').and.stub(); - instance.srcUrl = SRC_URL1; + instance.srcUrl.set(SRC_URL1); fixture.detectChanges(); const iFrameElem: HTMLIFrameElement = fixture.debugElement.query(By.css('iframe')).nativeElement; @@ -89,7 +89,7 @@ describe('IFrameWrapperComponent', () => { checkIFrameLoaded(SRC_URL1); iFrameLoadedSpy.calls.reset(); - instance.srcUrl = SRC_URL2; + instance.srcUrl.set(SRC_URL2); fixture.detectChanges(); setTimeout(() => { @@ -108,7 +108,7 @@ describe('IFrameWrapperComponent', () => { const origNgAfterViewInit = instance.iFrameWrapper.ngAfterViewInit.bind(instance.iFrameWrapper); spyOn(instance.iFrameWrapper, 'ngAfterViewInit').and.stub(); - instance.srcUrl = getTestPagePath(1); + instance.srcUrl.set(getTestPagePath(1)); fixture.detectChanges(); const mockedQueryList = mockQueryList(instance.iFrameWrapper); @@ -119,13 +119,13 @@ describe('IFrameWrapperComponent', () => { mockedIFrame.contentDocument.readyState = 'complete'; mockedIFrame.contentWindow.location.href = 'about:blank'; const addEventListenerSpy = spyOn(mockedIFrame, 'addEventListener'); - mockedQueryList.nextChange([ createElmentRef(mockedIFrame as any) ]); + mockedQueryList.nextChange([createElmentRef(mockedIFrame as any)]); expect(iFrameLoadedSpy).not.toHaveBeenCalled(); expect(addEventListenerSpy).toHaveBeenCalled(); // Trigger the load event with the correct page loaded. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const triggerLoadEvent = (addEventListenerSpy.calls.argsFor(0) as any[])[1] as () => void; mockedIFrame.contentWindow.location.href = getTestPageUrl(1); triggerLoadEvent(); @@ -137,14 +137,14 @@ describe('IFrameWrapperComponent', () => { it('changing width works', componentTest(() => TestComponent, (fixture, instance) => { - instance.srcUrl = SRC_URL1; - instance.iFrameWidth = INITIAL_WIDTH; + instance.srcUrl.set(SRC_URL1); + instance.iFrameWidth.set(INITIAL_WIDTH); fixture.detectChanges(); const iFrameElem: HTMLIFrameElement = fixture.debugElement.query(By.css('iframe')).nativeElement; expect(iFrameElem.style.width).toEqual(INITIAL_WIDTH); - instance.iFrameWidth = CHANGED_WIDTH; + instance.iFrameWidth.set(CHANGED_WIDTH); fixture.detectChanges(); expect(iFrameElem.style.width).toBe(CHANGED_WIDTH); }), @@ -152,13 +152,13 @@ describe('IFrameWrapperComponent', () => { it('changing height works', componentTest(() => TestComponent, (fixture, instance) => { - instance.srcUrl = SRC_URL1; + instance.srcUrl.set(SRC_URL1); fixture.detectChanges(); const iFrameElem: HTMLIFrameElement = fixture.debugElement.query(By.css('iframe')).nativeElement; expect(iFrameElem.style.height).toEqual(INITIAL_HEIGHT); - instance.iFrameHeight = CHANGED_HEIGHT; + instance.iFrameHeight.set(CHANGED_HEIGHT); fixture.detectChanges(); expect(iFrameElem.style.height).toBe(CHANGED_HEIGHT); }), @@ -166,11 +166,11 @@ describe('IFrameWrapperComponent', () => { it('changing width and height before setting srcUrl works', componentTest(() => TestComponent, (fixture, instance) => { - instance.iFrameWidth = CHANGED_WIDTH; - instance.iFrameHeight = CHANGED_HEIGHT; + instance.iFrameWidth.set(CHANGED_WIDTH); + instance.iFrameHeight.set(CHANGED_HEIGHT); fixture.detectChanges(); - instance.srcUrl = SRC_URL1; + instance.srcUrl.set(SRC_URL1); fixture.detectChanges(); const iFrameElem: HTMLIFrameElement = fixture.debugElement.query(By.css('iframe')).nativeElement; @@ -195,9 +195,9 @@ const createElmentRef = (element: HTMLElement): ElementRef => ({ template: ` `, standalone: false, @@ -206,9 +206,9 @@ class TestComponent { @ViewChild('iFrameWrapper', { static: true }) iFrameWrapper: IFrameWrapperComponent; - srcUrl: string; - iFrameHeight = INITIAL_HEIGHT; - iFrameWidth: string; + readonly srcUrl = model(); + readonly iFrameHeight = model(INITIAL_HEIGHT); + readonly iFrameWidth = model(); onIFrameLoad(): void { } } diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.spec.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.spec.ts index 8d1e65ec56..3f13f1b349 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.spec.ts @@ -1,5 +1,5 @@ import { Component, ViewChild } from '@angular/core'; -import { tick } from '@angular/core/testing'; +import { TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { I18nService } from '@gentics/cms-components'; import { TagChangedFn, TagEditorResult } from '@gentics/cms-integration-api-models'; @@ -9,7 +9,6 @@ import { mockPipes } from '@gentics/ui-core/testing'; import { cloneDeep } from 'lodash-es'; import { componentTest } from '../../../../testing/component-test'; import { configureComponentTest } from '../../../../testing/configure-component-test'; -import { spyOnDynamicallyCreatedComponent } from '../../../../testing/dynamic-components'; import { getExampleEditableTag, getMockedTagEditorContext } from '../../../../testing/test-tag-editor-data.mock'; import { ErrorHandler } from '../../../core/providers/error-handler/error-handler.service'; import { ApplicationStateService } from '../../../state'; @@ -20,13 +19,18 @@ import { GenticsTagEditorComponent } from '../gentics-tag-editor/gentics-tag-edi import { IFrameWrapperComponent } from '../iframe-wrapper/iframe-wrapper.component'; import { TagPropertyEditorHostComponent } from '../tag-property-editor-host/tag-property-editor-host.component'; import { TagEditorHostComponent } from './tag-editor-host.component'; +import { TagPropertyEditorResolverService } from '../../providers/tag-property-editor-resolver/tag-property-editor-resolver.service'; +import { spyWithOriginalFn } from '../../../../testing/spy-with-original'; describe('TagEditorHostComponent', () => { + let resolver: TagPropertyEditorResolverService; + beforeEach(() => { configureComponentTest({ imports: [GenticsUICoreModule.forRoot()], providers: [ + TagPropertyEditorResolverService, { provide: ErrorHandler, useClass: MockErrorHandlerService }, { provide: ApplicationStateService, useClass: TestApplicationState }, { provide: I18nService, useClass: MockI18nService }, @@ -41,12 +45,14 @@ describe('TagEditorHostComponent', () => { mockPipes('objTagName'), ], }); + + resolver = TestBed.inject(TagPropertyEditorResolverService); }); describe('editTag()', () => { it('creates and shows the GenticsTagEditor and passes on the result\'s resolve(), and destroys the TagEditor again', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { const tag = getExampleEditableTag(); const expectedEditedTag = getExampleEditableTag(); (expectedEditedTag.properties['property0'] as StringTagPartProperty).stringValue = 'modified Value'; @@ -54,10 +60,14 @@ describe('TagEditorHostComponent', () => { let editTagSpy: jasmine.Spy = null; let resolve: (tag: TagEditorResult) => void = null; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent], (componentType, componentInstance) => { - editTagSpy = spyOn(componentInstance.instance, 'editTag').and.returnValue( + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + editTagSpy = spyOn(ref.instance, 'editTag').and.returnValue( new Promise((resolveFn) => resolve = resolveFn), ); + + return ref; }); fixture.detectChanges(); @@ -100,7 +110,7 @@ describe('TagEditorHostComponent', () => { ); it('creates and shows the CustomTagEditorHostComponent and passes on the result\'s resolve(), and destroys the TagEditor again', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { const tag = getExampleEditableTag(); tag.tagType.externalEditorUrl = 'http://localhost/customTagEditor'; const expectedEditedTag = getExampleEditableTag(); @@ -110,15 +120,23 @@ describe('TagEditorHostComponent', () => { let editTagSpy: jasmine.Spy = null; let resolve: (tag: TagEditorResult) => void = null; - spyOnDynamicallyCreatedComponent([CustomTagEditorHostComponent], (componentType, componentInstance) => { - editTagSpy = spyOn(componentInstance.instance, 'editTag').and.returnValue( + spyWithOriginalFn(resolver, 'createCustomTagEditor', (original, container) => { + const ref = original(container); + + editTagSpy = spyOn(ref.instance, 'editTag').and.returnValue( new Promise((resolveFn) => resolve = resolveFn), ); + + return ref; }); fixture.detectChanges(); const result = fixture.componentInstance.tagEditorHost.editTag(tag, context); + tick(); + fixture.detectChanges(); + await fixture.whenRenderingDone(); + // Make sure that the TagEditor's editTag() method has been called appropriately. expect(editTagSpy.calls.argsFor(0)[0]).toEqual(tag); assertTagEditorContextsEqual(context, editTagSpy.calls.argsFor(0)[1]); @@ -156,16 +174,20 @@ describe('TagEditorHostComponent', () => { ); it('creates and shows the GenticsTagEditor, passes on the result\'s reject(), and destroys the TagEditor again', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { const tag = getExampleEditableTag(); const context = getMockedTagEditorContext(tag); let editTagSpy: jasmine.Spy = null; let reject: (error?: any) => void = null; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent], (componentType, componentInstance) => { - editTagSpy = spyOn(componentInstance.instance, 'editTag').and.returnValue( - new Promise((resolveFn, rejectFn) => reject = rejectFn), + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + editTagSpy = spyOn(ref.instance, 'editTag').and.returnValue( + new Promise((_, rejectFn) => reject = rejectFn), ); + + return ref; }); fixture.detectChanges(); @@ -204,7 +226,7 @@ describe('TagEditorHostComponent', () => { ); it('creates and shows the GenticsTagEditor in read-only mode, rejects the promise if the TagEditor resolves it, and destroys the TagEditor again', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { const tag = getExampleEditableTag(); const editedTag = getExampleEditableTag(); (editedTag.properties['property0'] as StringTagPartProperty).stringValue = 'modified Value'; @@ -213,10 +235,14 @@ describe('TagEditorHostComponent', () => { let editTagSpy: jasmine.Spy = null; let resolve: (error?: any) => void = null; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent], (componentType, componentInstance) => { - editTagSpy = spyOn(componentInstance.instance, 'editTag').and.returnValue( - new Promise((resolveFn, rejectFn) => resolve = resolveFn), + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + editTagSpy = spyOn(ref.instance, 'editTag').and.returnValue( + new Promise((resolveFn) => resolve = resolveFn), ); + + return ref; }); fixture.detectChanges(); @@ -281,15 +307,19 @@ describe('TagEditorHostComponent', () => { } it('creates and shows the GenticsTagEditor and passes on onChangeFn calls', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { const tag = getExampleEditableTag(); const context = getMockedTagEditorContext(tag); let editTagLiveSpy: jasmine.Spy = null; let onChangeFn: TagChangedFn; - spyOnDynamicallyCreatedComponent([GenticsTagEditorComponent], (componentType, componentInstance) => { - editTagLiveSpy = spyOn(componentInstance.instance, 'editTagLive').and + spyWithOriginalFn(resolver, 'createGenticsTagEditor', (original, container) => { + const ref = original(container); + + editTagLiveSpy = spyOn(ref.instance, 'editTagLive').and .callFake((tag, context, changeFn) => onChangeFn = changeFn); + + return ref; }); const reportedChangedStates: TagPropertyMap[] = []; @@ -314,16 +344,20 @@ describe('TagEditorHostComponent', () => { ); it('creates and shows the CustomTagEditorHostComponent and passes on onChangeFn calls that pass validation', - componentTest(() => TestComponent, (fixture, instance) => { + componentTest(() => TestComponent, async (fixture, instance) => { const tag = getExampleEditableTag(); tag.tagType.externalEditorUrl = 'http://localhost/customTagEditor'; const context = getMockedTagEditorContext(tag); let editTagLiveSpy: jasmine.Spy = null; let onChangeFn: TagChangedFn; - spyOnDynamicallyCreatedComponent([CustomTagEditorHostComponent], (componentType, componentInstance) => { - editTagLiveSpy = spyOn(componentInstance.instance, 'editTagLive').and + spyWithOriginalFn(resolver, 'createCustomTagEditor', (original, container) => { + const ref = original(container); + + editTagLiveSpy = spyOn(ref.instance, 'editTagLive').and .callFake((tag, context, changeFn) => onChangeFn = changeFn); + + return ref; }); const reportedChangedStates: TagPropertyMap[] = []; diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.ts index 9130cf405d..83e027cf8f 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-editor-host/tag-editor-host.component.ts @@ -2,16 +2,16 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - ComponentFactoryResolver, ComponentRef, Input, OnDestroy, ViewChild, ViewContainerRef, } from '@angular/core'; -import { CompleteTagEditor, TagChangedFn, TagEditorContext, TagEditorError, TagEditorResult } from '@gentics/cms-integration-api-models'; +import { CompleteTagEditor, TagChangedFn, TagEditorContext, TagEditorResult } from '@gentics/cms-integration-api-models'; import { EditableTag } from '@gentics/cms-models'; import { cloneDeep } from 'lodash-es'; +import { TagPropertyEditorResolverService } from '../../providers/tag-property-editor-resolver/tag-property-editor-resolver.service'; import { CustomTagEditorHostComponent } from '../custom-tag-editor-host/custom-tag-editor-host.component'; import { GenticsTagEditorComponent } from '../gentics-tag-editor/gentics-tag-editor.component'; @@ -25,7 +25,7 @@ import { GenticsTagEditorComponent } from '../gentics-tag-editor/gentics-tag-edi templateUrl: './tag-editor-host.component.html', styleUrls: ['./tag-editor-host.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - standalone: false + standalone: false, }) export class TagEditorHostComponent implements OnDestroy, CompleteTagEditor { @@ -39,8 +39,8 @@ export class TagEditorHostComponent implements OnDestroy, CompleteTagEditor { private tagEditorComponent: ComponentRef; constructor( - private componentFactoryResolver: ComponentFactoryResolver, private changeDetector: ChangeDetectorRef, + private resolver: TagPropertyEditorResolverService, ) { } ngOnDestroy(): void { @@ -53,7 +53,6 @@ export class TagEditorHostComponent implements OnDestroy, CompleteTagEditor { * Opens a tag editor for the specified tag. * Based on the configuration of the TagType, either the GenticsTagEditor or * a custom tag editor is used. - * * @param tag The tag to be edited - the property tag.tagType must be set. * @param context The current context. * @returns A promise, which when the user clicks OK, resolves and returns a copy of the edited tag @@ -63,7 +62,7 @@ export class TagEditorHostComponent implements OnDestroy, CompleteTagEditor { const clones = this.initTagEditor(tag, context); return this.tagEditorComponent.instance.editTag(clones.tagClone, clones.contextClone) - .then(editedTag => { + .then((editedTag) => { this.closeTagEditor(); if (!context.readOnly) { return editedTag; @@ -87,7 +86,6 @@ export class TagEditorHostComponent implements OnDestroy, CompleteTagEditor { * Since the GenticsTagEditor and the CustomTagEditorHost both perform validation * before calling onChangeFn, it is guaranteed that is parameter will be a TagPropertyMap * only if all TagProperties are valid, otherwise the parameter will be null. - * * @param tag The tag to be edited - the property tag.tagType must be set. * @param context The current context. * @param onChangeFn This function must be called with the entire `TagPropertyMap` of the tag whenever a change @@ -104,11 +102,10 @@ export class TagEditorHostComponent implements OnDestroy, CompleteTagEditor { /** * Creates either a `GenticsTagEditor` or a `CustomTagEditor`, based on the configuration of the * TagType and returns clones of the tag and the context. - * * @param tag * @param context */ - private initTagEditor(tag: EditableTag, context: TagEditorContext): { tagClone: EditableTag, contextClone: TagEditorContext } { + private initTagEditor(tag: EditableTag, context: TagEditorContext): { tagClone: EditableTag; contextClone: TagEditorContext } { if (this.tagEditorComponent) { this.closeTagEditor(); } @@ -134,23 +131,12 @@ export class TagEditorHostComponent implements OnDestroy, CompleteTagEditor { } private createGenticsTagEditor(): ComponentRef { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(GenticsTagEditorComponent); - if (componentFactory) { - const ref = this.tagEditorContainer.createComponent(componentFactory); - ref.instance.showTitle = this.showTitle; - return ref; - } else { - throw new TagEditorError('Could not resolve ComponentFactory for GenticsTagEditorComponent.'); - } + const ref = this.resolver.createGenticsTagEditor(this.tagEditorContainer); + ref.instance.showTitle = this.showTitle; + return ref; } private createCustomTagEditor(url: string): ComponentRef { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(CustomTagEditorHostComponent); - if (componentFactory) { - return this.tagEditorContainer.createComponent(componentFactory); - } else { - throw new TagEditorError('Could not resolve ComponentFactory for CustomTagEditorHostComponent.'); - } + return this.resolver.createCustomTagEditor(this.tagEditorContainer); } - } diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.spec.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.spec.ts index c7aa176861..57313eeb5b 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.spec.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.spec.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, model } from '@angular/core'; import { TestBed, tick } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; @@ -40,37 +40,37 @@ describe('TagPropertyEditorHostComponent', () => { componentTest(() => TestComponent, (fixture, instance) => { const tagPart = getExampleEditableTag().tagType.parts[0]; const resolverService: TagPropertyEditorResolverService = TestBed.inject(TagPropertyEditorResolverService); - spyOn(resolverService, 'resolveTagPropertyEditorFactory').and.callThrough(); + const resolverSpy = spyOn(resolverService, 'createPropertyEditor').and.callThrough(); expect(tagPart).toBeTruthy(); - expect(instance.tagPart).toBeFalsy(); + expect(instance.tagPart()).toBeFalsy(); fixture.detectChanges(); tick(); const tagPropertyEditorHost = fixture.debugElement.query(By.directive(TagPropertyEditorHostComponent)); expect(( tagPropertyEditorHost.nativeElement).children.length).toBe(0); - instance.tagPart = tagPart; + instance.tagPart.set(tagPart); fixture.detectChanges(); tick(); - expect(resolverService.resolveTagPropertyEditorFactory).toHaveBeenCalledWith(tagPart); + expect(resolverSpy).toHaveBeenCalledTimes(1); + expect(resolverSpy.calls.first().args[1]).toBe(tagPart); expect(fixture.debugElement.query(By.directive(TextTagPropertyEditor))).toBeTruthy(); }), ); - it('properly distroys the TagPropertyEditor component', + it('properly destroys the TagPropertyEditor component', componentTest(() => TestComponent, (fixture, instance) => { const tagPart = getExampleEditableTag().tagType.parts[0]; - const resolverService: TagPropertyEditorResolverService = TestBed.inject(TagPropertyEditorResolverService); expect(tagPart).toBeTruthy(); - expect(instance.tagPart).toBeFalsy(); + expect(instance.tagPart()).toBeFalsy(); fixture.detectChanges(); tick(); const tagPropertyEditorHost = fixture.debugElement.query(By.directive(TagPropertyEditorHostComponent)); expect(( tagPropertyEditorHost.nativeElement).children.length).toBe(0); - instance.tagPart = tagPart; + instance.tagPart.set(tagPart); fixture.detectChanges(); tick(); expect(fixture.debugElement.query(By.directive(TextTagPropertyEditor))).toBeTruthy(); @@ -85,10 +85,10 @@ describe('TagPropertyEditorHostComponent', () => { @Component({ template: ` - + `, standalone: false, }) class TestComponent { - tagPart: TagPart; + readonly tagPart = model(); } diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.ts index b53189aea8..3fb4cfeb43 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/components/tag-property-editor-host/tag-property-editor-host.component.ts @@ -61,10 +61,7 @@ export class TagPropertyEditorHostComponent implements OnDestroy, OnChanges { } private setUpTagPropertyEditor(part: TagPart): void { - const componentFactory = this.tagPropertyEditorResolver.resolveTagPropertyEditorFactory(part); - if (componentFactory) { - this.editorComponent = this.viewContainer.createComponent(componentFactory); - } + this.editorComponent = this.tagPropertyEditorResolver.createPropertyEditor(this.viewContainer, part); } private disposeEditorComponent(): void { diff --git a/cms-ui/apps/editor-ui/src/app/tag-editor/providers/tag-property-editor-resolver/tag-property-editor-resolver.service.ts b/cms-ui/apps/editor-ui/src/app/tag-editor/providers/tag-property-editor-resolver/tag-property-editor-resolver.service.ts index 0d8cd8a4cb..efb2b95c6a 100644 --- a/cms-ui/apps/editor-ui/src/app/tag-editor/providers/tag-property-editor-resolver/tag-property-editor-resolver.service.ts +++ b/cms-ui/apps/editor-ui/src/app/tag-editor/providers/tag-property-editor-resolver/tag-property-editor-resolver.service.ts @@ -1,4 +1,4 @@ -import { ComponentFactory, ComponentFactoryResolver, Injectable, Type } from '@angular/core'; +import { ComponentRef, Injectable, Type, ViewContainerRef } from '@angular/core'; import { TagPropertyEditor } from '@gentics/cms-integration-api-models'; import { TagPart, TagPartType } from '@gentics/cms-models'; import { CustomTagPropertyEditorHostComponent } from '../../components/custom-tag-property-editor-host/custom-tag-property-editor-host.component'; @@ -18,6 +18,8 @@ import { PageUrlTagPropertyEditor } from '../../components/tag-property-editors/ import { SelectTagPropertyEditor } from '../../components/tag-property-editors/select-tag-property-editor/select-tag-property-editor.component'; import { TagRefTagPropertyEditor } from '../../components/tag-property-editors/tagref-tag-property-editor/tagref-tag-property-editor.component'; import { TextTagPropertyEditor } from '../../components/tag-property-editors/text-tag-property-editor/text-tag-property-editor.component'; +import { GenticsTagEditorComponent } from '../../components/gentics-tag-editor/gentics-tag-editor.component'; +import { CustomTagEditorHostComponent } from '../../components/custom-tag-editor-host/custom-tag-editor-host.component'; // Maps the TagPartTypes to their TagPropertyEditor components. const DEFAULT_EDITORS = new Map>(); @@ -62,15 +64,20 @@ DEFAULT_EDITORS.set(TagPartType.Handlebars, TextTagPropertyEditor); /** * Looks up the ComponentFactory for the TagPropertyEditor that is configured for * a particular TagPartType. + * Mainly used as abstraction layer and to make it easier to create stubs/mocks in tests. */ @Injectable() export class TagPropertyEditorResolverService { - private componentFactories = new Map, ComponentFactory>(); + createGenticsTagEditor(container: ViewContainerRef): ComponentRef { + return container.createComponent(GenticsTagEditorComponent); + } - constructor(private componentFactoryResolver: ComponentFactoryResolver) { } + createCustomTagEditor(container: ViewContainerRef): ComponentRef { + return container.createComponent(CustomTagEditorHostComponent); + } - resolveTagPropertyEditorFactory(tagPart: TagPart): ComponentFactory { + createPropertyEditor(container: ViewContainerRef, tagPart: TagPart): ComponentRef | null { let componentType: Type; if (!tagPart.externalEditorUrl) { componentType = DEFAULT_EDITORS.get(tagPart.typeId); @@ -82,12 +89,6 @@ export class TagPropertyEditorResolverService { componentType = CustomTagPropertyEditorHostComponent; } - let componentFactory = this.componentFactories.get(componentType); - if (!componentFactory) { - componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType); - this.componentFactories.set(componentType, componentFactory); - } - return componentFactory; + return container.createComponent(componentType); } - } diff --git a/cms-ui/apps/editor-ui/src/testing/dynamic-components.ts b/cms-ui/apps/editor-ui/src/testing/dynamic-components.ts deleted file mode 100644 index 8ac92764ff..0000000000 --- a/cms-ui/apps/editor-ui/src/testing/dynamic-components.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Type, ComponentRef, ComponentFactoryResolver, ComponentFactory} from '@angular/core'; -import {TestBed} from '@angular/core/testing'; - -/** - * Allows to create a spy on a dynamically created component. - * @param componentTypes The component types on which a spy should be created. - * @param setUpSpyFn The callback function that will set up the spy. - */ -export function spyOnDynamicallyCreatedComponent( - componentTypes: Type[], - setUpSpyFn: (componentType: Type, componentInstance: ComponentRef) => void -): void { - const componentFactoryResolver: ComponentFactoryResolver = TestBed.inject(ComponentFactoryResolver); - const origResolveFn = componentFactoryResolver.resolveComponentFactory.bind(componentFactoryResolver); - - spyOn(componentFactoryResolver, 'resolveComponentFactory').and.callFake((componentType: Type) => { - const componentFactory: ComponentFactory = origResolveFn(componentType); - - // Only spy on component types we are interested in. - if (componentTypes.findIndex(type => type === componentType) !== -1) { - const origCreateFn = componentFactory.create.bind(componentFactory); - spyOn(componentFactory, 'create').and.callFake((...args: any[]) => { - const component: ComponentRef = origCreateFn(...args); - setUpSpyFn(componentType, component); - return component; - }); - } - - return componentFactory; - }); -} diff --git a/cms-ui/apps/editor-ui/src/testing/spy-with-original.ts b/cms-ui/apps/editor-ui/src/testing/spy-with-original.ts new file mode 100644 index 0000000000..b8ed854fe2 --- /dev/null +++ b/cms-ui/apps/editor-ui/src/testing/spy-with-original.ts @@ -0,0 +1,11 @@ +export function spyWithOriginalFn( + obj: T, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + fnName: T[K] extends Function ? K : never, + stub: T[K] extends (...params: infer A) => infer V ? (original: T[K], ...params: A) => V : never, +): void { + const originalFn = obj[fnName]; + spyOn(obj, fnName).and.callFake(((...args) => { + return stub(originalFn, ...args); + }) as any); +} diff --git a/cms-ui/apps/editor-ui/tsconfig.json b/cms-ui/apps/editor-ui/tsconfig.json index 1c4f9a9178..765eea88df 100644 --- a/cms-ui/apps/editor-ui/tsconfig.json +++ b/cms-ui/apps/editor-ui/tsconfig.json @@ -6,7 +6,8 @@ "isolatedModules": true, "emitDecoratorMetadata": false, "module": "preserve", - "types": ["node", "jquery"] + "types": ["node", "jquery"], + "ignoreDeprecations": "6.0" }, "files": [], "include": [], diff --git a/cms-ui/apps/image-editor-demo/project.json b/cms-ui/apps/image-editor-demo/project.json index 98dbb3598a..e647e242a7 100644 --- a/cms-ui/apps/image-editor-demo/project.json +++ b/cms-ui/apps/image-editor-demo/project.json @@ -4,7 +4,7 @@ "projectType": "application", "prefix": "gtx", "sourceRoot": "apps/image-editor-demo/src", - "tags": ["app", "docs", "demo", "angular"], + "tags": ["app", "docs", "demo", "angular", "test:karma"], "implicitDependencies": ["image-editor", "ui-core"], "targets": { "build": { diff --git a/cms-ui/apps/image-editor-demo/src/app/playground.component.html b/cms-ui/apps/image-editor-demo/src/app/playground.component.html index 88f99a91ac..8fbbeed599 100644 --- a/cms-ui/apps/image-editor-demo/src/app/playground.component.html +++ b/cms-ui/apps/image-editor-demo/src/app/playground.component.html @@ -30,9 +30,9 @@

Component Inputs:

- - - + + +
diff --git a/cms-ui/apps/image-editor-demo/src/app/playground.component.ts b/cms-ui/apps/image-editor-demo/src/app/playground.component.ts index c9bb45eb0c..7fa788cf89 100644 --- a/cms-ui/apps/image-editor-demo/src/app/playground.component.ts +++ b/cms-ui/apps/image-editor-demo/src/app/playground.component.ts @@ -1,5 +1,5 @@ import {Component, ChangeDetectorRef} from "@angular/core"; -import {ImageTransformParams, AspectRatio, AspectRatios} from '@gentics/image-editor'; +import {ImageTransformParams, AspectRatio, AspectRatios, UILanguage} from '@gentics/image-editor'; @Component({ selector: 'app', @@ -8,10 +8,10 @@ import {ImageTransformParams, AspectRatio, AspectRatios} from '@gentics/image-ed standalone: false }) export class PlaygroundAppComponent { - language = 'en'; + language: UILanguage = 'en'; sourceImage = 'portrait.jpg'; slowConnection = false; - transformParams: Partial = {}; + transformParams: ImageTransformParams | null = null; disableAspectRatios: AspectRatio[] = []; customAspectRatios: AspectRatio[] = []; disableOriginal = false; diff --git a/cms-ui/apps/image-editor-demo/tsconfig.json b/cms-ui/apps/image-editor-demo/tsconfig.json index 3461bc4c19..605aebb959 100644 --- a/cms-ui/apps/image-editor-demo/tsconfig.json +++ b/cms-ui/apps/image-editor-demo/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "moduleResolution": "bundler", - "module": "preserve" + "module": "preserve", + "ignoreDeprecations": "6.0" }, "references": [ { diff --git a/cms-ui/apps/ui-core-docs/project.json b/cms-ui/apps/ui-core-docs/project.json index 7693382acc..105220977d 100644 --- a/cms-ui/apps/ui-core-docs/project.json +++ b/cms-ui/apps/ui-core-docs/project.json @@ -4,7 +4,7 @@ "projectType": "application", "prefix": "gtx", "sourceRoot": "apps/ui-core-docs/src", - "tags": ["app", "docs", "demo", "angular"], + "tags": ["app", "docs", "demo", "angular", "test:karma"], "targets": { "compile": { "executor": "nx:run-commands", diff --git a/cms-ui/apps/ui-core-docs/src/app/app.component.ts b/cms-ui/apps/ui-core-docs/src/app/app.component.ts index 23c5575646..514a11c599 100644 --- a/cms-ui/apps/ui-core-docs/src/app/app.component.ts +++ b/cms-ui/apps/ui-core-docs/src/app/app.component.ts @@ -35,7 +35,7 @@ export class App implements OnInit, OnDestroy { }); filteredContentItems: any[]; hasContent = false; - splitFocus = 'left'; + splitFocus:'left' | 'right' = 'left'; searchQuery = ''; subscription: Subscription; @@ -94,7 +94,7 @@ export class App implements OnInit, OnDestroy { this.hasContent = false; } - private focusRightPanel(): void { + public focusRightPanel(): void { this.hasContent = true; setTimeout(() => this.splitFocus = 'right'); } diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/accordion-demo/accordion-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/accordion-demo/accordion-demo.component.html index d8958c6a46..fd81056151 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/accordion-demo/accordion-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/accordion-demo/accordion-demo.component.html @@ -16,13 +16,13 @@

Demos


- + Try clicking the title
- + You should not be able to read this @@ -40,13 +40,13 @@

Demos


- + Try clicking the title
- + You should not be able to read this '> diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/breadcrumbs-demo/breadcrumbs-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/breadcrumbs-demo/breadcrumbs-demo.component.html index 276f446fc6..b05666443b 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/breadcrumbs-demo/breadcrumbs-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/breadcrumbs-demo/breadcrumbs-demo.component.html @@ -34,7 +34,7 @@

Demos

{ href: "#", text: "Gentics UI Core" }, { href: "#", text: "Components" }, { href: "#/breadcrumbs", text: "Breadcrumbs" } - ]' disabled> + ]' [disabled]>

The active attribute can also be bound to a boolean property.

@@ -63,7 +63,7 @@

Demos

{ href: "#", text: "Gentics UI Core" }, { href: "#", text: "Components" }, { href: "#/breadcrumbs", text: "Breadcrumbs" } - ]' disabled> + ]' [disabled]> Demos
@@ -15,8 +15,8 @@

Demos

@@ -47,8 +47,8 @@

Demos

Demos
diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/date-time-picker-demo/date-time-picker-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/date-time-picker-demo/date-time-picker-demo.component.html index 85564dbc2a..b16e6a2710 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/date-time-picker-demo/date-time-picker-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/date-time-picker-demo/date-time-picker-demo.component.html @@ -27,7 +27,7 @@

Demos

Demos
diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/dropdown-list-demo/dropdown-list-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/dropdown-list-demo/dropdown-list-demo.component.html index 8952d63deb..8c62b97833 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/dropdown-list-demo/dropdown-list-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/dropdown-list-demo/dropdown-list-demo.component.html @@ -129,7 +129,7 @@

Demos

- + Please Select... @@ -146,7 +146,7 @@

Demos

+ Width = Contents @@ -254,7 +254,7 @@

Demos

Setting `sticky` to "true" will prevent the dropdown from closing when the contents are clicked.

- + Please Select... @@ -271,7 +271,7 @@

Demos

+ Display Items @@ -421,7 +421,7 @@

Demos

A DropdownList may contain arbitrary content, not just lists.

- + Not a list @@ -436,7 +436,7 @@

A Title

Demos
  • {{ rejectedTextFile.name }} {{ rejectedTextFile.type }}
  • } -
    Preventing accidental file drop on the page / browser tab
    -
    This container does not accept files.
    -
    This container does not accept files.
    +
    This container does not accept files.
    +
    This container does not accept files.
    This container does not accept files.
    This container does@if (preventLocal) { not @@ -338,8 +338,8 @@

    Preventing accidental file drop on the page / browser tab

    @@ -411,7 +411,7 @@

    Reacting to the Global Drag/Drop State

    -
    Drag files into the page or drop here to trigger events
    diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/file-picker-demo/file-picker-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/file-picker-demo/file-picker-demo.component.html index ce5605b3d6..3e2a54fbc0 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/file-picker-demo/file-picker-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/file-picker-demo/file-picker-demo.component.html @@ -92,7 +92,7 @@

    Demos

    are passed to the drop area and drop events are passed to the file picker.

    - Demo
    - + add Standalone Tab 1 @@ -138,25 +138,25 @@

    Demo

    - + Disabled tab Never seen content - + Inactive tab This tab is inactive - + Read-only tab This tab is read-only - + Read-only & Inactive tab @@ -251,7 +251,7 @@

    Demo

    - + add Standalone Tab 1 diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.html index 800dde015b..5463bf8acf 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.html @@ -21,9 +21,9 @@

    Demos

    @@ -150,7 +150,7 @@

    Demos

    - +
    name.value: {{ addressForm.get('name').value | json }}
    diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.ts b/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.ts index e147c3cb60..3a6fd1f913 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.ts +++ b/cms-ui/apps/ui-core-docs/src/app/pages/input-demo/input-demo.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { FormControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { IDocumentation } from '../../common/docs'; import { InjectDocumentation } from '../../common/docs-loader'; @@ -26,4 +26,10 @@ export class InputDemoPage { streetName: new UntypedFormControl(''), }), }); + + get streetNameControl(): FormControl { + return this.addressForm + .get('address') + ?.get('streetName') as FormControl; + } } diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/notification-service-demo/notification-service-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/notification-service-demo/notification-service-demo.component.html index 06cc4fe8c9..f17fd43246 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/notification-service-demo/notification-service-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/notification-service-demo/notification-service-demo.component.html @@ -17,7 +17,7 @@

    Demos

    - +
    show diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/range-demo/range-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/range-demo/range-demo.component.html index 1cd94c8a3b..ca97c53b60 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/range-demo/range-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/range-demo/range-demo.component.html @@ -72,17 +72,17 @@

    Demos

    - +
    - +
    diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/select-demo/select-demo.component.html b/cms-ui/apps/ui-core-docs/src/app/pages/select-demo/select-demo.component.html index 5ded6a7cb3..1c38fec172 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/select-demo/select-demo.component.html +++ b/cms-ui/apps/ui-core-docs/src/app/pages/select-demo/select-demo.component.html @@ -56,7 +56,7 @@

    Demos

    - + @for (item of options; track item) { } @@ -69,7 +69,7 @@

    Demos

    + Value: (( clearableSelectVal | json )) @@ -376,7 +376,7 @@

    Demos

    - + @for (item of options; track item) { } @@ -388,7 +388,7 @@

    Demos

    value === true) + .map(([elem]) => elem); } updateSortColumn(columnId: string): void { diff --git a/cms-ui/apps/ui-core-docs/src/app/pages/trable-demo/trable-demo.component.ts b/cms-ui/apps/ui-core-docs/src/app/pages/trable-demo/trable-demo.component.ts index 4d978cbba1..04e5b8aab2 100644 --- a/cms-ui/apps/ui-core-docs/src/app/pages/trable-demo/trable-demo.component.ts +++ b/cms-ui/apps/ui-core-docs/src/app/pages/trable-demo/trable-demo.component.ts @@ -5,6 +5,7 @@ import { TableActionClickEvent, TableColumn, TableRow, + TableSelection, TableSortOrder, TrableRow, TrableRowExpandEvent, @@ -172,8 +173,12 @@ export class TrableDemoPage implements OnInit { this.rebuildRows(); } - updateSelection(selection: string[]): void { - this.selection = selection; + updateSelection(selection: string[] | TableSelection): void { + this.selection = Array.isArray(selection) + ? selection + : Object.entries(selection) + .filter(([_, state]) => state === true) + .map(([id]) => id); } rebuildRows(): void { diff --git a/cms-ui/apps/ui-core-docs/tsconfig.json b/cms-ui/apps/ui-core-docs/tsconfig.json index 02a6d863bd..f46a608390 100644 --- a/cms-ui/apps/ui-core-docs/tsconfig.json +++ b/cms-ui/apps/ui-core-docs/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "moduleResolution": "bundler", - "module": "preserve" + "module": "preserve", + "ignoreDeprecations": "6.0" }, "references": [ { diff --git a/cms-ui/cypress.preset.js b/cms-ui/cypress.preset.js deleted file mode 100644 index 6b09918a98..0000000000 --- a/cms-ui/cypress.preset.js +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/unbound-method */ -/* eslint-disable import/no-nodejs-modules */ -const { resolve } = require('path'); -const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); - -const CYPRESS_TYPE_COMPONENT = 'component'; - -/** - * Mapping for each package to have it's own dedicated port. - * This is used for component-tests, so that they can run in paralell, - * as cypress always attempts to start the tests on 8081, which is in most - * cases already used, or at the very least, once from in the first test. - * All subsequent tests would result in an error where the port is already - * in use, and removes the usefullness of paralellism. - */ -const PORT_MAPPING = { - // Applications - 'admin-ui': 8381, - 'editor-ui': 8382, - 'ct-link-checker': 8383, - // Libraries - 'cms-components': 8481, - 'form-grid': 8482, - 'image-editor': 8483, - 'ui-core': 8484, -}; - -function createReporterOptions(cypressType, type, name, isCI) { - isCI = typeof isCI === 'boolean' ? isCI : false; - - if (!isCI) { - return { - reporter: 'min', - } - } - - return { - // Screenshots are irrelevant on CI - screenshotOnRunFailure: false, - // Setup reporters - reporter: resolve(__dirname, 'node_modules/cypress-multi-reporters'), - reporterOptions: { - reporterEnabled: 'min, mocha-junit-reporter', - mochaJunitReporterReporterOptions: { - mochaFile: resolve(__dirname, `.reports/${type}/${name}/CYPRESS-${cypressType}-report.xml`), - testCaseSwitchClassnameAndName: true, - jenkinsMode: true, - rootSuiteTitle: name, - testsuitesTitle: `UI Cypress ${cypressType} Tests: ${name}`, - jenkinsClassnamePrefix: `ui.${cypressType}.${name}`, - }, - }, - }; -} - -module.exports = { - createComponentTestConfiguration(type, name, isCI, mainConfig) { - const config = createReporterOptions(CYPRESS_TYPE_COMPONENT, type, name, isCI); - config.port = PORT_MAPPING[name] || 8580; - - const mergedConfig = { - ...mainConfig, - ...config, - // Cypress 14+ defaults justInTimeCompile to true (webpack only), which can - // intermittently run 0 tests in CI. Remove this line to opt back in. - justInTimeCompile: false, - }; - - /* - * We have to add the tsconfig-paths plugin here, otherwise our paths for - * packages from this mono-repo won't resolve. - * No idea why we have to to this manually, and why this isn't in NX on default. - */ - mergedConfig.devServer ??= {}; - mergedConfig.devServer.webpackConfig ??= {}; - mergedConfig.devServer.webpackConfig.resolve ??= {}; - mergedConfig.devServer.webpackConfig.resolve.plugins ??= []; - - mergedConfig.devServer.webpackConfig.resolve.plugins.push(new TsconfigPathsPlugin({ - configFile: resolve(__dirname, 'tsconfig.base.json'), - })); - - /* - * Styles have to be defined in here manually, as they are stripped by NX. - */ - mergedConfig.devServer.options ??= {}; - mergedConfig.devServer.options.projectConfig ??= {}; - mergedConfig.devServer.options.projectConfig.buildOptions ??= {}; - mergedConfig.devServer.options.projectConfig.buildOptions.stylePreprocessorOptions ??= {}; - mergedConfig.devServer.options.projectConfig.buildOptions.stylePreprocessorOptions.includePaths ??= []; - - // Don't override the styles in case they *are* provided - if (!mergedConfig.devServer.options.projectConfig.buildOptions.styles) { - mergedConfig.devServer.options.projectConfig.buildOptions.styles = ['apps/component-tests-harness/src/styles.scss']; - } - - // Always setup basic includePaths - mergedConfig.devServer.options.projectConfig.buildOptions.stylePreprocessorOptions.includePaths.push( - resolve(__dirname, "libs",), - resolve(__dirname, "node_modules"), - resolve(__dirname, type, name, 'src/styles'), - ); - - return mergedConfig; - }, -}; diff --git a/cms-ui/cypress.preset.ts b/cms-ui/cypress.preset.ts new file mode 100644 index 0000000000..1c320688c4 --- /dev/null +++ b/cms-ui/cypress.preset.ts @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable import/no-nodejs-modules */ +import { resolve } from 'path'; +import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; + +const CYPRESS_TYPE_COMPONENT = 'component'; + +/** + * Mapping for each package to have it's own dedicated port. + * This is used for component-tests, so that they can run in paralell, + * as cypress always attempts to start the tests on 8081, which is in most + * cases already used, or at the very least, once from in the first test. + * All subsequent tests would result in an error where the port is already + * in use, and removes the usefullness of paralellism. + */ +const PORT_MAPPING: Record = { + // Applications + 'admin-ui': 8381, + 'editor-ui': 8382, + 'ct-link-checker': 8383, + // Libraries + 'cms-components': 8481, + 'form-grid': 8482, + 'image-editor': 8483, + 'ui-core': 8484, +}; + +function createReporterOptions( + cypressType: 'component', + type: 'apps' | 'libs', + name: string, + isCI: boolean, +): Cypress.ConfigOptions { + isCI = typeof isCI === 'boolean' ? isCI : false; + + if (!isCI) { + return { + reporter: 'min', + } + } + + return { + // Screenshots are irrelevant on CI + screenshotOnRunFailure: false, + // Setup reporters + reporter: resolve(__dirname, 'node_modules/cypress-multi-reporters'), + reporterOptions: { + reporterEnabled: 'min, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + mochaFile: resolve(__dirname, `.reports/${type}/${name}/CYPRESS-${cypressType}-report.xml`), + testCaseSwitchClassnameAndName: true, + jenkinsMode: true, + rootSuiteTitle: name, + testsuitesTitle: `UI Cypress ${cypressType} Tests: ${name}`, + jenkinsClassnamePrefix: `ui.${cypressType}.${name}`, + }, + }, + }; +} + +export function createComponentTestConfiguration( + type: 'apps' | 'libs', + name: string, + isCI: boolean, + mainConfig: any, +): Cypress.ResolvedConfigOptions['component'] { + const config = createReporterOptions(CYPRESS_TYPE_COMPONENT, type, name, isCI); + config.port = PORT_MAPPING[name] || 8580; + + const mergedConfig = { + ...mainConfig, + ...config, + // Cypress 14+ defaults justInTimeCompile to true (webpack only), which can + // intermittently run 0 tests in CI. Remove this line to opt back in. + justInTimeCompile: false, + }; + + /* + * We have to add the tsconfig-paths plugin here, otherwise our paths for + * packages from this mono-repo won't resolve. + * No idea why we have to to this manually, and why this isn't in NX on default. + */ + mergedConfig.devServer ??= {}; + mergedConfig.devServer.webpackConfig ??= {}; + mergedConfig.devServer.webpackConfig.resolve ??= {}; + mergedConfig.devServer.webpackConfig.resolve.plugins ??= []; + + mergedConfig.devServer.webpackConfig.resolve.plugins.push(new TsconfigPathsPlugin({ + configFile: resolve(__dirname, 'tsconfig.base.json'), + })); + + /* + * Styles have to be defined in here manually, as they are stripped by NX. + */ + mergedConfig.devServer.options ??= {}; + mergedConfig.devServer.options.projectConfig ??= {}; + mergedConfig.devServer.options.projectConfig.buildOptions ??= {}; + mergedConfig.devServer.options.projectConfig.buildOptions.stylePreprocessorOptions ??= {}; + mergedConfig.devServer.options.projectConfig.buildOptions.stylePreprocessorOptions.includePaths ??= []; + + // Don't override the styles in case they *are* provided + if (!mergedConfig.devServer.options.projectConfig.buildOptions.styles) { + mergedConfig.devServer.options.projectConfig.buildOptions.styles = ['apps/component-tests-harness/src/styles.scss']; + } + + // Always setup basic includePaths + mergedConfig.devServer.options.projectConfig.buildOptions.stylePreprocessorOptions.includePaths.push( + resolve(__dirname, "libs",), + resolve(__dirname, "node_modules"), + resolve(__dirname, type, name, 'src/styles'), + ); + + return mergedConfig; +}; diff --git a/cms-ui/jest.config.ts b/cms-ui/jest.config.ts deleted file mode 100644 index c288652d96..0000000000 --- a/cms-ui/jest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Config } from 'jest'; -import { getJestProjectsAsync } from '@nx/jest'; - -export default async (): Promise => ({ - projects: await getJestProjectsAsync(), - setupFilesAfterEnv: [ - 'jest-extended/all', - ], -}); diff --git a/cms-ui/jest.preset.ts b/cms-ui/jest.preset.ts deleted file mode 100644 index 1955c440ef..0000000000 --- a/cms-ui/jest.preset.ts +++ /dev/null @@ -1,76 +0,0 @@ -import nxPreset from '@nx/jest/preset'; -import { Config } from 'jest'; -import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; - -export function getNxPreset(): Partial { - return nxPreset; -} - -export function createSWCConfig(type: 'apps' | 'libs', name: string, swcFile?: string): Partial { - let swcJestConfig: any; - if (swcFile) { - swcJestConfig = JSON.parse(readFileSync(swcFile, 'utf-8')); - } else { - swcJestConfig = JSON.parse(readFileSync(`${__dirname}/jest.swcrc`, 'utf-8')); - } - - // Reading the SWC compilation config and remove the "exclude" - // for the test files to be compiled by SWC - delete swcJestConfig.exclude; - - // disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. - // If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" - if (swcJestConfig.swcrc === undefined) { - swcJestConfig.swcrc = false; - } - - return { - ...getNxPreset(), - displayName: name, - transform: { - '^.+\\.[tj]s$': ['@swc/jest', swcJestConfig], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - testEnvironment: 'node', - collectCoverage: true, - coverageDirectory: resolve(__dirname, `coverage/${type}/${name}`), - }; -} - -export function createAngularConfig(type: 'apps' | 'libs', name: string): Partial { - return { - ...getNxPreset(), - displayName: name, - setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: resolve(__dirname, `coverage/${type}/${name}`), - transform: { - '^.+\\.(ts|mjs|js|html)$': [ - 'jest-preset-angular', - { - tsconfig: '/tsconfig.spec.json', - stringifyContentPathRegex: '\\.(html|svg)$', - }, - ], - }, - transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], - snapshotSerializers: [ - 'jest-preset-angular/build/serializers/no-ng-attributes', - 'jest-preset-angular/build/serializers/ng-snapshot', - 'jest-preset-angular/build/serializers/html-comment', - ], - }; -} - -export function createCIReporters(type: 'apps' | 'libs', name: string): Partial { - return { - reporters: [ - 'default', - ['jest-junit', { - outputFile: resolve(__dirname, `.reports/${type}/${name}/JEST-report.xml`), - classNameTemplate: 'ui.unit.{displayname}.{classname}', - suiteNameTemplate: 'UI Unit Test {displayName}: {title}', - }], - ], - }; -} diff --git a/cms-ui/libs/aloha-models/tsconfig.json b/cms-ui/libs/aloha-models/tsconfig.json index e97cb76459..ff42b2cecd 100644 --- a/cms-ui/libs/aloha-models/tsconfig.json +++ b/cms-ui/libs/aloha-models/tsconfig.json @@ -1,20 +1,21 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "importHelpers": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "ignoreDeprecations": "6.0" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] } diff --git a/cms-ui/libs/cms-components/cypress/tsconfig.json b/cms-ui/libs/cms-components/cypress/tsconfig.json index 12f174438e..c36c03f3ff 100644 --- a/cms-ui/libs/cms-components/cypress/tsconfig.json +++ b/cms-ui/libs/cms-components/cypress/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "../../dist/out-tsc", "module": "commonjs", "types": ["cypress", "node"], - "sourceMap": false + "sourceMap": false, + "ignoreDeprecations": "6.0" }, "include": [ "**/*.ts", diff --git a/cms-ui/libs/cms-components/project.json b/cms-ui/libs/cms-components/project.json index d9ae230b12..69488a537d 100644 --- a/cms-ui/libs/cms-components/project.json +++ b/cms-ui/libs/cms-components/project.json @@ -4,7 +4,7 @@ "projectType": "library", "prefix": "gtx", "sourceRoot": "libs/cms-components/src", - "tags": ["lib", "angular", "publish"], + "tags": ["lib", "angular", "publish", "test:karma"], "targets": { "build": { "executor": "@nx/angular:ng-packagr-lite", diff --git a/cms-ui/libs/cms-components/src/lib/components/i18n-input/i18n-input.component.spec.ts b/cms-ui/libs/cms-components/src/lib/components/i18n-input/i18n-input.component.spec.ts index 09917e8fcf..03015ae7ce 100644 --- a/cms-ui/libs/cms-components/src/lib/components/i18n-input/i18n-input.component.spec.ts +++ b/cms-ui/libs/cms-components/src/lib/components/i18n-input/i18n-input.component.spec.ts @@ -1,12 +1,13 @@ import { CommonModule } from '@angular/common'; -import { Component, NO_ERRORS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; +import { Component, model, NO_ERRORS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MockI18nPipe } from '@gentics/cms-components/testing'; import { GenticsUICoreModule } from '@gentics/ui-core'; -import { Subscription } from 'rxjs'; +import { distinctUntilChanged, Subscription } from 'rxjs'; import { I18nInputComponent } from './i18n-input.component'; +import { isEqual } from 'lodash-es'; const DEFAULT_LANGUAGE = 'de'; const SECOND_LANGUAGE = 'en'; @@ -63,12 +64,12 @@ describe('I18nInputComponent', () => { tick(); fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(2); + expect(changeSpy).toHaveBeenCalledTimes(1); expect(component.currentValue).toEqual({ [DEFAULT_LANGUAGE]: firstValue }); // -------------------- - component.activeLanguage = SECOND_LANGUAGE; + component.activeLanguage.set(SECOND_LANGUAGE); tick(); fixture.detectChanges(); changeSpy.calls.reset(); @@ -82,7 +83,7 @@ describe('I18nInputComponent', () => { tick(); fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(2); + expect(changeSpy).toHaveBeenCalledTimes(1); expect(component.currentValue).toEqual({ [DEFAULT_LANGUAGE]: firstValue, [SECOND_LANGUAGE]: secondValue, @@ -113,7 +114,7 @@ describe('I18nInputComponent', () => { tick(); fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(2); + expect(changeSpy).toHaveBeenCalledTimes(1); expect(component.currentValue).toEqual({ [DEFAULT_LANGUAGE]: newValue, [SECOND_LANGUAGE]: secondValue, @@ -130,16 +131,16 @@ describe('I18nInputComponent', () => { template: ` `, standalone: false, }) class TestComponent implements OnInit, OnDestroy { public control: FormControl; - public activeLanguage = DEFAULT_LANGUAGE; - public availableLanguages = AVAILABLE_LANGUAGES; + public readonly activeLanguage = model(DEFAULT_LANGUAGE); + public readonly availableLanguages = model(AVAILABLE_LANGUAGES); public currentValue: Record; @@ -147,7 +148,9 @@ class TestComponent implements OnInit, OnDestroy { ngOnInit(): void { this.control = new FormControl(); - this.subscription = this.control.valueChanges.subscribe((value) => { + this.subscription = this.control.valueChanges.pipe( + distinctUntilChanged(isEqual), + ).subscribe((value) => { this.valueChangeHandler(value); }); } diff --git a/cms-ui/libs/cms-components/src/lib/components/i18n-select/i18n-select.component.spec.ts b/cms-ui/libs/cms-components/src/lib/components/i18n-select/i18n-select.component.spec.ts index dc355f735c..15a24b63db 100644 --- a/cms-ui/libs/cms-components/src/lib/components/i18n-select/i18n-select.component.spec.ts +++ b/cms-ui/libs/cms-components/src/lib/components/i18n-select/i18n-select.component.spec.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, NO_ERRORS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; +import { Component, model, NO_ERRORS_SCHEMA, OnDestroy, OnInit } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -44,12 +44,12 @@ describe('I18nSelectComponent', () => { fixture.detectChanges(); await fixture.whenRenderingDone(); await fixture.whenRenderingDone(); - selectComponent = fixture.debugElement.children[0].children[0].componentInstance + selectComponent = fixture.debugElement.children[0].children[0].componentInstance; }); it('should trigger a change when the value has been changed', fakeAsync(() => { const options = ['Hello World', 'Example 123', 'Foo Bar']; - component.options = options; + component.options.set(options); fixture.detectChanges(); tick(); @@ -62,12 +62,12 @@ describe('I18nSelectComponent', () => { tick(); fixture.detectChanges(); - expect(changeSpy).toHaveBeenCalledTimes(1); + expect(changeSpy).toHaveBeenCalledTimes(1); // FIXME: expect(component.currentValue).toEqual({ [DEFAULT_LANGUAGE]: options[0] }); // -------------------- - component.activeLanguage = SECOND_LANGUAGE; + component.activeLanguage.set(SECOND_LANGUAGE); tick(); fixture.detectChanges(); changeSpy.calls.reset(); @@ -93,7 +93,7 @@ describe('I18nSelectComponent', () => { [SECOND_LANGUAGE]: options[1], }; - component.options = options; + component.options.set(options); component.control.setValue(initialValue); // Needs two ticks, one for the observable, and one for the setTimeout in the subscription handler tick(); @@ -127,10 +127,12 @@ describe('I18nSelectComponent', () => { template: ` - {{ opt }} + @for (opt of options(); track opt) { + {{ opt }} + } `, @@ -139,17 +141,17 @@ describe('I18nSelectComponent', () => { class TestComponent implements OnInit, OnDestroy { public control: FormControl; - public activeLanguage = DEFAULT_LANGUAGE; - public availableLanguages = AVAILABLE_LANGUAGES; + public readonly activeLanguage = model(DEFAULT_LANGUAGE); + public readonly availableLanguages = model(AVAILABLE_LANGUAGES); public currentValue: Record; - public options: string[] = []; + public readonly options = model([]); private subscription: Subscription; ngOnInit(): void { this.control = new FormControl(); - this.subscription = this.control.valueChanges.subscribe(value => { + this.subscription = this.control.valueChanges.subscribe((value) => { this.valueChangeHandler(value); }); } diff --git a/cms-ui/libs/cms-components/testing/src/lib/dynamic-components.ts b/cms-ui/libs/cms-components/testing/src/lib/dynamic-components.ts deleted file mode 100644 index e5516e24be..0000000000 --- a/cms-ui/libs/cms-components/testing/src/lib/dynamic-components.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ComponentFactory, ComponentFactoryResolver, ComponentRef, Type } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; - -/** - * Allows to create a spy on a dynamically created component. - * @param componentTypes The component types on which a spy should be created. - * @param setUpSpyFn The callback function that will set up the spy. - */ -export function spyOnDynamicallyCreatedComponent( - componentTypes: Type[], - setUpSpyFn: (componentType: Type, componentInstance: ComponentRef) => void, -): void { - const componentFactoryResolver: ComponentFactoryResolver = TestBed.inject(ComponentFactoryResolver); - const origResolveFn = componentFactoryResolver.resolveComponentFactory.bind(componentFactoryResolver); - - spyOn(componentFactoryResolver, 'resolveComponentFactory').and.callFake((componentType: Type) => { - const componentFactory: ComponentFactory = origResolveFn(componentType); - - // Only spy on component types we are interested in. - if (componentTypes.findIndex(type => type === componentType) !== -1) { - const origCreateFn = componentFactory.create.bind(componentFactory); - spyOn(componentFactory, 'create').and.callFake((...args: any[]) => { - const component: ComponentRef = origCreateFn(...args); - setUpSpyFn(componentType, component); - return component; - }); - } - - return componentFactory; - }); -} diff --git a/cms-ui/libs/cms-components/testing/src/lib/index.ts b/cms-ui/libs/cms-components/testing/src/lib/index.ts index cfee7587aa..5b575d042d 100644 --- a/cms-ui/libs/cms-components/testing/src/lib/index.ts +++ b/cms-ui/libs/cms-components/testing/src/lib/index.ts @@ -1,4 +1,3 @@ export * from './configure-component-test'; -export * from './dynamic-components'; export * from './mocks'; export * from './utils'; diff --git a/cms-ui/libs/cms-components/tsconfig.json b/cms-ui/libs/cms-components/tsconfig.json index f6f8a9022c..22e7b04eff 100644 --- a/cms-ui/libs/cms-components/tsconfig.json +++ b/cms-ui/libs/cms-components/tsconfig.json @@ -8,7 +8,8 @@ "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": false, "noFallthroughCasesInSwitch": true, - "module": "preserve" + "module": "preserve", + "ignoreDeprecations": "6.0" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/cms-ui/libs/cms-integration-api-models/tsconfig.json b/cms-ui/libs/cms-integration-api-models/tsconfig.json index a17ac23a1d..ff42b2cecd 100644 --- a/cms-ui/libs/cms-integration-api-models/tsconfig.json +++ b/cms-ui/libs/cms-integration-api-models/tsconfig.json @@ -8,7 +8,8 @@ "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true + "noPropertyAccessFromIndexSignature": true, + "ignoreDeprecations": "6.0" }, "files": [], "include": [], diff --git a/cms-ui/libs/cms-models/.swcrc b/cms-ui/libs/cms-models/.swcrc new file mode 100644 index 0000000000..48e979e424 --- /dev/null +++ b/cms-ui/libs/cms-models/.swcrc @@ -0,0 +1,30 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [ + "jest.config.[ct]s", + "jest.config.ci.[ct]s", + ".*\\.spec.tsx?$", + ".*\\.test.tsx?$", + "./src/jest-setup.ts$", + "./**/jest-setup.ts$", + ".*.js$" + ] +} diff --git a/cms-ui/libs/cms-models/tsconfig.json b/cms-ui/libs/cms-models/tsconfig.json index e97cb76459..ff42b2cecd 100644 --- a/cms-ui/libs/cms-models/tsconfig.json +++ b/cms-ui/libs/cms-models/tsconfig.json @@ -1,20 +1,21 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "importHelpers": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - } - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "ignoreDeprecations": "6.0" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] } diff --git a/cms-ui/libs/cms-rest-client-angular/jest.config.ci.ts b/cms-ui/libs/cms-rest-client-angular/jest.config.ci.ts deleted file mode 100644 index e05a37e51a..0000000000 --- a/cms-ui/libs/cms-rest-client-angular/jest.config.ci.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCIReporters } from '../../jest.preset'; -import config from './jest.config'; - -export default { - ...config, - ...createCIReporters('libs', 'cms-rest-client-angular'), -}; diff --git a/cms-ui/libs/cms-rest-client-angular/jest.config.ts b/cms-ui/libs/cms-rest-client-angular/jest.config.ts deleted file mode 100644 index 1f6c04d78c..0000000000 --- a/cms-ui/libs/cms-rest-client-angular/jest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'jest'; -import { createAngularConfig } from '../../jest.preset'; - -export default defineConfig({ - ...createAngularConfig('libs', 'cms-rest-client-angular'), -}); diff --git a/cms-ui/libs/cms-rest-client-angular/project.json b/cms-ui/libs/cms-rest-client-angular/project.json index 467b1e5070..f6697b5713 100644 --- a/cms-ui/libs/cms-rest-client-angular/project.json +++ b/cms-ui/libs/cms-rest-client-angular/project.json @@ -4,31 +4,11 @@ "sourceRoot": "libs/cms-rest-client-angular/src", "prefix": "lib", "projectType": "library", - "tags": ["lib", "angular", "publish"], + "tags": ["lib", "angular", "publish", "test:vitest"], "targets": { - "test": { - "executor": "@nx/jest:jest", - "outputs": [ - "{workspaceRoot}/.reports/{projectRoot}/JEST-report.xml", - "{workspaceRoot}/coverage/{projectRoot}" - ], - "options": { - "jestConfig": "{projectRoot}/jest.config.ts" - }, - "configurations": { - "watch": { - "watch": true - }, - "ci": { - "color": false, - "colors": false, - "ci": true, - "jestConfig": "{projectRoot}/jest.config.ci.ts" - } - } - }, "build": { "executor": "@nx/angular:ng-packagr-lite", + "defaultConfiguration": "production", "options": { "tsConfig": "libs/cms-rest-client-angular/tsconfig.lib.json", "project": "libs/cms-rest-client-angular/ng-package.json" @@ -38,11 +18,13 @@ "tsConfig": "libs/cms-rest-client-angular/tsconfig.lib.prod.json" }, "development": {} - }, - "defaultConfiguration": "production" + } }, "lint": { "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/angular:unit-test" } } } diff --git a/cms-ui/libs/cms-rest-client-angular/src/lib/angular-cms-client-driver.spec.ts b/cms-ui/libs/cms-rest-client-angular/src/lib/angular-cms-client-driver.spec.ts new file mode 100644 index 0000000000..8a85933cf9 --- /dev/null +++ b/cms-ui/libs/cms-rest-client-angular/src/lib/angular-cms-client-driver.spec.ts @@ -0,0 +1,48 @@ +import { HttpResponse, provideHttpClient, withInterceptors } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { ResponseCode, UserListResponse } from '@gentics/cms-models'; +import { of } from 'rxjs'; +import { GCMSRestClientModule } from './cms-rest-client.module'; +import { GCMSRestClientService } from './cms-rest-client.service'; + +it('should handle the response correctly', async () => { + let requestCounter = 0; + const RESPONSE: UserListResponse = { + responseInfo: { + responseCode: ResponseCode.OK, + }, + messages: [], + hasMoreItems: false, + numItems: 0, + items: [], + }; + + TestBed.configureTestingModule({ + imports: [ + GCMSRestClientModule, + ], + providers: [ + provideHttpClient( + withInterceptors([ + (req) => { + requestCounter++; + + return of(new HttpResponse({ + status: 200, + statusText: 'OK', + url: req.url, + body: RESPONSE, + })); + }, + ]), + ), + ], + }); + + const client = TestBed.inject(GCMSRestClientService); + const request = client.user.list(); + const res = await request.toPromise(); + + expect(requestCounter).toEqual(1); + expect(res).toEqual(RESPONSE); +}); diff --git a/cms-ui/libs/cms-rest-client-angular/src/test-setup.ts b/cms-ui/libs/cms-rest-client-angular/src/test-setup.ts deleted file mode 100644 index bed175f000..0000000000 --- a/cms-ui/libs/cms-rest-client-angular/src/test-setup.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; - -setupZonelessTestEnv({ - errorOnUnknownElements: true, - errorOnUnknownProperties: true, -}); diff --git a/cms-ui/libs/cms-rest-client-angular/tsconfig.json b/cms-ui/libs/cms-rest-client-angular/tsconfig.json index 5256a940ee..63410cb5a0 100644 --- a/cms-ui/libs/cms-rest-client-angular/tsconfig.json +++ b/cms-ui/libs/cms-rest-client-angular/tsconfig.json @@ -8,7 +8,8 @@ "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "module": "preserve" + "module": "preserve", + "ignoreDeprecations": "6.0" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/cms-ui/libs/cms-rest-client-angular/tsconfig.lib.json b/cms-ui/libs/cms-rest-client-angular/tsconfig.lib.json index 34c7d1bd82..cbbdfd6949 100644 --- a/cms-ui/libs/cms-rest-client-angular/tsconfig.lib.json +++ b/cms-ui/libs/cms-rest-client-angular/tsconfig.lib.json @@ -11,10 +11,11 @@ "exclude": [ "src/**/*.spec.ts", "src/**/*.test.ts", - "jest.config.ts", - "jest.config.cts", - "src/test-setup.ts", "testing/src/**/*.spec.ts", - "testing/src/**/*.test.ts" + "testing/src/**/*.test.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts" ] } diff --git a/cms-ui/libs/cms-rest-client-angular/tsconfig.spec.json b/cms-ui/libs/cms-rest-client-angular/tsconfig.spec.json index cc26179588..936c2767ee 100644 --- a/cms-ui/libs/cms-rest-client-angular/tsconfig.spec.json +++ b/cms-ui/libs/cms-rest-client-angular/tsconfig.spec.json @@ -2,16 +2,20 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "target": "es2016", - "types": ["jest", "node"], - "moduleResolution": "node10" + "types": [ + "vitest/globals" + ] + }, + "angularCompilerOptions": { + "compilationMode": "full" }, - "files": ["src/test-setup.ts"], "include": [ - "jest.config.ts", + "src/**/*.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", "src/**/*.test.ts", - "src/**/*.spec.ts", "src/**/*.d.ts" ] } diff --git a/cms-ui/libs/cms-rest-client-angular/vite.config.ci.mts b/cms-ui/libs/cms-rest-client-angular/vite.config.ci.mts new file mode 100644 index 0000000000..c2e349d9fc --- /dev/null +++ b/cms-ui/libs/cms-rest-client-angular/vite.config.ci.mts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from 'vite'; +import { createProjectConfiguration, getPlugins } from '../../vitest.project'; + +export default defineConfig(() => ({ + root: __dirname, + ...createProjectConfiguration('libs', 'cms-rest-client-angular', 'ci'), + plugins: getPlugins('angular'), +})); diff --git a/cms-ui/libs/cms-rest-client-angular/vite.config.mts b/cms-ui/libs/cms-rest-client-angular/vite.config.mts new file mode 100644 index 0000000000..9fc9ee4a8f --- /dev/null +++ b/cms-ui/libs/cms-rest-client-angular/vite.config.mts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from 'vite'; +import { createProjectConfiguration, getPlugins } from '../../vitest.project'; + +export default defineConfig(() => ({ + root: __dirname, + ...createProjectConfiguration('libs', 'cms-rest-client-angular'), + plugins: getPlugins('angular'), +})); diff --git a/cms-ui/jest.swcrc b/cms-ui/libs/cms-rest-client/.swcrc similarity index 100% rename from cms-ui/jest.swcrc rename to cms-ui/libs/cms-rest-client/.swcrc diff --git a/cms-ui/libs/cms-rest-client/jest.config.ci.ts b/cms-ui/libs/cms-rest-client/jest.config.ci.ts deleted file mode 100644 index cfe9e82e00..0000000000 --- a/cms-ui/libs/cms-rest-client/jest.config.ci.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCIReporters } from '../../jest.preset'; -import config from './jest.config'; - -export default { - ...config, - ...createCIReporters('libs', 'cms-rest-client'), -}; diff --git a/cms-ui/libs/cms-rest-client/jest.config.ts b/cms-ui/libs/cms-rest-client/jest.config.ts deleted file mode 100644 index 9a42162e37..0000000000 --- a/cms-ui/libs/cms-rest-client/jest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'jest'; -import { createSWCConfig } from '../../jest.preset'; - -export default defineConfig({ - ...createSWCConfig('libs', 'cms-rest-client'), -}); diff --git a/cms-ui/libs/cms-rest-client/project.json b/cms-ui/libs/cms-rest-client/project.json index 5a9fddbdf4..9fe61f59bb 100644 --- a/cms-ui/libs/cms-rest-client/project.json +++ b/cms-ui/libs/cms-rest-client/project.json @@ -3,31 +3,6 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/cms-rest-client/src", "projectType": "library", - "tags": [ - "lib", - "publish" - ], - "targets": { - "test": { - "executor": "@nx/jest:jest", - "outputs": [ - "{workspaceRoot}/.reports/{projectRoot}/JEST-report.xml", - "{workspaceRoot}/coverage/{projectRoot}" - ], - "options": { - "jestConfig": "{projectRoot}/jest.config.ts" - }, - "configurations": { - "watch": { - "watch": true - }, - "ci": { - "color": false, - "colors": false, - "ci": true, - "jestConfig": "{projectRoot}/jest.config.ci.ts" - } - } - } - } + "tags": ["lib", "publish", "test:vitest"], + "targets": {} } diff --git a/cms-ui/libs/cms-rest-client/src/lib/drivers/fetch-driver.spec.ts b/cms-ui/libs/cms-rest-client/src/lib/drivers/fetch-driver.spec.ts index f105e182c7..cb78ab67ad 100644 --- a/cms-ui/libs/cms-rest-client/src/lib/drivers/fetch-driver.spec.ts +++ b/cms-ui/libs/cms-rest-client/src/lib/drivers/fetch-driver.spec.ts @@ -2,224 +2,201 @@ import { Response as GCMSResponse, ResponseCode } from '@gentics/cms-models'; import { GCMSRestClientAbortError, GCMSRestClientRequestError } from '../errors'; import { RequestMethod } from '../models'; import { GCMSFetchDriver } from './fetch-driver'; -import 'jest-extended'; -describe('FetchDriver', () => { +/* Safe and restore the original fetch implementation in the runs */ - /* Safe and restore the original fetch imlementation in the runs */ +let originalFetch: typeof global.fetch; - let originalFetch: typeof global.fetch; +beforeEach(() => { + originalFetch = global.fetch; +}); - beforeEach(() => { - originalFetch = global.fetch; - }); +afterEach(() => { + global.fetch = originalFetch; +}); + +it('should execute a created request only once, and return the same value', async () => { + const driver = new GCMSFetchDriver(); + const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; + const STATUS_MSG = 'Ok'; + const STATUS_CODE = 200; + const RESPONSE_DATA: GCMSResponse = { + responseInfo: { + responseCode: ResponseCode.OK, + responseMessage: 'Success', + }, + messages: [], + }; + + let execCounter = 0; + + global.fetch = vitest.fn(() => { + execCounter++; + + return Promise.resolve>({ + status: STATUS_CODE, + statusText: STATUS_MSG, + ok: STATUS_CODE < 400, + headers: new Headers(), + text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), + json: () => Promise.resolve(RESPONSE_DATA), + }); + }) as any; - afterEach(() => { - global.fetch = originalFetch; + const req = driver.performMappedRequest({ + headers: {}, + method: RequestMethod.GET, + url: REQUEST_URL, + params: {}, }); - it('should execute a created request only once, and return the same value', async () => { - const driver = new GCMSFetchDriver(); - const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; - const STATUS_MSG = 'Ok'; - const STATUS_CODE = 200; - const RESPONSE_DATA: GCMSResponse = { - responseInfo: { - responseCode: ResponseCode.OK, - responseMessage: 'Success', - }, - messages: [], - }; - - let execCounter = 0; - - global.fetch = jest.fn(() => { - execCounter++; - - return Promise.resolve>({ - status: STATUS_CODE, - statusText: STATUS_MSG, - ok: STATUS_CODE < 400, - headers: new Headers(), - text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), - json: () => Promise.resolve(RESPONSE_DATA), - }); - }) as any; + const [res1, res2, res3] = await Promise.all([ + req.send(), + req.send(), + req.send(), + ]); + + expect(execCounter).toEqual(1); + expect(res1).toBe(res2); + expect(res1).toBe(res3); + expect(res2).toBe(res3); +}); + +it('should return a proper error on an error response', async () => { + const driver = new GCMSFetchDriver(); + const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; + const STATUS_MSG = 'Invalid'; + const STATUS_CODE = 400; + const RESPONSE_DATA: GCMSResponse = { + responseInfo: { + responseCode: ResponseCode.INVALID_DATA, + responseMessage: 'Invalid Data sent', + }, + messages: [], + }; + + global.fetch = vitest.fn(() => { + return Promise.resolve>({ + status: STATUS_CODE, + statusText: STATUS_MSG, + ok: STATUS_CODE < 400, + headers: new Headers(), + text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), + json: () => Promise.resolve(RESPONSE_DATA), + }); + }) as any; - const req = driver.performMappedRequest({ + await expect(function () { + return driver.performMappedRequest({ headers: {}, method: RequestMethod.GET, url: REQUEST_URL, params: {}, - }); - - const [res1, res2, res3] = await Promise.all([ - req.send(), - req.send(), - req.send(), - ]); - - expect(execCounter).toEqual(1); - expect(res1).toBe(res2); - expect(res1).toBe(res3); - expect(res2).toBe(res3); - }); - - it('should return a proper error on an error response', async () => { - const driver = new GCMSFetchDriver(); - const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; - const STATUS_MSG = 'Invalid'; - const STATUS_CODE = 400; - const RESPONSE_DATA: GCMSResponse = { - responseInfo: { - responseCode: ResponseCode.INVALID_DATA, - responseMessage: 'Invalid Data sent', - }, - messages: [], - }; - - global.fetch = jest.fn(() => { - return Promise.resolve>({ - status: STATUS_CODE, - statusText: STATUS_MSG, - ok: STATUS_CODE < 400, - headers: new Headers(), - text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), - json: () => Promise.resolve(RESPONSE_DATA), - }); - }) as any; - - try { - await driver.performMappedRequest({ - headers: {}, - method: RequestMethod.GET, - url: REQUEST_URL, - params: {}, - }).send(); - expect.fail('Should not resolve!'); - } catch (err) { - // Don't use `toBeInstanceOf`, doesn't work! - expect(err instanceof GCMSRestClientRequestError).toEqual(true); - expect(err).toMatchObject>({ - responseCode: STATUS_CODE, - rawBody: JSON.stringify(RESPONSE_DATA), - data: RESPONSE_DATA, - }); - } - }); + }).send(); + }).rejects.toThrowErrorMatchingInlineSnapshot('[Error: Request "GET http://localhost:8080/rest/nowhere" responded with error code 400: "Invalid"]'); +}); - /* +/* * Sometimes some older Endpoints will *always* return a 200, where we have to find out * that the `responseInfo` actually tells us otherwise. * The driver/client should properly detect this and give us a proper error response. */ - it('should return a proper error on a HTTP success response', async () => { - const driver = new GCMSFetchDriver(); - const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; - const STATUS_MSG = 'Invalid'; - const STATUS_CODE = 400; - const RESPONSE_DATA: GCMSResponse = { - responseInfo: { - responseCode: ResponseCode.INVALID_DATA, - responseMessage: 'Invalid Data sent', - }, - messages: [], - }; - - global.fetch = jest.fn(() => { - return Promise.resolve>({ - status: STATUS_CODE, - statusText: STATUS_MSG, - ok: STATUS_CODE < 400, - headers: new Headers(), - text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), - json: () => Promise.resolve(RESPONSE_DATA), - }); - }) as any; - - try { - await driver.performMappedRequest({ - headers: {}, - method: RequestMethod.GET, - url: REQUEST_URL, - params: {}, - }).send(); - expect.fail('Should not resolve!'); - } catch (err) { - // Don't use `toBeInstanceOf`, doesn't work! - expect(err instanceof GCMSRestClientRequestError).toEqual(true); - expect(err).toMatchObject>({ - responseCode: STATUS_CODE, - rawBody: JSON.stringify(RESPONSE_DATA), - data: RESPONSE_DATA, - }); - expect((err as GCMSRestClientRequestError).message).toEqual(`Request "GET ${REQUEST_URL}" responded with error code ${STATUS_CODE}: "${STATUS_MSG}"`); - } - }); - - it('should cancel the request when told to do so', async () => { - const driver = new GCMSFetchDriver(); - const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; - const STATUS_MSG = 'Ok'; - const STATUS_CODE = 200; - const RESPONSE_DATA: GCMSResponse = { - responseInfo: { - responseCode: ResponseCode.OK, - responseMessage: 'Success', - }, - messages: [], - }; - - global.fetch = jest.fn((args) => { - if (typeof args === 'string') { - args = { url: args }; - } else if (args instanceof URL) { - args = { url: args.toString() }; - } - const signal = (args as RequestInit).signal; - - return new Promise>((resolve, reject) => { - let aborted = false; - - setTimeout(() => { - if (aborted) { - return; - } - - resolve({ - status: STATUS_CODE, - statusText: STATUS_MSG, - ok: STATUS_CODE < 400, - headers: new Headers(), - text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), - json: () => Promise.resolve(RESPONSE_DATA), - }); - }, 1_000); - - signal.addEventListener('abort', () => { - aborted = true; // 🤘 - reject(signal.reason); - }); - }); - }) as any; +it('should return a proper error on a HTTP success response', async () => { + const driver = new GCMSFetchDriver(); + const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; + const STATUS_MSG = 'OK'; + const STATUS_CODE = 200; + const RESPONSE_DATA: GCMSResponse = { + responseInfo: { + responseCode: ResponseCode.INVALID_DATA, + responseMessage: 'Invalid Data sent', + }, + messages: [], + }; + + global.fetch = vitest.fn(() => { + return Promise.resolve>({ + status: STATUS_CODE, + statusText: STATUS_MSG, + ok: STATUS_CODE < 400, + headers: new Headers(), + text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), + json: () => Promise.resolve(RESPONSE_DATA), + }); + }) as any; - const req = driver.performMappedRequest({ + await expect(function () { + return driver.performMappedRequest({ headers: {}, method: RequestMethod.GET, url: REQUEST_URL, params: {}, - }); + }).send(); + }).rejects.toThrowErrorMatchingInlineSnapshot('[Error: Unexpected error while parsing response-data from "GET http://localhost:8080/rest/nowhere"]'); +}); - const res = req.send(); +it('should cancel the request when told to do so', async () => { + const driver = new GCMSFetchDriver(); + const REQUEST_URL = 'http://localhost:8080/rest/nowhere'; + const STATUS_MSG = 'Ok'; + const STATUS_CODE = 200; + const RESPONSE_DATA: GCMSResponse = { + responseInfo: { + responseCode: ResponseCode.OK, + responseMessage: 'Success', + }, + messages: [], + }; + + global.fetch = vitest.fn((args) => { + if (typeof args === 'string') { + args = { url: args }; + } else if (args instanceof URL) { + args = { url: args.toString() }; + } + const signal = (args as RequestInit).signal; + + return new Promise>((resolve, reject) => { + let aborted = false; + + setTimeout(() => { + if (aborted) { + return; + } + + resolve({ + status: STATUS_CODE, + statusText: STATUS_MSG, + ok: STATUS_CODE < 400, + headers: new Headers(), + text: () => Promise.resolve(JSON.stringify(RESPONSE_DATA)), + json: () => Promise.resolve(RESPONSE_DATA), + }); + }, 1_000); - req.cancel(); + signal.addEventListener('abort', () => { + aborted = true; // 🤘 + reject(signal.reason); + }); + }); + }) as any; - try { - await res; - expect.fail('Should not resolve!'); - } catch (err) { - // Don't use `toBeInstanceOf`, doesn't work! - expect(err instanceof GCMSRestClientAbortError).toEqual(true); - } + const req = driver.performMappedRequest({ + headers: {}, + method: RequestMethod.GET, + url: REQUEST_URL, + params: {}, }); + + const res = req.send(); + + req.cancel(); + + try { + await res; + expect.fail('Should not resolve!'); + } catch (err) { + // Don't use `toBeInstanceOf`, doesn't work! + expect(err instanceof GCMSRestClientAbortError).toEqual(true); + } }); diff --git a/cms-ui/libs/cms-rest-client/tsconfig.json b/cms-ui/libs/cms-rest-client/tsconfig.json index ea98558249..21824dd178 100644 --- a/cms-ui/libs/cms-rest-client/tsconfig.json +++ b/cms-ui/libs/cms-rest-client/tsconfig.json @@ -1,23 +1,24 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "importHelpers": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "ignoreDeprecations": "6.0" }, - { - "path": "./tsconfig.spec.json" - } - ] + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/cms-ui/libs/cms-rest-client/tsconfig.lib.json b/cms-ui/libs/cms-rest-client/tsconfig.lib.json index 6653d0830b..6e230ed947 100644 --- a/cms-ui/libs/cms-rest-client/tsconfig.lib.json +++ b/cms-ui/libs/cms-rest-client/tsconfig.lib.json @@ -1,15 +1,23 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": [ - "jest.config.ts", - "jest.config.cts", - "src/**/*.spec.ts", - "src/**/*.test.ts" - ] + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": [ + "src/**/*.spec.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ] } diff --git a/cms-ui/libs/cms-rest-client/tsconfig.spec.json b/cms-ui/libs/cms-rest-client/tsconfig.spec.json index cebbc1b5bf..26db20034e 100644 --- a/cms-ui/libs/cms-rest-client/tsconfig.spec.json +++ b/cms-ui/libs/cms-rest-client/tsconfig.spec.json @@ -1,16 +1,28 @@ { - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "moduleResolution": "node10", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "jest.config.ci.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] } diff --git a/cms-ui/libs/cms-rest-client/vitest.config.mts b/cms-ui/libs/cms-rest-client/vitest.config.mts new file mode 100644 index 0000000000..ad291e8240 --- /dev/null +++ b/cms-ui/libs/cms-rest-client/vitest.config.mts @@ -0,0 +1,9 @@ +/// +import { ConfigEnv, defineConfig } from 'vite'; +import { createProjectConfiguration, getPlugins } from '../../vitest.project'; + +export default defineConfig((env: ConfigEnv) => ({ + root: __dirname, + ...createProjectConfiguration('libs', 'cms-rest-client', env.mode), + plugins: getPlugins('library'), +})); diff --git a/cms-ui/libs/cms-rest-clients-angular/project.json b/cms-ui/libs/cms-rest-clients-angular/project.json index ee29811938..ffddc14776 100644 --- a/cms-ui/libs/cms-rest-clients-angular/project.json +++ b/cms-ui/libs/cms-rest-clients-angular/project.json @@ -4,7 +4,7 @@ "projectType": "library", "prefix": "gtx", "sourceRoot": "libs/cms-rest-clients-angular/src", - "tags": ["lib", "angular", "publish"], + "tags": ["lib", "angular", "publish", "test:karma"], "targets": { "build": { "executor": "@nx/angular:package", diff --git a/cms-ui/libs/cms-rest-clients-angular/tsconfig.json b/cms-ui/libs/cms-rest-clients-angular/tsconfig.json index 4f2d5f01d9..0a8d6384d4 100644 --- a/cms-ui/libs/cms-rest-clients-angular/tsconfig.json +++ b/cms-ui/libs/cms-rest-clients-angular/tsconfig.json @@ -7,7 +7,8 @@ "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": false, "noFallthroughCasesInSwitch": true, - "module": "preserve" + "module": "preserve", + "ignoreDeprecations": "6.0" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/cms-ui/libs/common/.swcrc b/cms-ui/libs/common/.swcrc new file mode 100644 index 0000000000..48e979e424 --- /dev/null +++ b/cms-ui/libs/common/.swcrc @@ -0,0 +1,30 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [ + "jest.config.[ct]s", + "jest.config.ci.[ct]s", + ".*\\.spec.tsx?$", + ".*\\.test.tsx?$", + "./src/jest-setup.ts$", + "./**/jest-setup.ts$", + ".*.js$" + ] +} diff --git a/cms-ui/libs/common/jest.config.ci.ts b/cms-ui/libs/common/jest.config.ci.ts deleted file mode 100644 index 6027a38c48..0000000000 --- a/cms-ui/libs/common/jest.config.ci.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCIReporters } from '../../jest.preset'; -import config from './jest.config'; - -export default { - ...config, - ...createCIReporters('libs', 'common'), -}; diff --git a/cms-ui/libs/common/jest.config.ts b/cms-ui/libs/common/jest.config.ts deleted file mode 100644 index d5c898d59a..0000000000 --- a/cms-ui/libs/common/jest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'jest'; -import { createSWCConfig } from '../../jest.preset'; - -export default defineConfig({ - ...createSWCConfig('libs', 'common'), -}); diff --git a/cms-ui/libs/common/project.json b/cms-ui/libs/common/project.json index b75eaeaef8..f2b9eb9d24 100644 --- a/cms-ui/libs/common/project.json +++ b/cms-ui/libs/common/project.json @@ -3,28 +3,6 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/common/src", "projectType": "library", - "tags": ["lib", "publish"], - "targets": { - "test": { - "executor": "@nx/jest:jest", - "outputs": [ - "{workspaceRoot}/.reports/{projectRoot}/JEST-report.xml", - "{workspaceRoot}/coverage/{projectRoot}" - ], - "options": { - "jestConfig": "{projectRoot}/jest.config.ts" - }, - "configurations": { - "watch": { - "watch": true - }, - "ci": { - "color": false, - "colors": false, - "ci": true, - "jestConfig": "{projectRoot}/jest.config.ci.ts" - } - } - } - } + "tags": ["lib", "publish", "test:vitest"], + "targets": {} } diff --git a/cms-ui/libs/common/tsconfig.json b/cms-ui/libs/common/tsconfig.json index 323b842565..21824dd178 100644 --- a/cms-ui/libs/common/tsconfig.json +++ b/cms-ui/libs/common/tsconfig.json @@ -8,7 +8,8 @@ "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true + "noPropertyAccessFromIndexSignature": true, + "ignoreDeprecations": "6.0" }, "files": [], "include": [], diff --git a/cms-ui/libs/common/tsconfig.lib.json b/cms-ui/libs/common/tsconfig.lib.json index f91800d050..23e5421d7b 100644 --- a/cms-ui/libs/common/tsconfig.lib.json +++ b/cms-ui/libs/common/tsconfig.lib.json @@ -10,6 +10,16 @@ "jest.config.ts", "jest.config.cts", "src/**/*.spec.ts", - "src/**/*.test.ts" + "src/**/*.test.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" ] } diff --git a/cms-ui/libs/common/tsconfig.spec.json b/cms-ui/libs/common/tsconfig.spec.json index 988c97c068..24a3309f64 100644 --- a/cms-ui/libs/common/tsconfig.spec.json +++ b/cms-ui/libs/common/tsconfig.spec.json @@ -2,14 +2,19 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "moduleResolution": "node10", - "types": ["jest", "node"] + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] }, "include": [ - "jest.config.ts", - "jest.config.cts", - "src/**/*.test.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", "src/**/*.spec.ts", "src/**/*.d.ts" ] diff --git a/cms-ui/libs/common/vitest.config.mts b/cms-ui/libs/common/vitest.config.mts new file mode 100644 index 0000000000..e596b83854 --- /dev/null +++ b/cms-ui/libs/common/vitest.config.mts @@ -0,0 +1,9 @@ +/// +import { ConfigEnv, defineConfig } from 'vite'; +import { createProjectConfiguration, getPlugins } from '../../vitest.project'; + +export default defineConfig((env: ConfigEnv) => ({ + root: __dirname, + ...createProjectConfiguration('libs', 'common', env.mode), + plugins: getPlugins('library'), +})); diff --git a/cms-ui/libs/e2e-utils/.swcrc b/cms-ui/libs/e2e-utils/.swcrc new file mode 100644 index 0000000000..48e979e424 --- /dev/null +++ b/cms-ui/libs/e2e-utils/.swcrc @@ -0,0 +1,30 @@ +{ + "jsc": { + "target": "es2017", + "parser": { + "syntax": "typescript", + "decorators": true, + "dynamicImport": true + }, + "transform": { + "decoratorMetadata": true, + "legacyDecorator": true + }, + "keepClassNames": true, + "externalHelpers": true, + "loose": true + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": [ + "jest.config.[ct]s", + "jest.config.ci.[ct]s", + ".*\\.spec.tsx?$", + ".*\\.test.tsx?$", + "./src/jest-setup.ts$", + "./**/jest-setup.ts$", + ".*.js$" + ] +} diff --git a/cms-ui/libs/e2e-utils/jest.config.ci.ts b/cms-ui/libs/e2e-utils/jest.config.ci.ts deleted file mode 100644 index 583c148962..0000000000 --- a/cms-ui/libs/e2e-utils/jest.config.ci.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCIReporters } from '../../jest.preset'; -import config from './jest.config'; - -export default { - ...config, - ...createCIReporters('libs', 'e2e-utils'), -}; diff --git a/cms-ui/libs/e2e-utils/jest.config.ts b/cms-ui/libs/e2e-utils/jest.config.ts deleted file mode 100644 index 212ad4a5fb..0000000000 --- a/cms-ui/libs/e2e-utils/jest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'jest'; -import { createSWCConfig } from '../../jest.preset'; - -export default defineConfig({ - ...createSWCConfig('libs', 'e2e-utils'), -}); diff --git a/cms-ui/libs/e2e-utils/project.json b/cms-ui/libs/e2e-utils/project.json index 57a9bfdbec..2aa8abd54a 100644 --- a/cms-ui/libs/e2e-utils/project.json +++ b/cms-ui/libs/e2e-utils/project.json @@ -4,28 +4,5 @@ "sourceRoot": "libs/e2e-utils/src", "projectType": "library", "tags": ["lib"], - "targets": { - "test": { - "executor": "@nx/jest:jest", - "outputs": [ - "{workspaceRoot}/.reports/{projectRoot}/JEST-report.xml", - "{workspaceRoot}/coverage/{projectRoot}" - ], - "options": { - "jestConfig": "{projectRoot}/jest.config.ts", - "passWithNoTests": true - }, - "configurations": { - "watch": { - "watch": true - }, - "ci": { - "color": false, - "colors": false, - "ci": true, - "jestConfig": "{projectRoot}/jest.config.ci.ts" - } - } - } - } + "targets": {} } diff --git a/cms-ui/libs/e2e-utils/tsconfig.json b/cms-ui/libs/e2e-utils/tsconfig.json index 759a84c860..c84210bbf3 100644 --- a/cms-ui/libs/e2e-utils/tsconfig.json +++ b/cms-ui/libs/e2e-utils/tsconfig.json @@ -8,16 +8,14 @@ "noImplicitOverride": false, "noImplicitReturns": false, "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, + "ignoreDeprecations": "6.0" }, "files": [], "include": [], "references": [ { "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" } ] } diff --git a/cms-ui/libs/e2e-utils/tsconfig.lib.json b/cms-ui/libs/e2e-utils/tsconfig.lib.json index 62d465d9b4..e2648706ce 100644 --- a/cms-ui/libs/e2e-utils/tsconfig.lib.json +++ b/cms-ui/libs/e2e-utils/tsconfig.lib.json @@ -3,17 +3,8 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "declaration": true, - "types": [ - "node", - "jquery", - "playwright" - ] + "types": ["node", "jquery", "playwright"] }, "include": ["src/**/*.ts"], - "exclude": [ - "jest.config.ts", - "jest.config.cts", - "src/**/*.spec.ts", - "src/**/*.test.ts" - ] + "exclude": [] } diff --git a/cms-ui/libs/e2e-utils/tsconfig.spec.json b/cms-ui/libs/e2e-utils/tsconfig.spec.json deleted file mode 100644 index 988c97c068..0000000000 --- a/cms-ui/libs/e2e-utils/tsconfig.spec.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "moduleResolution": "node10", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "jest.config.cts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/cms-ui/libs/form-grid/cypress/tsconfig.json b/cms-ui/libs/form-grid/cypress/tsconfig.json index e5a2789bc0..fcb7d52faf 100644 --- a/cms-ui/libs/form-grid/cypress/tsconfig.json +++ b/cms-ui/libs/form-grid/cypress/tsconfig.json @@ -7,11 +7,12 @@ "module": "commonjs", "types": ["cypress", "node"], "sourceMap": false, + "ignoreDeprecations": "6.0", "strict": false, "noImplicitOverride": false, "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": false, - "noFallthroughCasesInSwitch": true, + "noFallthroughCasesInSwitch": true }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/cms-ui/libs/form-grid/jest.config.ci.ts b/cms-ui/libs/form-grid/jest.config.ci.ts deleted file mode 100644 index 3082250081..0000000000 --- a/cms-ui/libs/form-grid/jest.config.ci.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createCIReporters } from '../../jest.preset'; -import config from './jest.config'; - -export default { - ...config, - ...createCIReporters('libs', 'form-grid'), -}; diff --git a/cms-ui/libs/form-grid/jest.config.ts b/cms-ui/libs/form-grid/jest.config.ts deleted file mode 100644 index d1385f6dbc..0000000000 --- a/cms-ui/libs/form-grid/jest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'jest'; -import { createAngularConfig } from '../../jest.preset'; - -export default defineConfig({ - ...createAngularConfig('libs', 'form-grid'), -}); diff --git a/cms-ui/libs/form-grid/project.json b/cms-ui/libs/form-grid/project.json index 2f06e7b08c..cf464c110f 100644 --- a/cms-ui/libs/form-grid/project.json +++ b/cms-ui/libs/form-grid/project.json @@ -4,7 +4,7 @@ "projectType": "library", "prefix": "gtx", "sourceRoot": "libs/form-grid/src", - "tags": ["lib", "angular", "publish"], + "tags": ["lib", "angular", "publish", "test:vitest"], "targets": { "build": { "executor": "@nx/angular:package", @@ -22,25 +22,7 @@ "defaultConfiguration": "production" }, "test": { - "executor": "@nx/jest:jest", - "outputs": [ - "{workspaceRoot}/.reports/{projectRoot}/JEST-report.xml", - "{workspaceRoot}/coverage/{projectRoot}" - ], - "options": { - "jestConfig": "{projectRoot}/jest.config.ts" - }, - "configurations": { - "watch": { - "watch": true - }, - "ci": { - "color": false, - "colors": false, - "ci": true, - "jestConfig": "{projectRoot}/jest.config.ci.ts" - } - } + "executor": "@nx/angular:unit-test" }, "lint": { "executor": "@nx/eslint:lint" diff --git a/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.html b/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.html index 61987c8c43..02269d1abd 100644 --- a/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.html +++ b/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.html @@ -155,7 +155,7 @@ } @default { - Unknown setting type {{ setting.type }} + Unknown setting type {{ getTypeIfValid(setting) }} } } } diff --git a/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.ts b/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.ts index 0a2633d995..3439af2029 100644 --- a/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.ts +++ b/cms-ui/libs/form-grid/src/lib/components/dynamic-form-settings/dynamic-form-settings.component.ts @@ -164,4 +164,12 @@ export class DynamicFormSettingsComponent { this.updateData(setting, sanitizeItemReference(value)); } } + + public getTypeIfValid(setting: FormSettingConfiguration): string { + if(Boolean(setting)) { + return setting.type; + } + + return ""; + } } diff --git a/cms-ui/libs/form-grid/src/lib/utils/conditions.spec.ts b/cms-ui/libs/form-grid/src/lib/utils/conditions.spec.ts new file mode 100644 index 0000000000..3b05c28b53 --- /dev/null +++ b/cms-ui/libs/form-grid/src/lib/utils/conditions.spec.ts @@ -0,0 +1,55 @@ +import { FormElement, FormElementConfiguration, FormSettingConfiguration, FormSettingType } from '@gentics/cms-models'; +import { isSettingVisible } from './conditions'; + +it('simple single condition', () => { + const SETTING: FormSettingConfiguration = { + id: 'foobar', + type: FormSettingType.BOOLEAN, + labelI18n: { + en: 'foobar', + }, + condition: { + source: { + setting: 'other', + }, + equals: true, + }, + }; + const CONFIG: FormElementConfiguration = { + labelI18n: { + en: 'dummy config', + }, + settings: [ + SETTING, + { + id: 'other', + type: FormSettingType.BOOLEAN, + labelI18n: { + en: 'other', + }, + }, + ], + }; + const ELEMENT: FormElement = { + id: 'something', + label: { + en: 'dummy element', + }, + type: 'property', + uiSchemaPage: 0, + formGridOptions: { + type: 'dummy', + other: true, + }, + }; + + expect(isSettingVisible(SETTING, CONFIG, ELEMENT)).toBe(true); + + expect(isSettingVisible(SETTING, CONFIG, { + ...ELEMENT, + formGridOptions: { + type: 'dummy', + other: false, + }, + })).toBe(false); +}); diff --git a/cms-ui/libs/form-grid/src/test-setup.ts b/cms-ui/libs/form-grid/src/test-setup.ts deleted file mode 100644 index bed175f000..0000000000 --- a/cms-ui/libs/form-grid/src/test-setup.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; - -setupZonelessTestEnv({ - errorOnUnknownElements: true, - errorOnUnknownProperties: true, -}); diff --git a/cms-ui/libs/form-grid/tsconfig.json b/cms-ui/libs/form-grid/tsconfig.json index a0cd5cfd23..086eb1710d 100644 --- a/cms-ui/libs/form-grid/tsconfig.json +++ b/cms-ui/libs/form-grid/tsconfig.json @@ -7,7 +7,8 @@ "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "module": "preserve" + "module": "preserve", + "ignoreDeprecations": "6.0" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/cms-ui/libs/form-grid/tsconfig.lib.json b/cms-ui/libs/form-grid/tsconfig.lib.json index df778a7d31..f9446d5ec9 100644 --- a/cms-ui/libs/form-grid/tsconfig.lib.json +++ b/cms-ui/libs/form-grid/tsconfig.lib.json @@ -11,11 +11,10 @@ "exclude": [ "src/**/*.spec.ts", "src/**/*.test.ts", - "jest.config.ts", - "jest.config.cts", - "src/test-setup.ts", "cypress/**/*", "cypress.config.ts", + "vite.config.ci.mts", + "vite.config.mts", "**/*.cy.ts", "**/*.cy.js", "**/*.cy.tsx", diff --git a/cms-ui/libs/form-grid/tsconfig.spec.json b/cms-ui/libs/form-grid/tsconfig.spec.json index cc26179588..d92e2cb97a 100644 --- a/cms-ui/libs/form-grid/tsconfig.spec.json +++ b/cms-ui/libs/form-grid/tsconfig.spec.json @@ -2,16 +2,37 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "target": "es2016", - "types": ["jest", "node"], - "moduleResolution": "node10" + "types": [ + "vitest/globals", + "jquery" + ], + "strict": false, + "noImplicitOverride": false, + "noPropertyAccessFromIndexSignature": false, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": true, + "emitDecoratorMetadata": false, + "ignoreDeprecations": "6.0" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": false, + "strictInputAccessModifiers": false, + "strictTemplates": false }, - "files": ["src/test-setup.ts"], "include": [ - "jest.config.ts", + "src/**/*.ts", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", "src/**/*.test.ts", - "src/**/*.spec.ts", "src/**/*.d.ts" + ], + "exclude": [ + "cypress.config.ci.ts", + "cypress.config.ts", + "cypress/**/*", + "src/**/*.cy.ts" ] } diff --git a/cms-ui/libs/form-grid/vite.config.ci.mts b/cms-ui/libs/form-grid/vite.config.ci.mts new file mode 100644 index 0000000000..2fa1f00eb2 --- /dev/null +++ b/cms-ui/libs/form-grid/vite.config.ci.mts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from 'vite'; +import { createProjectConfiguration, getPlugins } from '../../vitest.project'; + +export default defineConfig(() => ({ + root: __dirname, + ...createProjectConfiguration('libs', 'form-grid', 'ci'), + plugins: getPlugins('angular'), +})); diff --git a/cms-ui/libs/form-grid/vite.config.mts b/cms-ui/libs/form-grid/vite.config.mts new file mode 100644 index 0000000000..f7ada83347 --- /dev/null +++ b/cms-ui/libs/form-grid/vite.config.mts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from 'vite'; +import { createProjectConfiguration, getPlugins } from '../../vitest.project'; + +export default defineConfig(() => ({ + root: __dirname, + ...createProjectConfiguration('libs', 'form-grid'), + plugins: getPlugins('angular'), +})); diff --git a/cms-ui/libs/image-editor/cypress/tsconfig.json b/cms-ui/libs/image-editor/cypress/tsconfig.json index 12f174438e..c36c03f3ff 100644 --- a/cms-ui/libs/image-editor/cypress/tsconfig.json +++ b/cms-ui/libs/image-editor/cypress/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "../../dist/out-tsc", "module": "commonjs", "types": ["cypress", "node"], - "sourceMap": false + "sourceMap": false, + "ignoreDeprecations": "6.0" }, "include": [ "**/*.ts", diff --git a/cms-ui/libs/image-editor/project.json b/cms-ui/libs/image-editor/project.json index 7515df7b84..20d7fa3fcc 100644 --- a/cms-ui/libs/image-editor/project.json +++ b/cms-ui/libs/image-editor/project.json @@ -4,7 +4,7 @@ "projectType": "library", "prefix": "gtx", "sourceRoot": "libs/image-editor/src", - "tags": ["lib", "angular", "publish"], + "tags": ["lib", "angular", "publish", "test:karma"], "targets": { "build": { "executor": "@nx/angular:ng-packagr-lite", diff --git a/cms-ui/libs/image-editor/tsconfig.json b/cms-ui/libs/image-editor/tsconfig.json index 413a3ace6e..0d9fbc82ce 100644 --- a/cms-ui/libs/image-editor/tsconfig.json +++ b/cms-ui/libs/image-editor/tsconfig.json @@ -7,7 +7,8 @@ "noPropertyAccessFromIndexSignature": false, "noImplicitReturns": false, "noFallthroughCasesInSwitch": true, - "module": "preserve" + "module": "preserve", + "ignoreDeprecations": "6.0" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/cms-ui/libs/ui-core/cypress/tsconfig.json b/cms-ui/libs/ui-core/cypress/tsconfig.json index 12f174438e..c36c03f3ff 100644 --- a/cms-ui/libs/ui-core/cypress/tsconfig.json +++ b/cms-ui/libs/ui-core/cypress/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "../../dist/out-tsc", "module": "commonjs", "types": ["cypress", "node"], - "sourceMap": false + "sourceMap": false, + "ignoreDeprecations": "6.0" }, "include": [ "**/*.ts", diff --git a/cms-ui/libs/ui-core/project.json b/cms-ui/libs/ui-core/project.json index 21ccec5757..175bc481ba 100644 --- a/cms-ui/libs/ui-core/project.json +++ b/cms-ui/libs/ui-core/project.json @@ -4,7 +4,7 @@ "projectType": "library", "prefix": "gtx", "sourceRoot": "libs/ui-core/src", - "tags": ["lib", "angular", "publish"], + "tags": ["lib", "angular", "publish", "test:karma"], "targets": { "build": { "executor": "@nx/angular:ng-packagr-lite", diff --git a/cms-ui/libs/ui-core/src/lib/common/colors.ts b/cms-ui/libs/ui-core/src/lib/common/colors.ts index 8fb19fedb1..6a3ea0e7ee 100644 --- a/cms-ui/libs/ui-core/src/lib/common/colors.ts +++ b/cms-ui/libs/ui-core/src/lib/common/colors.ts @@ -1 +1 @@ -export type ColorThemes = 'primary' | 'secondary' | 'success' | 'warning' | 'alert'; +export type ColorThemes = 'primary' | 'secondary' | 'success' | 'warning' | 'alert' | 'default'; diff --git a/cms-ui/libs/ui-core/src/lib/common/modal.ts b/cms-ui/libs/ui-core/src/lib/common/modal.ts index 677ce92eda..2e9e5e4d25 100644 --- a/cms-ui/libs/ui-core/src/lib/common/modal.ts +++ b/cms-ui/libs/ui-core/src/lib/common/modal.ts @@ -6,18 +6,20 @@ import { ModalClosingReason } from '@gentics/cms-integration-api-models'; export interface IDialogConfig { title: string; body?: string; - buttons: { - id?: string; - label: string; - type?: 'default' | 'secondary' | 'success'| 'warning' | 'alert'; - flat?: boolean; - // If specified, will be returned as the - // value of the resolved promise (or the reason if rejected). - returnValue?: any; - // If true, clicking the button will cause - // the promise to reject rather than resolve - shouldReject?: boolean; - }[]; + buttons: Array; +} + +export interface DialogButton { + id?: string; + label: string; + type?: 'default' | 'secondary' | 'success'| 'warning' | 'alert'; + flat?: boolean; + // If specified, will be returned as the + // value of the resolved promise (or the reason if rejected). + returnValue?: any; + // If true, clicking the button will cause + // the promise to reject rather than resolve + shouldReject?: boolean; } /** diff --git a/cms-ui/libs/ui-core/src/lib/components/breadcrumbs/breadcrumbs.component.html b/cms-ui/libs/ui-core/src/lib/components/breadcrumbs/breadcrumbs.component.html index 30e146f811..d75b717917 100644 --- a/cms-ui/libs/ui-core/src/lib/components/breadcrumbs/breadcrumbs.component.html +++ b/cms-ui/libs/ui-core/src/lib/components/breadcrumbs/breadcrumbs.component.html @@ -54,7 +54,7 @@ >...
    }
    - @for (link of links; track link; let i = $index; let last = $last) { + @for (link of links; track link?.href || link?.text; let i = $index; let last = $last) { @if (multilineExpanded || i > 0) { { ]; fixture.detectChanges(); + tick(1000); expect(linkHrefs(fixture)).toEqual(['/a', './b', '#c']); @@ -135,6 +136,9 @@ describe('Breadcrumbs', () => { { text: 'C', href: '#ccc' }, ]; fixture.detectChanges(); + fixture.whenRenderingDone(); + tick(1000); + fixture.detectChanges(); expect(linkHrefs(fixture)).toEqual(['/aaa', './bbb', '#ccc'], 'href of breadcrumb links did not change by reference'); diff --git a/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts b/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts index c92c586f3c..78165bec49 100644 --- a/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts +++ b/cms-ui/libs/ui-core/src/lib/components/button/button.component.ts @@ -39,7 +39,7 @@ export class ButtonComponent { * "success", "warning" or "alert". */ @Input() - public type: 'default' | 'secondary' | 'success' | 'warning' | 'alert' = 'default'; + public type: 'primary' | 'secondary' | 'success' | 'warning' | 'alert' | 'default' = 'default'; /** * Setting the "flat" attribute gives the button a transparent background diff --git a/cms-ui/libs/ui-core/src/lib/components/checkbox/checkbox.component.ts b/cms-ui/libs/ui-core/src/lib/components/checkbox/checkbox.component.ts index c39e3a7049..66491242fa 100644 --- a/cms-ui/libs/ui-core/src/lib/components/checkbox/checkbox.component.ts +++ b/cms-ui/libs/ui-core/src/lib/components/checkbox/checkbox.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { cancelEvent, coerceToBoolean, randomId } from '@gentics/common'; import { CHECKBOX_STATE_INDETERMINATE, type CheckboxState } from '../../common'; import { generateFormProvider } from '../../utils'; @@ -81,6 +81,12 @@ export class CheckboxComponent extends BaseFormElementComponent { @Input() public formValue: string | null = null; + @Input() + public indeterminate = false; + + @Output() + public override valueChange = new EventEmitter(); + protected onValueChange(): void { // no-op } diff --git a/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.html b/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.html index 2b5a85c0bf..ed585ec4d6 100644 --- a/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.html +++ b/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.html @@ -11,8 +11,8 @@ @for (year of years; track year) { {{ year }} @@ -40,13 +40,13 @@ @@ -62,13 +62,13 @@ @@ -85,13 +85,13 @@ diff --git a/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.ts b/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.ts index f0e026eca8..2b937e944f 100644 --- a/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.ts +++ b/cms-ui/libs/ui-core/src/lib/components/date-time-picker-controls/date-time-picker-controls.component.ts @@ -5,12 +5,14 @@ import { ChangeDetectorRef, Component, ElementRef, + EventEmitter, HostBinding, Input, OnChanges, OnDestroy, OnInit, Optional, + Output, SimpleChanges, ViewChild, } from '@angular/core'; @@ -116,6 +118,12 @@ export class DateTimePickerControlsComponent @Input() public displaySeconds = false; + @Output() + public override valueChange = new EventEmitter(); + + @Output() + change = new EventEmitter(); + /** * When `true`, the controls use the "compact" (small screen) styling for all screen sizes. Defaults to `false` */ diff --git a/cms-ui/libs/ui-core/src/lib/components/date-time-picker/date-time-picker.component.html b/cms-ui/libs/ui-core/src/lib/components/date-time-picker/date-time-picker.component.html index 4d6fc0d421..003ec641b8 100644 --- a/cms-ui/libs/ui-core/src/lib/components/date-time-picker/date-time-picker.component.html +++ b/cms-ui/libs/ui-core/src/lib/components/date-time-picker/date-time-picker.component.html @@ -3,8 +3,8 @@ [focusable]="true" [clickable]="true" (boxClick)="showModal()" - (focus)="handleFocus($event)" - (blur)="handleBlur($event)" + (focus)="handleFocus()" + (blur)="handleBlur()" > @if (label) { diff --git a/cms-ui/libs/ui-core/src/lib/components/file-picker/file-picker.component.html b/cms-ui/libs/ui-core/src/lib/components/file-picker/file-picker.component.html index d56dbe1001..5704f0b68a 100644 --- a/cms-ui/libs/ui-core/src/lib/components/file-picker/file-picker.component.html +++ b/cms-ui/libs/ui-core/src/lib/components/file-picker/file-picker.component.html @@ -17,8 +17,8 @@ [icon]="icon" [type]="dropArea?.pageDragHovered ? 'success' : type" [flat]="flat || dropArea?.dragHovered" - (focus)="handleFocus($event)" - (blur)="handleBlur($event)" + (focus)="handleFocus()" + (blur)="handleBlur()" (click)="openFilePicker()" > diff --git a/cms-ui/libs/ui-core/src/lib/components/grouped-tabs/grouped-tabs.component.html b/cms-ui/libs/ui-core/src/lib/components/grouped-tabs/grouped-tabs.component.html index 68bb3ff8bc..a363c114e7 100644 --- a/cms-ui/libs/ui-core/src/lib/components/grouped-tabs/grouped-tabs.component.html +++ b/cms-ui/libs/ui-core/src/lib/components/grouped-tabs/grouped-tabs.component.html @@ -1,11 +1,11 @@
    @@ -51,7 +51,7 @@ [attr.data-id]="action.id" [icon]="true" [size]="'small'" - [disabled]="disabled || (rows?.length < 1 && totalCount < 1) || selected.length < 1 || !selectable || !(action | gtxTableActionEnabled)" + [disabled]="disabled || (rows?.length < 1 && (totalCount ?? 0) < 1) || selectedCount < 1 || !selectable || !(action | gtxTableActionEnabled)" [title]="action.label" [type]="action.type" (click)="handleMultiActionClick(action)" @@ -82,7 +82,7 @@ @@ -122,7 +122,7 @@ class="selection-checkbox" [value]="selected[row.id]" [disabled]="disabled" - (valueChange)="updateRowSelection(row, $event)" + (valueChange)="updateRowSelection(row)" >
    } diff --git a/cms-ui/libs/ui-core/src/lib/components/table/table.component.ts b/cms-ui/libs/ui-core/src/lib/components/table/table.component.ts index e7d1d58a52..e8b6a40e45 100644 --- a/cms-ui/libs/ui-core/src/lib/components/table/table.component.ts +++ b/cms-ui/libs/ui-core/src/lib/components/table/table.component.ts @@ -83,7 +83,7 @@ export class TableComponent extends BaseTableComponent> implem } } - public toggleAllSelections(event: MouseEvent): void { + public toggleAllSelections(event?: MouseEvent): void { cancelEvent(event); if (this.selectAllType == null || this.selectAllType === TableSelectAllType.NONE) { @@ -123,4 +123,10 @@ export class TableComponent extends BaseTableComponent> implem this.allSelected = (this.rows || []).every((row) => this.selected[row.id] === true); } + + public get selectedCount(): number { + return Array.isArray(this.selected) + ? this.selected.length + : Object.values(this.selected).filter(Boolean).length; + } } diff --git a/cms-ui/libs/ui-core/src/lib/components/textarea/textarea.component.ts b/cms-ui/libs/ui-core/src/lib/components/textarea/textarea.component.ts index 8d3a4812f6..b1943ab62a 100644 --- a/cms-ui/libs/ui-core/src/lib/components/textarea/textarea.component.ts +++ b/cms-ui/libs/ui-core/src/lib/components/textarea/textarea.component.ts @@ -131,7 +131,7 @@ export class TextareaComponent extends BaseFormElementComponent implemen } } - public textAreaInputHandler(event: KeyboardEvent): void { + public textAreaInputHandler(event: KeyboardEvent | InputEvent): void { const elementValue = (event.target as HTMLTextAreaElement).value; this.triggerChange(elementValue); } diff --git a/cms-ui/libs/ui-core/src/lib/components/trable/trable.component.html b/cms-ui/libs/ui-core/src/lib/components/trable/trable.component.html index 385a7277a4..223b054a7f 100644 --- a/cms-ui/libs/ui-core/src/lib/components/trable/trable.component.html +++ b/cms-ui/libs/ui-core/src/lib/components/trable/trable.component.html @@ -46,7 +46,7 @@ class="action-button" [icon]="true" [size]="'small'" - [disabled]="(rows?.length < 1) || (action.disabled | gtxTableActionEnabled)" + [disabled]="(rows?.length < 1) || !(action | gtxTableActionEnabled)" [title]="action.label" [type]="action.type" [attr.data-id]="action.id" @@ -70,14 +70,14 @@ }
    -