feat(docs-infra): implement popup to inform about the use of cookies (#42259)
This commit adds a popup to angular.io to inform the user about the use of cookies. Once the user confirms having read the info, the popup will not be shown on subsequent visits. This commit is partly based on angular/material.angular.io#988. Fixes #42209 PR Close #42259
This commit is contained in:
parent
1a6a79b63a
commit
828fde6e0d
|
@ -1,5 +1,7 @@
|
||||||
<div id="top-of-page"></div>
|
<div id="top-of-page"></div>
|
||||||
|
|
||||||
|
<aio-cookies-popup></aio-cookies-popup>
|
||||||
|
|
||||||
<div *ngIf="isFetching" class="progress-bar-container">
|
<div *ngIf="isFetching" class="progress-bar-container">
|
||||||
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
|
<mat-progress-bar mode="indeterminate" color="warn"></mat-progress-bar>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { MatSidenav } from '@angular/material/sidenav';
|
||||||
import { By, Title } from '@angular/platform-browser';
|
import { By, Title } from '@angular/platform-browser';
|
||||||
import { ElementsLoader } from 'app/custom-elements/elements-loader';
|
import { ElementsLoader } from 'app/custom-elements/elements-loader';
|
||||||
import { DocumentService } from 'app/documents/document.service';
|
import { DocumentService } from 'app/documents/document.service';
|
||||||
|
import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
import { CurrentNodes } from 'app/navigation/navigation.model';
|
import { CurrentNodes } from 'app/navigation/navigation.model';
|
||||||
import { NavigationNode, NavigationService } from 'app/navigation/navigation.service';
|
import { NavigationNode, NavigationService } from 'app/navigation/navigation.service';
|
||||||
|
@ -701,6 +702,13 @@ describe('AppComponent', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('aio-cookies-popup', () => {
|
||||||
|
it('should have a cookies popup', () => {
|
||||||
|
const cookiesPopupDe = fixture.debugElement.query(By.directive(CookiesPopupComponent));
|
||||||
|
expect(cookiesPopupDe.componentInstance).toBeInstanceOf(CookiesPopupComponent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deployment banner', () => {
|
describe('deployment banner', () => {
|
||||||
it('should show a message if the deployment mode is "archive"', async () => {
|
it('should show a message if the deployment mode is "archive"', async () => {
|
||||||
createTestingModule('a/b', 'archive');
|
createTestingModule('a/b', 'archive');
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { AppComponent } from 'app/app.component';
|
import { AppComponent } from 'app/app.component';
|
||||||
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
|
import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry';
|
||||||
import { Deployment } from 'app/shared/deployment.service';
|
import { Deployment } from 'app/shared/deployment.service';
|
||||||
|
import { CookiesPopupComponent } from 'app/layout/cookies-popup/cookies-popup.component';
|
||||||
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
|
||||||
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
|
import { DtComponent } from 'app/layout/doc-viewer/dt.component';
|
||||||
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
|
import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component';
|
||||||
|
@ -163,6 +164,7 @@ export const svgIconProviders = [
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
CookiesPopupComponent,
|
||||||
DocViewerComponent,
|
DocViewerComponent,
|
||||||
DtComponent,
|
DtComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { LocalStorage } from 'app/shared/storage.service';
|
||||||
|
import { CookiesPopupComponent, storageKey } from './cookies-popup.component';
|
||||||
|
|
||||||
|
describe('CookiesPopupComponent', () => {
|
||||||
|
let mockLocalStorage: MockLocalStorage;
|
||||||
|
let fixture: ComponentFixture<CookiesPopupComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockLocalStorage = new MockLocalStorage();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
CookiesPopupComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: LocalStorage, useValue: mockLocalStorage },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CookiesPopupComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make the popup visible by default', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getCookiesPopup()).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include the correct content in the popup', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const popup = getCookiesPopup() as Element;
|
||||||
|
const infoBtn = popup.querySelector<HTMLAnchorElement>('a[mat-button]:nth-child(1)');
|
||||||
|
const okBtn = popup.querySelector<HTMLButtonElement>('button[mat-button]:nth-child(2)');
|
||||||
|
|
||||||
|
expect(popup.textContent).toContain(
|
||||||
|
'This site uses cookies from Google to deliver its services and to analyze traffic.');
|
||||||
|
|
||||||
|
expect(infoBtn).toBeInstanceOf(HTMLElement);
|
||||||
|
expect(infoBtn?.href).toBe('https://policies.google.com/technologies/cookies');
|
||||||
|
expect(infoBtn?.textContent).toMatch(/learn more/i);
|
||||||
|
|
||||||
|
expect(okBtn).toBeInstanceOf(HTMLElement);
|
||||||
|
expect(okBtn?.textContent).toMatch(/ok, got it/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide the cookies popup if the user has already accepted cookies', () => {
|
||||||
|
mockLocalStorage.setItem(storageKey, 'true');
|
||||||
|
fixture = TestBed.createComponent(CookiesPopupComponent);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getCookiesPopup()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('acceptCookies()', () => {
|
||||||
|
it('should hide the cookies popup', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getCookiesPopup()).not.toBeNull();
|
||||||
|
|
||||||
|
fixture.componentInstance.acceptCookies();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getCookiesPopup()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store the user\'s confirmation', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(mockLocalStorage.getItem(storageKey)).toBeNull();
|
||||||
|
|
||||||
|
fixture.componentInstance.acceptCookies();
|
||||||
|
expect(mockLocalStorage.getItem(storageKey)).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
function getCookiesPopup() {
|
||||||
|
return (fixture.nativeElement as HTMLElement).querySelector('.cookies-popup');
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockLocalStorage implements Pick<Storage, 'getItem' | 'setItem'> {
|
||||||
|
private items = new Map<string, string>();
|
||||||
|
|
||||||
|
getItem(key: string): string | null {
|
||||||
|
return this.items.get(key) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key: string, val: string): void {
|
||||||
|
this.items.set(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { LocalStorage } from 'app/shared/storage.service';
|
||||||
|
|
||||||
|
export const storageKey = 'aio-accepts-cookies';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'aio-cookies-popup',
|
||||||
|
template: `
|
||||||
|
<div class="cookies-popup no-print" *ngIf="!hasAcceptedCookies">
|
||||||
|
<h2 class="visually-hidden">Cookies concent notice</h2>
|
||||||
|
|
||||||
|
This site uses cookies from Google to deliver its services and to analyze traffic.
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a mat-button href="https://policies.google.com/technologies/cookies" target="_blank" rel="noopener">
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
<button mat-button (click)="acceptCookies()">
|
||||||
|
OK, got it
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class CookiesPopupComponent {
|
||||||
|
/** Whether the user has already accepted the cookies disclaimer. */
|
||||||
|
hasAcceptedCookies: boolean;
|
||||||
|
|
||||||
|
constructor(@Inject(LocalStorage) private storage: Storage) {
|
||||||
|
this.hasAcceptedCookies = this.storage.getItem(storageKey) === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptCookies() {
|
||||||
|
this.storage.setItem(storageKey, 'true');
|
||||||
|
this.hasAcceptedCookies = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@
|
||||||
@forward 'cli-pages/cli-pages';
|
@forward 'cli-pages/cli-pages';
|
||||||
@forward 'code/code';
|
@forward 'code/code';
|
||||||
@forward 'contribute/contribute';
|
@forward 'contribute/contribute';
|
||||||
|
@forward 'cookies-popup/cookies-popup';
|
||||||
@forward 'contributor/contributor';
|
@forward 'contributor/contributor';
|
||||||
@forward 'deploy-theme/deploy-theme';
|
@forward 'deploy-theme/deploy-theme';
|
||||||
@forward 'details/details';
|
@forward 'details/details';
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
@use 'card/card-theme';
|
@use 'card/card-theme';
|
||||||
@use 'code/code-theme';
|
@use 'code/code-theme';
|
||||||
@use 'contributor/contributor-theme';
|
@use 'contributor/contributor-theme';
|
||||||
|
@use 'cookies-popup/cookies-popup-theme';
|
||||||
@use 'deploy-theme/deploy-theme';
|
@use 'deploy-theme/deploy-theme';
|
||||||
@use 'details/details-theme';
|
@use 'details/details-theme';
|
||||||
@use 'errors/errors-theme';
|
@use 'errors/errors-theme';
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
@include card-theme.theme($theme);
|
@include card-theme.theme($theme);
|
||||||
@include code-theme.theme($theme);
|
@include code-theme.theme($theme);
|
||||||
@include contributor-theme.theme($theme);
|
@include contributor-theme.theme($theme);
|
||||||
|
@include cookies-popup-theme.theme($theme);
|
||||||
@include deploy-theme.theme($theme);
|
@include deploy-theme.theme($theme);
|
||||||
@include details-theme.theme($theme);
|
@include details-theme.theme($theme);
|
||||||
@include errors-theme.theme($theme);
|
@include errors-theme.theme($theme);
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
@use 'sass:map';
|
||||||
|
@use '../../constants' as c;
|
||||||
|
@use '~@angular/material' as mat;
|
||||||
|
|
||||||
|
@mixin theme($theme) {
|
||||||
|
$is-dark-theme: map.get($theme, is-dark);
|
||||||
|
|
||||||
|
aio-cookies-popup {
|
||||||
|
.cookies-popup {
|
||||||
|
background: if($is-dark-theme, map.get(mat.$grey-palette, 50), #252525);
|
||||||
|
color: if($is-dark-theme,
|
||||||
|
map.get(map.get(mat.$grey-palette, contrast), 50),
|
||||||
|
map.get(mat.$dark-theme-foreground-palette, secondary-text)
|
||||||
|
);
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
.mat-button {
|
||||||
|
color: if($is-dark-theme, c.$blue, c.$lightblue);
|
||||||
|
|
||||||
|
.mat-button-focus-overlay {
|
||||||
|
background: if($is-dark-theme, c.$black, c.$white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
@use '~@angular/cdk' as cdk;
|
||||||
|
@use '~@angular/material' as mat;
|
||||||
|
|
||||||
|
$inner-spacing: 16px;
|
||||||
|
|
||||||
|
aio-cookies-popup {
|
||||||
|
.cookies-popup {
|
||||||
|
@include mat.elevation(6);
|
||||||
|
border-radius: 4px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
position: fixed;
|
||||||
|
margin: 24px;
|
||||||
|
max-width: 430px;
|
||||||
|
padding: $inner-spacing $inner-spacing $inner-spacing / 2;
|
||||||
|
z-index: cdk.$overlay-container-z-index + 1;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: $inner-spacing $inner-spacing / -2 0 0;
|
||||||
|
|
||||||
|
.mat-button {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ export class SitePage {
|
||||||
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
|
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
|
||||||
sidenav = element(by.css('mat-sidenav'));
|
sidenav = element(by.css('mat-sidenav'));
|
||||||
docViewer = element(by.css('aio-doc-viewer'));
|
docViewer = element(by.css('aio-doc-viewer'));
|
||||||
|
cookiesPopup = element(by.css('.cookies-popup'));
|
||||||
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
||||||
ghLinks = this.docViewer
|
ghLinks = this.docViewer
|
||||||
.all(by.css('a'))
|
.all(by.css('a'))
|
||||||
|
@ -39,10 +40,15 @@ export class SitePage {
|
||||||
ga() { return browser.executeScript<any[][]>('return window["ga"].q'); }
|
ga() { return browser.executeScript<any[][]>('return window["ga"].q'); }
|
||||||
locationPath() { return browser.executeScript<string>('return document.location.pathname'); }
|
locationPath() { return browser.executeScript<string>('return document.location.pathname'); }
|
||||||
|
|
||||||
async navigateTo(pageUrl: string) {
|
async navigateTo(pageUrl: string, keepCookiesPopup = false) {
|
||||||
// Navigate to the page, disable animations, and wait for Angular.
|
// Navigate to the page, disable animations, potentially hide the cookies popup, and wait for
|
||||||
|
// Angular.
|
||||||
await browser.get(`/${pageUrl.replace(/^\//, '')}`);
|
await browser.get(`/${pageUrl.replace(/^\//, '')}`);
|
||||||
await browser.executeScript('document.body.classList.add(\'no-animations\')');
|
await browser.executeScript('document.body.classList.add(\'no-animations\')');
|
||||||
|
if (!keepCookiesPopup) {
|
||||||
|
// Hide the cookies popup to prevent it from obscuring other elements.
|
||||||
|
await browser.executeScript('arguments[0].remove()', this.cookiesPopup);
|
||||||
|
}
|
||||||
await browser.waitForAngular();
|
await browser.waitForAngular();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { browser, by } from 'protractor';
|
||||||
|
import { SitePage } from './app.po';
|
||||||
|
|
||||||
|
describe('cookies popup', () => {
|
||||||
|
const getButton = (idx: number) => page.cookiesPopup.all(by.css('.actions .mat-button')).get(idx);
|
||||||
|
let page: SitePage;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
page = new SitePage();
|
||||||
|
await page.navigateTo('', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => browser.executeScript('localStorage.clear()'));
|
||||||
|
|
||||||
|
it('should be shown by default', async () => {
|
||||||
|
expect(await page.cookiesPopup.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open a new tab with more info when clicking the first button', async () => {
|
||||||
|
// Click the "Learn more" button.
|
||||||
|
await page.click(getButton(0));
|
||||||
|
|
||||||
|
// Switch to the newly opened tab.
|
||||||
|
const originalWindowHandle = await browser.getWindowHandle();
|
||||||
|
const openedWindowHandle = (await browser.getAllWindowHandles()).pop() as string;
|
||||||
|
await browser.switchTo().window(openedWindowHandle);
|
||||||
|
await browser.waitForAngularEnabled(false);
|
||||||
|
|
||||||
|
// Verify the tab's URL.
|
||||||
|
expect(await browser.getCurrentUrl()).toBe('https://policies.google.com/technologies/cookies');
|
||||||
|
|
||||||
|
// Close the tab and switch back to the original tab.
|
||||||
|
await browser.waitForAngularEnabled(true);
|
||||||
|
await browser.close();
|
||||||
|
await browser.switchTo().window(originalWindowHandle);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not hide the popup when clicking the first button', async () => {
|
||||||
|
await page.click(getButton(0));
|
||||||
|
|
||||||
|
expect(await page.cookiesPopup.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide the popup when clicking the second button', async () => {
|
||||||
|
expect(await page.cookiesPopup.isDisplayed()).toBe(true);
|
||||||
|
|
||||||
|
await page.click(getButton(1));
|
||||||
|
expect(await page.cookiesPopup.isPresent()).toBe(false);
|
||||||
|
|
||||||
|
await page.navigateTo('', true);
|
||||||
|
expect(await page.cookiesPopup.isPresent()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,7 +3,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2017": 4619,
|
"runtime-es2017": 4619,
|
||||||
"main-es2017": 454003,
|
"main-es2017": 456795,
|
||||||
"polyfills-es2017": 55210
|
"polyfills-es2017": 55210
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2017": 4619,
|
"runtime-es2017": 4619,
|
||||||
"main-es2017": 454138,
|
"main-es2017": 456930,
|
||||||
"polyfills-es2017": 55348
|
"polyfills-es2017": 55348
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue