Skip to content
Merged

PD-5715 #2912

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 140 additions & 10 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,88 @@ import { TestBed } from '@angular/core/testing'
import { RouterTestingModule } from '@angular/router/testing'
import { AppComponent } from './app.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog'
import { MatDialog } from '@angular/material/dialog'
import { WINDOW_PROVIDERS } from './cdk/window'
import { FormBuilder } from '@angular/forms'
import { RecordWorksService } from './core/record-works/record-works.service'
import { PlatformInfoService } from './cdk/platform-info'
import { PlatformInfo, PlatformInfoService } from './cdk/platform-info'
import { ErrorHandlerService } from './core/error-handler/error-handler.service'
import { SnackbarService } from './cdk/snackbar/snackbar.service'
import { MatSnackBar } from '@angular/material/snack-bar'
import { Overlay } from '@angular/cdk/overlay'
import { TitleService } from './core/title-service/title.service'
import { of } from 'rxjs'
import { ZendeskService } from './core/zendesk/zendesk.service'
import { BehaviorSubject, of } from 'rxjs'
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'

/**
* Mirrors the defaults PlatformInfoService starts with, including the empty
* `currentRoute` it holds until the first NavigationEnd.
*/
const BASE_PLATFORM_INFO: PlatformInfo = {
rtl: false,
ltr: true,
screenDirection: 'ltr',
unsupportedBrowser: false,
desktop: true,
tabletOrHandset: false,
tablet: false,
handset: false,
edge: false,
ie: false,
safary: false,
firefox: false,
columns4: false,
columns8: false,
columns12: true,
hasOauthParameters: false,
social: false,
institutional: false,
queryParameters: {},
currentRoute: '',
reactivation: false,
reactivationCode: '',
summaryScreen: false,
}

