90978: Fix e2e tests failing due to interactions before page fully loaded

Now that we use initialNavigation: 'enabledBlocking', pages can appear to be loaded before some functionality is fully active.
In some cases this trips up Cypress, and it tries to interact with the app too soon.

We address this by introducing a new dsBrowserOnly pipe in order to defer the data-test attributes Cypress relies on to CSR.
This commit is contained in:
Yura Bondarenko
2022-04-27 09:06:55 +02:00
parent 22d5643d8b
commit d69a02e6cc
32 changed files with 123 additions and 43 deletions

View File

@@ -65,7 +65,7 @@ describe('My DSpace page', () => {
cy.visit('/mydspace'); cy.visit('/mydspace');
// Open the New Submission dropdown // Open the New Submission dropdown
cy.get('#dropdownSubmission').click(); cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Item" type in that dropdown // Click on the "Item" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="none"]').click(); cy.get('#entityControlsDropdownMenu button[title="none"]').click();
@@ -98,7 +98,7 @@ describe('My DSpace page', () => {
const id = subpaths[2]; const id = subpaths[2];
// Click the "Save for Later" button to save this submission // Click the "Save for Later" button to save this submission
cy.get('button#saveForLater').click(); cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
// "Save for Later" should send us to MyDSpace // "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace'); cy.url().should('include', '/mydspace');
@@ -122,7 +122,7 @@ describe('My DSpace page', () => {
cy.url().should('include', '/workspaceitems/' + id + '/edit'); cy.url().should('include', '/workspaceitems/' + id + '/edit');
// Discard our new submission by clicking Discard in Submission form & confirming // Discard our new submission by clicking Discard in Submission form & confirming
cy.get('button#discard').click(); cy.get('ds-submission-form-footer [data-test="discard"]').click();
cy.get('button#discard_submit').click(); cy.get('button#discard_submit').click();
// Discarding should send us back to MyDSpace // Discarding should send us back to MyDSpace
@@ -135,7 +135,7 @@ describe('My DSpace page', () => {
cy.visit('/mydspace'); cy.visit('/mydspace');
// Open the New Import dropdown // Open the New Import dropdown
cy.get('#dropdownImport').click(); cy.get('button[data-test="import-dropdown"]').click();
// Click on the "Item" type in that dropdown // Click on the "Item" type in that dropdown
cy.get('#importControlsDropdownMenu button[title="none"]').click(); cy.get('#importControlsDropdownMenu button[title="none"]').click();

View File

@@ -24,7 +24,7 @@ describe('Search Page', () => {
// Click each filter toggle to open *every* filter // Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well) // (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true }); cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Analyze <ds-search-page> for accessibility issues // Analyze <ds-search-page> for accessibility issues
testA11y( testA11y(

View File

@@ -27,7 +27,7 @@
<a *ngIf="bitstreamDownloadUrl != null" [href]="bitstreamDownloadUrl" <a *ngIf="bitstreamDownloadUrl != null" [href]="bitstreamDownloadUrl"
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}" title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
data-test="download-button"> [attr.data-test]="'download-button' | dsBrowserOnly">
<i class="fas fa-download fa-fw"></i> <i class="fas fa-download fa-fw"></i>
</a> </a>
<button [routerLink]="['/bitstreams/', bitstream.id, 'edit']" class="btn btn-outline-primary btn-sm" <button [routerLink]="['/bitstreams/', bitstream.id, 'edit']" class="btn btn-outline-primary btn-sm"

View File

@@ -12,6 +12,7 @@ import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; import { getBitstreamDownloadRoute } from '../../../../app-routing-paths';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe';
let comp: ItemEditBitstreamComponent; let comp: ItemEditBitstreamComponent;
let fixture: ComponentFixture<ItemEditBitstreamComponent>; let fixture: ComponentFixture<ItemEditBitstreamComponent>;
@@ -72,7 +73,11 @@ describe('ItemEditBitstreamComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot()],
declarations: [ItemEditBitstreamComponent, VarDirective], declarations: [
ItemEditBitstreamComponent,
VarDirective,
BrowserOnlyMockPipe,
],
providers: [ providers: [
{ provide: ObjectUpdatesService, useValue: objectUpdatesService } { provide: ObjectUpdatesService, useValue: objectUpdatesService }
], schemas: [ ], schemas: [

View File

@@ -11,6 +11,7 @@
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle <button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle
type="button" [disabled]="!(initialized$|async)" type="button" [disabled]="!(initialized$|async)"
attr.aria-label="{{'mydspace.new-submission-external' | translate}}" attr.aria-label="{{'mydspace.new-submission-external' | translate}}"
[attr.data-test]="'import-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission-external' | translate}}"> title="{{'mydspace.new-submission-external' | translate}}">
<i class="fa fa-file-import" aria-hidden="true"></i> <i class="fa fa-file-import" aria-hidden="true"></i>
<span class="caret"></span> <span class="caret"></span>

View File

@@ -13,6 +13,7 @@ import { ResourceType } from '../../../core/shared/resource-type';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { RouterStub } from '../../../shared/testing/router.stub'; import { RouterStub } from '../../../shared/testing/router.stub';
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
export function getMockEntityTypeService(): EntityTypeService { export function getMockEntityTypeService(): EntityTypeService {
const pageInfo = { elementsPerPage: 20, totalElements: 4, totalPages: 1, currentPage: 0 } as PageInfo; const pageInfo = { elementsPerPage: 20, totalElements: 4, totalPages: 1, currentPage: 0 } as PageInfo;
@@ -83,7 +84,8 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
], ],
declarations: [ declarations: [
MyDSpaceNewExternalDropdownComponent, MyDSpaceNewExternalDropdownComponent,
TestComponent TestComponent,
BrowserOnlyMockPipe
], ],
providers: [ providers: [
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() }, { provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
@@ -134,7 +136,8 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
], ],
declarations: [ declarations: [
MyDSpaceNewExternalDropdownComponent, MyDSpaceNewExternalDropdownComponent,
TestComponent TestComponent,
BrowserOnlyMockPipe,
], ],
providers: [ providers: [
{ provide: EntityTypeService, useValue: getMockEntityTypeService() }, { provide: EntityTypeService, useValue: getMockEntityTypeService() },

View File

@@ -9,6 +9,7 @@
<button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle <button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
type="button" [disabled]="!(initialized$|async)" type="button" [disabled]="!(initialized$|async)"
attr.aria-label="{{'mydspace.new-submission' | translate}}" attr.aria-label="{{'mydspace.new-submission' | translate}}"
[attr.data-test]="'submission-dropdown' | dsBrowserOnly"
title="{{'mydspace.new-submission' | translate}}"> title="{{'mydspace.new-submission' | translate}}">
<i class="fa fa-plus-circle" aria-hidden="true"></i> <i class="fa fa-plus-circle" aria-hidden="true"></i>
<span class="caret"></span> <span class="caret"></span>

View File

@@ -12,6 +12,7 @@ import { ItemType } from '../../../core/shared/item-relationships/item-type.mode
import { ResourceType } from '../../../core/shared/resource-type'; import { ResourceType } from '../../../core/shared/resource-type';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
export function getMockEntityTypeService(): EntityTypeService { export function getMockEntityTypeService(): EntityTypeService {
const type1: ItemType = { const type1: ItemType = {
@@ -87,7 +88,8 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
], ],
declarations: [ declarations: [
MyDSpaceNewSubmissionDropdownComponent, MyDSpaceNewSubmissionDropdownComponent,
TestComponent TestComponent,
BrowserOnlyMockPipe,
], ],
providers: [ providers: [
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() }, { provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
@@ -138,7 +140,8 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
], ],
declarations: [ declarations: [
MyDSpaceNewSubmissionDropdownComponent, MyDSpaceNewSubmissionDropdownComponent,
TestComponent TestComponent,
BrowserOnlyMockPipe,
], ],
providers: [ providers: [
{ provide: EntityTypeService, useValue: getMockEntityTypeService() }, { provide: EntityTypeService, useValue: getMockEntityTypeService() },

View File

@@ -3,8 +3,8 @@
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on"> <form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on">
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query" <input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}" formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" data-test="header-search-box"> class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" [attr.data-test]="'header-search-box' | dsBrowserOnly">
<a class="submit-icon" [routerLink]="" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" data-test="header-search-icon"> <a class="submit-icon" [routerLink]="" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly">
<em class="fas fa-search fa-lg fa-fw"></em> <em class="fas fa-search fa-lg fa-fw"></em>
</a> </a>
</form> </form>

View File

@@ -10,6 +10,7 @@ import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
import { SearchNavbarComponent } from './search-navbar.component'; import { SearchNavbarComponent } from './search-navbar.component';
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { BrowserOnlyMockPipe } from '../shared/testing/browser-only-mock.pipe';
describe('SearchNavbarComponent', () => { describe('SearchNavbarComponent', () => {
let component: SearchNavbarComponent; let component: SearchNavbarComponent;
@@ -44,7 +45,10 @@ describe('SearchNavbarComponent', () => {
useClass: TranslateLoaderMock useClass: TranslateLoaderMock
} }
})], })],
declarations: [SearchNavbarComponent], declarations: [
SearchNavbarComponent,
BrowserOnlyMockPipe,
],
providers: [ providers: [
{ provide: SearchService, useValue: mockSearchService } { provide: SearchService, useValue: mockSearchService }
] ]

View File

@@ -1,8 +1,8 @@
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}"> <ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" <li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
(click)="$event.stopPropagation();"> (click)="$event.stopPropagation();">
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" data-test="login-menu" @fadeInOut> <div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" class="dropdownLogin px-1 " [attr.aria-label]="'nav.login' |translate" (click)="$event.preventDefault()" ngbDropdownToggle> <a href="javascript:void(0);" class="dropdownLogin px-1 " [attr.aria-label]="'nav.login' |translate" (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly" ngbDropdownToggle>
{{ 'nav.login' | translate }} {{ 'nav.login' | translate }}
</a> </a>
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu <div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
@@ -17,9 +17,9 @@
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span> {{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
</a> </a>
</li> </li>
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" data-test="user-menu"> <li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.logout' |translate" (click)="$event.preventDefault()" [title]="'nav.logout' | translate" class="px-1" ngbDropdownToggle> <a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.logout' |translate" (click)="$event.preventDefault()" [title]="'nav.logout' | translate" class="px-1" [attr.data-test]="'user-menu' | dsBrowserOnly" ngbDropdownToggle>
<i class="fas fa-user-circle fa-lg fa-fw"></i></a> <i class="fas fa-user-circle fa-lg fa-fw"></i></a>
<div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.logout' |translate"> <div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.logout' |translate">
<ds-user-menu></ds-user-menu> <ds-user-menu></ds-user-menu>

View File

@@ -15,6 +15,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
describe('AuthNavMenuComponent', () => { describe('AuthNavMenuComponent', () => {
@@ -77,7 +78,8 @@ describe('AuthNavMenuComponent', () => {
TranslateModule.forRoot() TranslateModule.forRoot()
], ],
declarations: [ declarations: [
AuthNavMenuComponent AuthNavMenuComponent,
BrowserOnlyMockPipe
], ],
providers: [ providers: [
{ provide: HostWindowService, useValue: window }, { provide: HostWindowService, useValue: window },

View File

@@ -8,6 +8,6 @@
</ng-container> </ng-container>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" data-test="register">{{"login.form.new-user" | translate}}</a> <a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly">{{"login.form.new-user" | translate}}</a>
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" data-test="forgot">{{"login.form.forgot-password" | translate}}</a> <a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly">{{"login.form.forgot-password" | translate}}</a>
</div> </div>

View File

@@ -10,7 +10,7 @@
placeholder="{{'login.form.email' | translate}}" placeholder="{{'login.form.email' | translate}}"
required required
type="email" type="email"
data-test="email"> [attr.data-test]="'email' | dsBrowserOnly">
<label class="sr-only">{{"login.form.password" | translate}}</label> <label class="sr-only">{{"login.form.password" | translate}}</label>
<input [attr.aria-label]="'login.form.password' |translate" <input [attr.aria-label]="'login.form.password' |translate"
autocomplete="off" autocomplete="off"
@@ -19,12 +19,12 @@
formControlName="password" formControlName="password"
required required
type="password" type="password"
data-test="password"> [attr.data-test]="'password' | dsBrowserOnly">
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert" <div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert"
@fadeOut>{{ (error | async) | translate }}</div> @fadeOut>{{ (error | async) | translate }}</div>
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert" <div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert"
@fadeOut>{{ (message | async) | translate }}</div> @fadeOut>{{ (message | async) | translate }}</div>
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" data-test="login-button" <button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [attr.data-test]="'login-button' | dsBrowserOnly"
[disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button> [disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button>
</form> </form>

View File

@@ -17,6 +17,7 @@ import { storeModuleConfig } from '../../../../app.reducer';
import { AuthMethod } from '../../../../core/auth/models/auth.method'; import { AuthMethod } from '../../../../core/auth/models/auth.method';
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
describe('LogInPasswordComponent', () => { describe('LogInPasswordComponent', () => {
@@ -57,7 +58,8 @@ describe('LogInPasswordComponent', () => {
TranslateModule.forRoot() TranslateModule.forRoot()
], ],
declarations: [ declarations: [
LogInPasswordComponent LogInPasswordComponent,
BrowserOnlyMockPipe,
], ],
providers: [ providers: [
{ provide: AuthService, useClass: AuthServiceStub }, { provide: AuthService, useClass: AuthServiceStub },

View File

@@ -2,5 +2,5 @@
<div *ngIf="(error | async)" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div> <div *ngIf="(error | async)" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div>
<button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()" data-test="logout-button"><i class="fas fa-sign-out-alt"></i> {{"logout.form.submit" | translate}}</button> <button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()" [attr.data-test]="'logout-button' | dsBrowserOnly"><i class="fas fa-sign-out-alt"></i> {{"logout.form.submit" | translate}}</button>
</div> </div>

View File

@@ -12,6 +12,7 @@ import { Router } from '@angular/router';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { LogOutComponent } from './log-out.component'; import { LogOutComponent } from './log-out.component';
import { RouterStub } from '../testing/router.stub'; import { RouterStub } from '../testing/router.stub';
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
describe('LogOutComponent', () => { describe('LogOutComponent', () => {
@@ -46,7 +47,8 @@ describe('LogOutComponent', () => {
TranslateModule.forRoot() TranslateModule.forRoot()
], ],
declarations: [ declarations: [
LogOutComponent LogOutComponent,
BrowserOnlyMockPipe,
], ],
providers: [ providers: [
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },

View File

@@ -18,7 +18,7 @@
> >
<div class="card-columns row" *ngIf="objects?.hasSucceeded"> <div class="card-columns row" *ngIf="objects?.hasSucceeded">
<div class="card-column col col-sm-6 col-lg-4" *ngFor="let column of (columns$ | async)" @fadeIn> <div class="card-column col col-sm-6 col-lg-4" *ngFor="let column of (columns$ | async)" @fadeIn>
<div class="card-element" *ngFor="let object of column" data-test="grid-object"> <div class="card-element" *ngFor="let object of column" [attr.data-test]="'grid-object' | dsBrowserOnly">
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context" [linkType]="linkType"></ds-listable-object-component-loader> <ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context" [linkType]="linkType"></ds-listable-object-component-loader>
</div> </div>
</div> </div>

View File

@@ -17,7 +17,7 @@
(next)="goNext()" (next)="goNext()"
> >
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}"> <ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}">
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" data-test="list-object"> <li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" [attr.data-test]="'list-object' | dsBrowserOnly">
<ds-selectable-list-item-control *ngIf="selectable" [index]="i" <ds-selectable-list-item-control *ngIf="selectable" [index]="i"
[object]="object" [object]="object"
[selectionConfig]="selectionConfig" [selectionConfig]="selectionConfig"

View File

@@ -4,10 +4,10 @@
<div *ngIf="showScopeSelector === true" class="input-group-prepend"> <div *ngIf="showScopeSelector === true" class="input-group-prepend">
<button class="scope-button btn btn-outline-secondary text-truncate" [ngbTooltip]="(selectedScope | async)?.name" type="button" (click)="openScopeModal()">{{(selectedScope | async)?.name || ('search.form.scope.all' | translate)}}</button> <button class="scope-button btn btn-outline-secondary text-truncate" [ngbTooltip]="(selectedScope | async)?.name" type="button" (click)="openScopeModal()">{{(selectedScope | async)?.name || ('search.form.scope.all' | translate)}}</button>
</div> </div>
<input type="text" [(ngModel)]="query" name="query" class="form-control" attr.aria-label="{{ searchPlaceholder }}" data-test="search-box" <input type="text" [(ngModel)]="query" name="query" class="form-control" attr.aria-label="{{ searchPlaceholder }}" [attr.data-test]="'search-box' | dsBrowserOnly"
[placeholder]="searchPlaceholder"> [placeholder]="searchPlaceholder">
<span class="input-group-append"> <span class="input-group-append">
<button type="submit" class="search-button btn btn-{{brandColor}}" data-test="search-button"><i class="fas fa-search"></i> {{ ('search.form.search' | translate) }}</button> <button type="submit" class="search-button btn btn-{{brandColor}}" [attr.data-test]="'search-button' | dsBrowserOnly"><i class="fas fa-search"></i> {{ ('search.form.search' | translate) }}</button>
</span> </span>
</div> </div>
</div> </div>

View File

@@ -14,6 +14,7 @@ import { PaginationServiceStub } from '../testing/pagination-service.stub';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { FindListOptions } from '../../core/data/find-list-options.model'; import { FindListOptions } from '../../core/data/find-list-options.model';
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
describe('SearchFormComponent', () => { describe('SearchFormComponent', () => {
let comp: SearchFormComponent; let comp: SearchFormComponent;
@@ -37,7 +38,10 @@ describe('SearchFormComponent', () => {
{ provide: SearchConfigurationService, useValue: searchConfigService }, { provide: SearchConfigurationService, useValue: searchConfigService },
{ provide: DSpaceObjectDataService, useValue: { findById: () => createSuccessfulRemoteDataObject$(undefined)} } { provide: DSpaceObjectDataService, useValue: { findById: () => createSuccessfulRemoteDataObject$(undefined)} }
], ],
declarations: [SearchFormComponent] declarations: [
SearchFormComponent,
BrowserOnlyMockPipe,
]
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -4,6 +4,7 @@
class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId" class="filter-name d-flex" [attr.aria-controls]="regionId" [id]="toggleId"
[attr.aria-expanded]="false" [attr.aria-expanded]="false"
[attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate" [attr.aria-label]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate"
[attr.data-test]="'filter-toggle' | dsBrowserOnly"
> >
<h5 class="d-inline-block mb-0"> <h5 class="d-inline-block mb-0">
{{'search.filters.filter.' + filter.name + '.head'| translate}} {{'search.filters.filter.' + filter.name + '.head'| translate}}

View File

@@ -13,6 +13,7 @@ import { FilterType } from '../../models/filter-type.model';
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-page.component';
import { SequenceService } from '../../../../core/shared/sequence.service'; import { SequenceService } from '../../../../core/shared/sequence.service';
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
describe('SearchFilterComponent', () => { describe('SearchFilterComponent', () => {
let comp: SearchFilterComponent; let comp: SearchFilterComponent;
@@ -62,7 +63,10 @@ describe('SearchFilterComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
declarations: [SearchFilterComponent], declarations: [
SearchFilterComponent,
BrowserOnlyMockPipe,
],
providers: [ providers: [
{ provide: SearchService, useValue: searchServiceStub }, { provide: SearchService, useValue: searchServiceStub },
{ {

View File

@@ -173,7 +173,7 @@ import { DsSelectComponent } from './ds-select/ds-select.component';
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component'; import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component';
import { BrowserOnlyPipe } from './utils/browser-only.pipe';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
CommonModule, CommonModule,
@@ -215,7 +215,8 @@ const PIPES = [
ObjectKeysPipe, ObjectKeysPipe,
ObjectValuesPipe, ObjectValuesPipe,
ConsolePipe, ConsolePipe,
ObjNgFor ObjNgFor,
BrowserOnlyPipe,
]; ];
const COMPONENTS = [ const COMPONENTS = [

View File

@@ -1,5 +1,5 @@
<div class="facet-filter d-block mb-3 p-3"> <div class="facet-filter d-block mb-3 p-3">
<div (click)="toggle()" class="filter-name"> <div (click)="toggle()" class="filter-name" [attr.data-test]="'filter-toggle' | dsBrowserOnly">
<h5 class="d-inline-block mb-0"> <h5 class="d-inline-block mb-0">
{{ label | translate }} {{ label | translate }}
</h5> </h5>

View File

@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
/**
* Support dsBrowserOnly in unit tests.
*/
@Pipe({
name: 'dsBrowserOnly'
})
export class BrowserOnlyMockPipe implements PipeTransform {
transform(value: string): string | undefined {
return value;
}
}

View File

@@ -5,6 +5,7 @@ import { SharedModule } from '../shared.module';
import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive.stub'; import { NgComponentOutletDirectiveStub } from './ng-component-outlet-directive.stub';
import { QueryParamsDirectiveStub } from './query-params-directive.stub'; import { QueryParamsDirectiveStub } from './query-params-directive.stub';
import { RouterLinkDirectiveStub } from './router-link-directive.stub'; import { RouterLinkDirectiveStub } from './router-link-directive.stub';
import { BrowserOnlyMockPipe } from './browser-only-mock.pipe';
/** /**
* This module isn't used. It serves to prevent the AoT compiler * This module isn't used. It serves to prevent the AoT compiler
@@ -21,7 +22,8 @@ import { RouterLinkDirectiveStub } from './router-link-directive.stub';
QueryParamsDirectiveStub, QueryParamsDirectiveStub,
MySimpleItemActionComponent, MySimpleItemActionComponent,
RouterLinkDirectiveStub, RouterLinkDirectiveStub,
NgComponentOutletDirectiveStub NgComponentOutletDirectiveStub,
BrowserOnlyMockPipe,
], ],
exports: [ exports: [
QueryParamsDirectiveStub QueryParamsDirectiveStub

View File

@@ -0,0 +1,24 @@
import { Inject, Pipe, PipeTransform, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
/**
* A pipe that only returns its intput when run in the browser.
* Used to distinguish client-side rendered content from server-side rendered content.
*/
@Pipe({
name: 'dsBrowserOnly'
})
export class BrowserOnlyPipe implements PipeTransform {
constructor(
@Inject(PLATFORM_ID) private platformID: any,
) {
}
transform(value: string): string | undefined {
if (isPlatformBrowser((this.platformID))) {
return value;
} else {
return undefined;
}
}
}

View File

@@ -7,7 +7,7 @@
routerLinkActive="active" routerLinkActive="active"
[class.active]="currentMode === viewModeEnum.ListElement" [class.active]="currentMode === viewModeEnum.ListElement"
class="btn btn-secondary" class="btn btn-secondary"
data-test="list-view"> [attr.data-test]="'list-view' | dsBrowserOnly">
<i class="fas fa-list" title="{{'search.view-switch.show-list' | translate}}"></i> <i class="fas fa-list" title="{{'search.view-switch.show-list' | translate}}"></i>
</a> </a>
<a *ngIf="isToShow(viewModeEnum.GridElement)" <a *ngIf="isToShow(viewModeEnum.GridElement)"
@@ -18,7 +18,7 @@
routerLinkActive="active" routerLinkActive="active"
[class.active]="currentMode === viewModeEnum.GridElement" [class.active]="currentMode === viewModeEnum.GridElement"
class="btn btn-secondary" class="btn btn-secondary"
data-test="grid-view"> [attr.data-test]="'grid-view' | dsBrowserOnly">
<i class="fas fa-th-large" title="{{'search.view-switch.show-grid' | translate}}"></i> <i class="fas fa-th-large" title="{{'search.view-switch.show-grid' | translate}}"></i>
</a> </a>
<a *ngIf="isToShow(viewModeEnum.DetailedListElement)" <a *ngIf="isToShow(viewModeEnum.DetailedListElement)"
@@ -29,7 +29,7 @@
routerLinkActive="active" routerLinkActive="active"
[class.active]="currentMode === viewModeEnum.DetailedListElement" [class.active]="currentMode === viewModeEnum.DetailedListElement"
class="btn btn-secondary" class="btn btn-secondary"
data-test="detail-view"> [attr.data-test]="'detail-view' | dsBrowserOnly">
<i class="far fa-square" title="{{'search.view-switch.show-detail' | translate}}"></i> <i class="far fa-square" title="{{'search.view-switch.show-detail' | translate}}"></i>
</a> </a>
</div> </div>

View File

@@ -9,6 +9,7 @@ import { SearchService } from '../../core/shared/search/search.service';
import { ViewModeSwitchComponent } from './view-mode-switch.component'; import { ViewModeSwitchComponent } from './view-mode-switch.component';
import { SearchServiceStub } from '../testing/search-service.stub'; import { SearchServiceStub } from '../testing/search-service.stub';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe';
@Component({ template: '' }) @Component({ template: '' })
class DummyComponent { class DummyComponent {
@@ -36,7 +37,8 @@ describe('ViewModeSwitchComponent', () => {
], ],
declarations: [ declarations: [
ViewModeSwitchComponent, ViewModeSwitchComponent,
DummyComponent DummyComponent,
BrowserOnlyMockPipe,
], ],
providers: [ providers: [
{ provide: SearchService, useValue: searchService }, { provide: SearchService, useValue: searchService },

View File

@@ -3,6 +3,7 @@
<button *ngIf="(showDepositAndDiscard | async)" <button *ngIf="(showDepositAndDiscard | async)"
type="button" type="button"
id="discard" id="discard"
[attr.data-test]="'discard' | dsBrowserOnly"
class="btn btn-danger" class="btn btn-danger"
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)" [disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
(click)="$event.preventDefault();confirmDiscard(content)"> (click)="$event.preventDefault();confirmDiscard(content)">
@@ -26,6 +27,7 @@
<button type="button" <button type="button"
class="btn btn-secondary" class="btn btn-secondary"
id="save" id="save"
[attr.data-test]="'save' | dsBrowserOnly"
[disabled]="(processingSaveStatus | async) || !(hasUnsavedModification | async)" [disabled]="(processingSaveStatus | async) || !(hasUnsavedModification | async)"
(click)="save($event)"> (click)="save($event)">
<span><i class="fas fa-save"></i> {{'submission.general.save' | translate}}</span> <span><i class="fas fa-save"></i> {{'submission.general.save' | translate}}</span>
@@ -35,6 +37,7 @@
[class.btn-secondary]="(showDepositAndDiscard | async)" [class.btn-secondary]="(showDepositAndDiscard | async)"
class="btn" class="btn"
id="saveForLater" id="saveForLater"
[attr.data-test]="'save-for-later' | dsBrowserOnly"
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)" [disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
(click)="saveLater($event)"> (click)="saveLater($event)">
<span><i class="fas fa-save"></i> {{'submission.general.save-later' | translate}}</span> <span><i class="fas fa-save"></i> {{'submission.general.save-later' | translate}}</span>
@@ -42,6 +45,7 @@
<button *ngIf="(showDepositAndDiscard | async)" <button *ngIf="(showDepositAndDiscard | async)"
type="button" type="button"
id="deposit" id="deposit"
[attr.data-test]="'deposit' | dsBrowserOnly"
class="btn btn-success" class="btn btn-success"
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)" [disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
(click)="deposit($event)"> (click)="deposit($event)">

View File

@@ -15,12 +15,13 @@ import { SubmissionRestServiceStub } from '../../../shared/testing/submission-re
import { SubmissionFormFooterComponent } from './submission-form-footer.component'; import { SubmissionFormFooterComponent } from './submission-form-footer.component';
import { SubmissionRestService } from '../../../core/submission/submission-rest.service'; import { SubmissionRestService } from '../../../core/submission/submission-rest.service';
import { createTestComponent } from '../../../shared/testing/utils.test'; import { createTestComponent } from '../../../shared/testing/utils.test';
import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe';
const submissionServiceStub: SubmissionServiceStub = new SubmissionServiceStub(); const submissionServiceStub: SubmissionServiceStub = new SubmissionServiceStub();
const submissionId = mockSubmissionId; const submissionId = mockSubmissionId;
describe('SubmissionFormFooterComponent Component', () => { describe('SubmissionFormFooterComponent', () => {
let comp: SubmissionFormFooterComponent; let comp: SubmissionFormFooterComponent;
let compAsAny: any; let compAsAny: any;
@@ -36,7 +37,8 @@ describe('SubmissionFormFooterComponent Component', () => {
], ],
declarations: [ declarations: [
SubmissionFormFooterComponent, SubmissionFormFooterComponent,
TestComponent TestComponent,
BrowserOnlyMockPipe,
], ],
providers: [ providers: [
{ provide: SubmissionService, useValue: submissionServiceStub }, { provide: SubmissionService, useValue: submissionServiceStub },