blob: 7e2f499b298b1d2e2f7f0131c688ac420befd57d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter, ActivatedRoute, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
import { AccessDeniedComponent } from './access-denied.component';
import { renderComponent } from '../../testing/render';
import { provideIonicTesting } from '../../testing/ionic-testing';
import { provideFakeAdapters } from '../../testing/adapters';
describe('AccessDeniedComponent', () => {
const REQUIRED_TESTID = '[data-testid="access-denied-required"]';
let fixture: ComponentFixture<AccessDeniedComponent>;
let host: HTMLElement;
/** Renders the page as the guard would have left it, with the given `required` query param. */
async function render(required?: string): Promise<void> {
// Each case configures its own query params, so the module from the previous one has to go.
TestBed.resetTestingModule();
fixture = await renderComponent(AccessDeniedComponent, {
providers: [
provideRouter([]),
...provideIonicTesting(),
...provideFakeAdapters().providers,
{
provide: ActivatedRoute,
useValue: {
queryParamMap: of(convertToParamMap(required ? { required } : {})),
},
},
],
});
host = fixture.nativeElement as HTMLElement;
}
beforeEach(async () => {
await render();
});
it('states what happened in a single top-level heading', () => {
const headings = host.querySelectorAll('h1');
expect(headings).toHaveLength(1);
expect(headings[0].textContent?.trim()).toBeTruthy();
});
it('takes focus on the heading, because the user did not ask for this navigation', () => {
// A guard redirect changes the page without the user acting. Leaving focus wherever the
// previous screen left it gives a screen-reader user no reason for the change.
expect(document.activeElement).toBe(host.querySelector('h1'));
});
it('announces itself without interrupting', () => {
const region = host.querySelector('[aria-live]');
expect(region).not.toBeNull();
expect(region?.getAttribute('aria-live')).toBe('polite');
expect(region?.getAttribute('role')).toBe('alert');
});
it('offers a way out that does not depend on any permission', () => {
const back = host.querySelector('[data-testid="access-denied-dashboard"]');
expect(back).not.toBeNull();
expect(back?.getAttribute('ng-reflect-router-link') ?? back?.outerHTML).toContain('/dashboard');
});
it('renders translated copy rather than raw keys', () => {
// The fake i18n adapter echoes the key back, so a missed `| appTranslate` would show up as
// the literal key. Every visible string must have gone through the pipe.
const text = host.textContent ?? '';
expect(text).toContain('ACCESS_DENIED.TITLE');
expect(text).toContain('ACCESS_DENIED.MESSAGE');
expect(text).toContain('ACCESS_DENIED.HINT');
expect(text).toContain('ACCESS_DENIED.BACK_TO_DASHBOARD');
});
it('hides the decorative icon from assistive technology', () => {
expect(host.querySelector('ion-icon')?.getAttribute('aria-hidden')).toBe('true');
});
describe('the permissions the screen wanted', () => {
it('names them when the guard passed them on', async () => {
await render('READ_JOURNALENTRY,CREATE_JOURNALENTRY');
const required = host.querySelector(REQUIRED_TESTID);
expect(required?.textContent).toContain('READ_JOURNALENTRY');
expect(required?.textContent).toContain('CREATE_JOURNALENTRY');
});
it('also says what they mean, for a reader who does not speak in codes', async () => {
await render('READ_JOURNALENTRY,CREATE_JOURNALENTRY');
expect(host.querySelector(REQUIRED_TESTID)?.textContent).toContain(
'View and create journal entries',
);
});
it('says nothing at all when there are none to name', () => {
// Reached directly rather than by refusal: inventing a requirement would be worse than
// leaving the sentence out.
expect(host.querySelector(REQUIRED_TESTID)).toBeNull();
});
it('ignores empty entries rather than rendering a stray separator', async () => {
await render('READ_CLIENT,,');
const required = host.querySelector(REQUIRED_TESTID);
expect(required?.textContent).toContain('READ_CLIENT');
expect(required?.textContent).not.toContain(', ,');
});
});
});