describe('AppComponent', () => {
let platformInfo$: BehaviorSubject<PlatformInfo>
let zendesk: jasmine.SpyObj<ZendeskService>

/** Push a new platform state, the way PlatformInfoService's subject does. */
function emit(overrides: Partial<PlatformInfo>) {
platformInfo$.next({ ...BASE_PLATFORM_INFO, ...overrides })
}

function createApp() {
const fixture = TestBed.createComponent(AppComponent)
fixture.detectChanges()
return fixture
}

beforeEach(() => {
platformInfo$ = new BehaviorSubject<PlatformInfo>({ ...BASE_PLATFORM_INFO })
zendesk = jasmine.createSpyObj<ZendeskService>('ZendeskService', [
'show',
'hide',
'open',
'adaptPluginToPlatform',
'autofillTicketForm',
])

TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule],
declarations: [AppComponent],
providers: [
WINDOW_PROVIDERS,
PlatformInfoService,
ErrorHandlerService,
SnackbarService,
MatSnackBar,
MatDialog,
Overlay,
{
provide: PlatformInfoService,
useValue: { get: () => platformInfo$.asObservable() },
},
{ provide: ZendeskService, useValue: zendesk },
{
provide: TitleService,
useValue: {
Expand All @@ -42,9 +94,87 @@ describe('AppComponent', () => {
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents()
})

it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent)
const app = fixture.debugElement.componentInstance
expect(app).toBeTruthy()
})

describe('Zendesk help widget', () => {
it('hides the widget before the router resolves the first route', () => {
createApp()
expect(zendesk.hide).toHaveBeenCalledTimes(1)
expect(zendesk.show).not.toHaveBeenCalled()
})

// PD-5715 acceptance criteria
it('never shows the widget on the homepage', () => {
createApp()
emit({ currentRoute: '/' })
emit({ currentRoute: '/?utm_source=newsletter' })
expect(zendesk.show).not.toHaveBeenCalled()
expect(zendesk.hide).toHaveBeenCalledTimes(1)
})

// PD-5715 acceptance criteria, and the regression guard for 1892786e5:
// an unconditional _zendesk.hide() in the platformInfo subscription fails
// this test.
it('shows the widget on a registry page that is not the homepage', () => {
createApp()
emit({ currentRoute: '/my-orcid' })
expect(zendesk.show).toHaveBeenCalledTimes(1)
})

it('shows the widget on the 404 page, whose copy points at it', () => {
createApp()
emit({ currentRoute: '/this-route-does-not-exist' })
expect(zendesk.show).toHaveBeenCalledTimes(1)
})

it('hides the widget again when navigating back to the homepage', () => {
createApp()
emit({ currentRoute: '/my-orcid' })
zendesk.hide.calls.reset()
emit({ currentRoute: '/' })
expect(zendesk.hide).toHaveBeenCalledTimes(1)
})

it('does not show the widget while oauth parameters are present', () => {
createApp()
emit({
currentRoute: '/oauth/authorize',
hasOauthParameters: true,
queryParameters: { client_id: 'APP-0000' },
})
expect(zendesk.show).not.toHaveBeenCalled()
})

it('does not show the widget on trusted summary routes', () => {
createApp()
emit({ currentRoute: '/0000-0002-1825-0097/summary' })
expect(zendesk.show).not.toHaveBeenCalled()
})

it('acts on visibility transitions only, not on every platform emission', () => {
createApp()
emit({ currentRoute: '/my-orcid' })
emit({ currentRoute: '/my-orcid', handset: true }) // breakpoint change
emit({ currentRoute: '/my-orcid/works' })
expect(zendesk.show).toHaveBeenCalledTimes(1)
})

it('applies the RTL widget position when it shows the widget', () => {
createApp()
emit({
currentRoute: '/my-orcid',
rtl: true,
ltr: false,
screenDirection: 'rtl',
})
expect(zendesk.adaptPluginToPlatform).toHaveBeenCalledWith(
jasmine.objectContaining({ screenDirection: 'rtl' })
)
})
})
})
57 changes: 52 additions & 5 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import { OneTrustAccessibilityService } from './core/onetrust/onetrust-accessibi
standalone: false,
})
export class AppComponent {
currentlyDisplayingZendesk = true
/**
* The last visibility AppComponent requested from ZendeskService - not a
* claim about the widget's actual state. Starts `true` because the snippet
* in index.html renders the launcher by default.
*/
private zendeskDisplayRequested = true
headlessMode = false
footerlessMode = false
spacing: boolean
Expand All @@ -47,7 +52,7 @@ export class AppComponent {
_platformInfo: PlatformInfoService,
_router: Router,
_googleTagManagerService: GoogleTagManagerService,
_zendesk: ZendeskService,
private _zendesk: ZendeskService,
private _userService: UserService,
private _errorHandler: ErrorHandlerService,
@Inject(WINDOW) private _window: Window,
Expand All @@ -66,9 +71,7 @@ export class AppComponent {
)
this.setPlatformClasses(platformInfo)
this.screenDirection = platformInfo.screenDirection
_zendesk.hide()
_zendesk.adaptPluginToPlatform(platformInfo)
this.currentlyDisplayingZendesk = false
this.updateZendeskVisibility(platformInfo)
})
)
.subscribe()
Expand Down Expand Up @@ -108,6 +111,50 @@ export class AppComponent {
}
})
}

/**
* Routes that must never surface the Zendesk help widget.
*
* An empty `currentRoute` means PlatformInfoService has not seen a
* NavigationEnd yet. Treat it as hidden, so landing on the homepage hides the
* widget at bootstrap instead of after the router resolves the first route.
*/
private isZendeskFreeRoute(currentRoute: string): boolean {
const path = (currentRoute || '').split('?')[0].split('#')[0]
if (!path) {
return true // the router has not resolved a route yet
}
if (path === '/') {
return true // PD-5715: no help widget on the ORCID homepage
}
if (path.endsWith('/summary')) {
return true // the trusted summary screen hides the widget itself
}
return false
}

