From 9e57c0abaa0ea49c61c9f592516616b286722def Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Mon, 3 May 2021 16:52:52 +0200 Subject: [PATCH] fix(elements): add check if localStorage is available (#1073) --- .../src/components/app/app.component.ts | 5 +-- packages/elements/src/lib/browser.ts | 13 +++++++ .../mutation-test-report-app.spec.ts | 25 +++++++++++++ .../elements/test/unit/lib/browser.spec.ts | 35 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 packages/elements/src/lib/browser.ts create mode 100644 packages/elements/test/unit/lib/browser.spec.ts diff --git a/packages/elements/src/components/app/app.component.ts b/packages/elements/src/components/app/app.component.ts index e1350147b..63dec1f97 100644 --- a/packages/elements/src/components/app/app.component.ts +++ b/packages/elements/src/components/app/app.component.ts @@ -9,6 +9,7 @@ import theme from './theme.scss'; import { createCustomEvent } from '../../lib/custom-events'; import { FileUnderTestModel, Metrics, MutationTestMetricsResult, TestFileModel, TestMetrics } from 'mutation-testing-metrics/src/model'; import { toAbsoluteUrl } from '../../lib/htmlHelpers'; +import { isLocalStorageAvailable } from '../../lib/browser'; interface BaseContext { path: string[]; @@ -74,7 +75,7 @@ export class MutationTestReportAppComponent extends LitElement { // Set the theme when no theme is selected (light vs dark) if (!this.theme) { // 1. check local storage - const theme = localStorage.getItem('mutation-testing-elements-theme'); + const theme = isLocalStorageAvailable() && localStorage.getItem('mutation-testing-elements-theme'); if (theme) { this.theme = theme; // 2. check for user's OS preference @@ -154,7 +155,7 @@ export class MutationTestReportAppComponent extends LitElement { public themeSwitch = (event: CustomEvent) => { this.theme = event.detail; - localStorage.setItem('mutation-testing-elements-theme', this.theme); + isLocalStorageAvailable() && localStorage.setItem('mutation-testing-elements-theme', this.theme); }; public static styles = [globals, unsafeCSS(theme), bootstrap, unsafeCSS(style)]; diff --git a/packages/elements/src/lib/browser.ts b/packages/elements/src/lib/browser.ts new file mode 100644 index 000000000..58e301110 --- /dev/null +++ b/packages/elements/src/lib/browser.ts @@ -0,0 +1,13 @@ +/** + * Test if localStorage exists and is enabled + */ +export function isLocalStorageAvailable() { + const test = 'test'; + try { + localStorage.setItem(test, test); + localStorage.removeItem(test); + return true; + } catch (e) { + return false; + } +} diff --git a/packages/elements/test/unit/components/mutation-test-report-app.spec.ts b/packages/elements/test/unit/components/mutation-test-report-app.spec.ts index 42a076fc4..59fe4498c 100644 --- a/packages/elements/test/unit/components/mutation-test-report-app.spec.ts +++ b/packages/elements/test/unit/components/mutation-test-report-app.spec.ts @@ -179,6 +179,21 @@ describe(MutationTestReportAppComponent.name, () => { expect(localStorage.getItem('mutation-testing-elements-theme'), 'dark'); }); + it('should not set theme to local storage if localStorage is not available', async () => { + // Arrange + sut.element.report = createReport(); + const setItemStub = sinon.stub(localStorage, 'setItem').throws(new Error()); + await sut.whenStable(); + + // Act + sut.$('mte-theme-switch').dispatchEvent(createCustomEvent('theme-switch', 'dark')); + await sut.whenStable(); + + // Assert + expect(sut.element.theme).eq('dark'); + expect(setItemStub.notCalled).false; + }); + describe('themeBackgroundColor', () => { it('should show light theme-color on light theme', async () => { // Arrange @@ -198,6 +213,16 @@ describe(MutationTestReportAppComponent.name, () => { }); }); + it('should use fallbacks if localStorage is not available', async () => { + sinon.stub(localStorage, 'setItem').throws(new Error()); + matchMediaStub.withArgs('(prefers-color-scheme: dark)').returns({ matches: true } as MediaQueryList); + sut.element.report = createReport(); + await sut.whenStable(); + + // Assert + expect(sut.element.theme).eq('dark'); + }); + it('should choose attribute value over local storage', async () => { // Arrange localStorage.setItem('mutation-testing-elements-theme', 'dark'); diff --git a/packages/elements/test/unit/lib/browser.spec.ts b/packages/elements/test/unit/lib/browser.spec.ts new file mode 100644 index 000000000..1432fcab8 --- /dev/null +++ b/packages/elements/test/unit/lib/browser.spec.ts @@ -0,0 +1,35 @@ +import { isLocalStorageAvailable } from '../../../src/lib/browser'; +import sinon from 'sinon'; +import { expect } from 'chai'; + +describe(isLocalStorageAvailable.name, () => { + let setItemStub: sinon.SinonStub, ReturnType>; + let removeItemStub: sinon.SinonStub, ReturnType>; + + beforeEach(() => { + setItemStub = sinon.stub(localStorage, 'setItem'); + removeItemStub = sinon.stub(localStorage, 'removeItem'); + }); + + it(`should be false if setItem throws`, () => { + setItemStub.throws(new Error('Quota exceeded')); + + expect(isLocalStorageAvailable()).to.be.false; + }); + + it(`should be false if removeItem throws`, () => { + removeItemStub.throws(new Error('Quota exceeded')); + + expect(isLocalStorageAvailable()).to.be.false; + }); + + it(`should be false if localStorage is undefined`, () => { + sinon.replaceGetter(window, 'localStorage', () => (undefined as unknown) as Storage); + + expect(isLocalStorageAvailable()).to.be.false; + }); + + it('should be true if localStorage works', () => { + expect(isLocalStorageAvailable()).to.be.true; + }); +});