From 828fde6e0db3df9a7c2474d0d62d4efbadb114f6 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Fri, 11 Jun 2021 20:14:57 +0300 Subject: [PATCH] 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 --- aio/src/app/app.component.html | 2 + aio/src/app/app.component.spec.ts | 8 ++ aio/src/app/app.module.ts | 2 + .../cookies-popup.component.spec.ts | 92 +++++++++++++++++++ .../cookies-popup/cookies-popup.component.ts | 37 ++++++++ aio/src/styles/2-modules/_index.scss | 1 + aio/src/styles/2-modules/_theme.scss | 2 + .../cookies-popup/_cookies-popup-theme.scss | 27 ++++++ .../cookies-popup/_cookies-popup.scss | 28 ++++++ aio/tests/e2e/src/app.po.ts | 10 +- aio/tests/e2e/src/cookies-popup.e2e-spec.ts | 53 +++++++++++ goldens/size-tracking/aio-payloads.json | 4 +- 12 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 aio/src/app/layout/cookies-popup/cookies-popup.component.spec.ts create mode 100644 aio/src/app/layout/cookies-popup/cookies-popup.component.ts create mode 100644 aio/src/styles/2-modules/cookies-popup/_cookies-popup-theme.scss create mode 100644 aio/src/styles/2-modules/cookies-popup/_cookies-popup.scss create mode 100644 aio/tests/e2e/src/cookies-popup.e2e-spec.ts diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index be0db3b713..a8834fc492 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -1,5 +1,7 @@
+ +
diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index 16967f7ce3..18385db24a 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -7,6 +7,7 @@ import { MatSidenav } from '@angular/material/sidenav'; import { By, Title } from '@angular/platform-browser'; import { ElementsLoader } from 'app/custom-elements/elements-loader'; 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 { CurrentNodes } from 'app/navigation/navigation.model'; 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', () => { it('should show a message if the deployment mode is "archive"', async () => { createTestingModule('a/b', 'archive'); diff --git a/aio/src/app/app.module.ts b/aio/src/app/app.module.ts index 17e8bdba11..4a27d5b562 100644 --- a/aio/src/app/app.module.ts +++ b/aio/src/app/app.module.ts @@ -15,6 +15,7 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { AppComponent } from 'app/app.component'; import { CustomIconRegistry, SVG_ICONS } from 'app/shared/custom-icon-registry'; 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 { DtComponent } from 'app/layout/doc-viewer/dt.component'; import { ModeBannerComponent } from 'app/layout/mode-banner/mode-banner.component'; @@ -163,6 +164,7 @@ export const svgIconProviders = [ ], declarations: [ AppComponent, + CookiesPopupComponent, DocViewerComponent, DtComponent, FooterComponent, diff --git a/aio/src/app/layout/cookies-popup/cookies-popup.component.spec.ts b/aio/src/app/layout/cookies-popup/cookies-popup.component.spec.ts new file mode 100644 index 0000000000..dcae1b1c23 --- /dev/null +++ b/aio/src/app/layout/cookies-popup/cookies-popup.component.spec.ts @@ -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; + + 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('a[mat-button]:nth-child(1)'); + const okBtn = popup.querySelector('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 { + private items = new Map(); + + getItem(key: string): string | null { + return this.items.get(key) ?? null; + } + + setItem(key: string, val: string): void { + this.items.set(key, val); + } + } +}); diff --git a/aio/src/app/layout/cookies-popup/cookies-popup.component.ts b/aio/src/app/layout/cookies-popup/cookies-popup.component.ts new file mode 100644 index 0000000000..c398c8d9a2 --- /dev/null +++ b/aio/src/app/layout/cookies-popup/cookies-popup.component.ts @@ -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: ` +
+

Cookies concent notice

+ + This site uses cookies from Google to deliver its services and to analyze traffic. + +
+ + Learn more + + +
+
+ `, +}) +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; + } +} diff --git a/aio/src/styles/2-modules/_index.scss b/aio/src/styles/2-modules/_index.scss index 66f600a7af..913e3a9c58 100644 --- a/aio/src/styles/2-modules/_index.scss +++ b/aio/src/styles/2-modules/_index.scss @@ -12,6 +12,7 @@ @forward 'cli-pages/cli-pages'; @forward 'code/code'; @forward 'contribute/contribute'; +@forward 'cookies-popup/cookies-popup'; @forward 'contributor/contributor'; @forward 'deploy-theme/deploy-theme'; @forward 'details/details'; diff --git a/aio/src/styles/2-modules/_theme.scss b/aio/src/styles/2-modules/_theme.scss index fa0e651be1..62919bd78e 100644 --- a/aio/src/styles/2-modules/_theme.scss +++ b/aio/src/styles/2-modules/_theme.scss @@ -7,6 +7,7 @@ @use 'card/card-theme'; @use 'code/code-theme'; @use 'contributor/contributor-theme'; +@use 'cookies-popup/cookies-popup-theme'; @use 'deploy-theme/deploy-theme'; @use 'details/details-theme'; @use 'errors/errors-theme'; @@ -33,6 +34,7 @@ @include card-theme.theme($theme); @include code-theme.theme($theme); @include contributor-theme.theme($theme); + @include cookies-popup-theme.theme($theme); @include deploy-theme.theme($theme); @include details-theme.theme($theme); @include errors-theme.theme($theme); diff --git a/aio/src/styles/2-modules/cookies-popup/_cookies-popup-theme.scss b/aio/src/styles/2-modules/cookies-popup/_cookies-popup-theme.scss new file mode 100644 index 0000000000..a5bb9ec4a1 --- /dev/null +++ b/aio/src/styles/2-modules/cookies-popup/_cookies-popup-theme.scss @@ -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); + } + } + } + } + } +} diff --git a/aio/src/styles/2-modules/cookies-popup/_cookies-popup.scss b/aio/src/styles/2-modules/cookies-popup/_cookies-popup.scss new file mode 100644 index 0000000000..a7ee2675f7 --- /dev/null +++ b/aio/src/styles/2-modules/cookies-popup/_cookies-popup.scss @@ -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; + } + } + } +} diff --git a/aio/tests/e2e/src/app.po.ts b/aio/tests/e2e/src/app.po.ts index f10ab9fe6b..b91a0ff576 100644 --- a/aio/tests/e2e/src/app.po.ts +++ b/aio/tests/e2e/src/app.po.ts @@ -9,6 +9,7 @@ export class SitePage { docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs')); sidenav = element(by.css('mat-sidenav')); docViewer = element(by.css('aio-doc-viewer')); + cookiesPopup = element(by.css('.cookies-popup')); codeExample = element.all(by.css('aio-doc-viewer pre > code')); ghLinks = this.docViewer .all(by.css('a')) @@ -39,10 +40,15 @@ export class SitePage { ga() { return browser.executeScript('return window["ga"].q'); } locationPath() { return browser.executeScript('return document.location.pathname'); } - async navigateTo(pageUrl: string) { - // Navigate to the page, disable animations, and wait for Angular. + async navigateTo(pageUrl: string, keepCookiesPopup = false) { + // Navigate to the page, disable animations, potentially hide the cookies popup, and wait for + // Angular. await browser.get(`/${pageUrl.replace(/^\//, '')}`); 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(); } diff --git a/aio/tests/e2e/src/cookies-popup.e2e-spec.ts b/aio/tests/e2e/src/cookies-popup.e2e-spec.ts new file mode 100644 index 0000000000..1ae42752bf --- /dev/null +++ b/aio/tests/e2e/src/cookies-popup.e2e-spec.ts @@ -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); + }); +}); diff --git a/goldens/size-tracking/aio-payloads.json b/goldens/size-tracking/aio-payloads.json index fa92ee0f9c..4dd3b70d74 100755 --- a/goldens/size-tracking/aio-payloads.json +++ b/goldens/size-tracking/aio-payloads.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "runtime-es2017": 4619, - "main-es2017": 454003, + "main-es2017": 456795, "polyfills-es2017": 55210 } } @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2017": 4619, - "main-es2017": 454138, + "main-es2017": 456930, "polyfills-es2017": 55348 } }