private updateZendeskVisibility(platformInfo: PlatformInfo) {
const shouldDisplay =
!platformInfo.hasOauthParameters &&
!this.isZendeskFreeRoute(platformInfo.currentRoute)

// platformSubject also fires on breakpoint and query parameter changes.
// Act only on an actual transition, so a window resize does not clobber
// out-of-band callers such as OauthErrorComponent's show() or
// TrustedSummaryComponent's hide().
if (shouldDisplay === this.zendeskDisplayRequested) {
return
}
this.zendeskDisplayRequested = shouldDisplay

if (shouldDisplay) {
this._zendesk.show()
this._zendesk.adaptPluginToPlatform(platformInfo)
} else {
this._zendesk.hide()
}
}

showHeadlessOnOauthPage(currentRoute: string): boolean {
if (currentRoute) {
const value = HeadlessOnOauthRoutes.filter(
Expand Down
66 changes: 62 additions & 4 deletions src/app/core/zendesk/zendesk.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,79 @@
import { TestBed } from '@angular/core/testing'
import { WINDOW } from '../../cdk/window'
import { PlatformInfo } from '../../cdk/platform-info'

import { ZendeskService } from './zendesk.service'
import { WINDOW_PROVIDERS } from '../../cdk/window'

import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'

describe('ZendeskService', () => {
let service: ZendeskService
let fakeWindow: any

beforeEach(() => {
fakeWindow = {
location: { href: 'https://orcid.org/my-orcid' },
}
TestBed.configureTestingModule({
providers: [WINDOW_PROVIDERS],
providers: [{ provide: WINDOW, useValue: fakeWindow }],
})
service = TestBed.inject(ZendeskService)
})

it('should be created', () => {
expect(service).toBeTruthy()
})

/**
* The snippet is a third party script and can be missing entirely. A throw
* here would propagate into AppComponent's platformInfo subscription and
* tear it down for the rest of the session.
*/
it('does not throw when the widget is absent', () => {
expect(() => service.hide()).not.toThrow()
expect(() => service.show()).not.toThrow()
expect(() => service.open()).not.toThrow()
expect(() =>
service.adaptPluginToPlatform({ screenDirection: 'rtl' } as PlatformInfo)
).not.toThrow()
expect(() => service.autofillTicketForm()).not.toThrow()
})

it('forwards commands once the widget is present', () => {
fakeWindow.zE = jasmine.createSpy('zE')

service.hide()
expect(fakeWindow.zE).toHaveBeenCalledWith('webWidget', 'hide')

service.show()
expect(fakeWindow.zE).toHaveBeenCalledWith('webWidget', 'show')

service.open()
expect(fakeWindow.zE).toHaveBeenCalledWith('webWidget', 'open')
})

it('moves the widget to the left on RTL locales', () => {
fakeWindow.zE = jasmine.createSpy('zE')

service.adaptPluginToPlatform({ screenDirection: 'ltr' } as PlatformInfo)
expect(fakeWindow.zE).not.toHaveBeenCalled()

service.adaptPluginToPlatform({ screenDirection: 'rtl' } as PlatformInfo)
expect(fakeWindow.zE).toHaveBeenCalledWith('webWidget', 'updateSettings', {
webWidget: {
position: { horizontal: 'left', vertical: 'bottom' },
},
})
})

it('prefills the ticket form without a prior hide or show call', () => {
fakeWindow.zE = jasmine.createSpy('zE')

service.autofillTicketForm(undefined, 'App Oauth URL with issues')

expect(fakeWindow.zE).toHaveBeenCalled()
const settings = fakeWindow.zE.calls.mostRecent().args[2]
const subject = settings.webWidget.contactForm.fields.find(
(field) => field.id === 'subject'
)
expect(subject.prefill['*']).toBe('App Oauth URL with issues')
})
})
Loading
Loading