Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WSTEAMA-750: Service worker running in local development environment #11094

Merged
merged 35 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
33f8005
Allows service worker to run in local development environment
karinathomasbbc Sep 20, 2023
4b3ee6f
Merge branch 'latest' of github.com:bbc/simorgh into WSTEAMA-750-serv…
karinathomasbbc Sep 20, 2023
40340be
Reformat sw.js file
karinathomasbbc Sep 20, 2023
58909d6
Allow service worker on local environment
karinathomasbbc Sep 20, 2023
60db2d4
Do not enable service worker on AMP for local environment as it cause…
karinathomasbbc Sep 21, 2023
38a73bb
Adding unit tests for service worker
karinathomasbbc Oct 2, 2023
2d7cfe6
Merge branch 'latest' into WSTEAMA-750-service-worker-local
eagerterrier Oct 25, 2023
3c610f9
Merge branch 'latest' of github.com:bbc/simorgh into WSTEAMA-750-serv…
karinathomasbbc Dec 5, 2023
28e6977
Allow service worker on all environments
karinathomasbbc Dec 5, 2023
4e316b1
Migrate service worker to typescript & apply component standards
karinathomasbbc Dec 5, 2023
8f3b0e5
Updating cypress application tests to check that service worker files…
karinathomasbbc Dec 5, 2023
3eca8a5
Service Worker stuff
karinathomasbbc Dec 6, 2023
98ed550
Service Worker stuff
karinathomasbbc Dec 6, 2023
aabc507
Delete!
karinathomasbbc Dec 6, 2023
1fde913
Delete!
karinathomasbbc Dec 6, 2023
15a0587
Revert!
karinathomasbbc Dec 6, 2023
f932a07
Revert
karinathomasbbc Dec 6, 2023
340882d
Updating sw.js and adding a test
karinathomasbbc Dec 6, 2023
4c8cd53
Remove white space
karinathomasbbc Dec 6, 2023
7b121a9
Reinstate not supporting amp service worker on amp as it requires https
karinathomasbbc Dec 6, 2023
83971d1
Update version number
karinathomasbbc Dec 6, 2023
bb90928
Make service worker props optional & set default values
karinathomasbbc Dec 6, 2023
dd244d3
Clatty hack to read in the sw.js file, add an export statement, and t…
karinathomasbbc Dec 6, 2023
e425d74
Fix formatting
karinathomasbbc Dec 6, 2023
c0e1c8e
Merge branch 'latest' of github.com:bbc/simorgh into WSTEAMA-750-serv…
karinathomasbbc Jan 22, 2024
d8906c9
Fix caching for frosted_promo and moment-lib
karinathomasbbc Jan 22, 2024
a35f56b
Bump service worker version number as the contents have changed
karinathomasbbc Jan 22, 2024
cc27f6f
Reinstate original woff2 regex
karinathomasbbc Jan 22, 2024
ce86a23
Minor version of service worker
karinathomasbbc Jan 22, 2024
f11618a
Delete the service worker cloned file after running the tests
karinathomasbbc Jan 22, 2024
361705b
Don't delete the file as it causes code climate checks to fail
karinathomasbbc Feb 7, 2024
f8d33ea
Merge branch 'latest' of github.com:bbc/simorgh into WSTEAMA-750-serv…
karinathomasbbc Feb 7, 2024
81f3ca4
Merge branch 'latest' into WSTEAMA-750-service-worker-local
andrewscfc Feb 7, 2024
c8cecfe
Merge branch 'latest' into WSTEAMA-750-service-worker-local
karinathomasbbc Feb 12, 2024
aac7255
Merge branch 'latest' into WSTEAMA-750-service-worker-local
karinathomasbbc Feb 12, 2024
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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module.exports = {
'amp-custom',
'amp-access',
'amp-access-hide',
'amp-install-serviceworker',
'css',
'custom-element',
'custom-template',
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ yarn-error.log
secret.env
src/app/legacy/moment-timezone-include/tz
tsconfig.tsbuildinfo
src/service-worker-test.js
28 changes: 19 additions & 9 deletions cypress/e2e/application/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import envConfig from '../../support/config/envs';
import getPaths from '../../support/helpers/getPaths';
import { getTopicPagePath } from '../pages/topicPage/helpers';

const servicesUsingArticlePaths = ['news', 'scotland'];

describe('Application', () => {
Object.keys(config)
.filter(service =>
Expand All @@ -16,27 +14,39 @@ describe('Application', () => {
),
)
.forEach(service => {
const usesArticlePath = servicesUsingArticlePaths.includes(service);
if (!ampOnlyServices.includes(service)) {
it(`should return a 200 status code for ${service}'s service worker`, () => {
cy.testResponseCodeAndType({
path: usesArticlePath
? `/${config[service].name}/articles/sw.js`
: `/${config[service].name}/sw.js`,
path: `/${config[service].name}/sw.js`,
responseCode: 200,
type: 'application/javascript',
});
});

it(`should return a 200 status code for ${service}'s article service worker`, () => {
cy.testResponseCodeAndType({
path: `/${config[service].name}/articles/sw.js`,
responseCode: 200,
type: 'application/javascript',
});
});

it(`should return a 200 status code for ${service} manifest file`, () => {
cy.testResponseCodeAndType({
path: usesArticlePath
? `/${config[service].name}/articles/manifest.json`
: `/${config[service].name}/manifest.json`,
path: `/${config[service].name}/manifest.json`,
responseCode: 200,
type: 'application/json',
});
});

it(`should return a 200 status code for ${service} article manifest file`, () => {
cy.testResponseCodeAndType({
path: `/${config[service].name}/articles/manifest.json`,
responseCode: 200,
type: 'application/json',
});
});

it(`should awaken fresh data for pages for later tests`, () => {
// Add more here if you want to awaken fresh data for other page types
if (serviceHasPageType(service, 'topicPage')) {
Expand Down
59 changes: 35 additions & 24 deletions public/sw.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,53 @@
/* eslint-disable */
const version = 'v0.0.6';
/* eslint-disable no-useless-return */
/* eslint-disable import/prefer-default-export */
/* eslint-disable no-unused-vars */
/* eslint-disable no-undef */
/* eslint-disable no-restricted-globals */
const version = 'v0.1.0';
const cacheName = 'simorghCache_v1';

self.addEventListener('install', (event) => {
self.addEventListener('install', event => {
event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', function(event) {
if (/^https:\/\/ichef\.bbci\.co\.uk\/(news|ace\/standard)\/.+(\.jpg|\.png)$/.test(event.request.url)) {

const fetchEventHandler = async event => {
if (
/^https:\/\/ichef\.bbci\.co\.uk\/(news|ace\/standard)\/.+(\.jpg|\.png)$/.test(
event.request.url,
)
) {
const req = event.request.clone();

// Inspect the accept header for WebP support
let supportsWebp = false;
if (req.headers.has('accept')){
if (req.headers.has('accept')) {
supportsWebp = req.headers.get('accept').includes('webp');
}

// If we support WebP
if (supportsWebp && !/\/amz\/worldservice\/.*/.test(event.request.url)) {
event.respondWith(
fetch(`${req.url}.webp`, {
mode: 'no-cors'
})
mode: 'no-cors',
}),
);
}
}
else if (/((\/cwr\.js$)|(\.woff2$)|(modern\.frosted_promo\.32caa641\.js$)|(\/moment\-lib\.dfdb34b8\.js$))/.test(event.request.url)) {
event.respondWith(caches.open(cacheName).then(cache => {
return cache.match(event.request).then(cachedResponse => {
return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
});
});
}));
}
else {
return;
} else if (
/((\/cwr\.js$)|(\.woff2$)|(modern\.frosted_promo+.*?\.js$)|(\/moment-lib+.*?\.js$))/.test(
event.request.url,
)
) {
const cache = await caches.open(cacheName);
let response = await cache.match(event.request);

if (!response) {
response = await fetch(event.request.url);
cache.put(event.request, response.clone());
}

event.respondWith(response);
}
});
return;
};

onfetch = fetchEventHandler;
4 changes: 2 additions & 2 deletions src/app/components/PageLayoutWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import WebVitals from '../../legacy/containers/WebVitals';
import HeaderContainer from '../../legacy/containers/Header';
import FooterContainer from '../../legacy/containers/Footer';
import ManifestContainer from '../../legacy/containers/Manifest';
import ServiceWorkerContainer from '../../legacy/containers/ServiceWorker';
import ServiceWorker from '../ServiceWorker';
import { ServiceContext } from '../../contexts/ServiceContext';
import { RequestContext } from '../../contexts/RequestContext';
import ThemeProvider from '../ThemeProvider';
Expand Down Expand Up @@ -201,7 +201,7 @@ const PageLayoutWrapper = ({
]}
/>
<ThemeProvider service={service} variant={variant}>
{!isNextJs && <ServiceWorkerContainer />}
{!isNextJs && <ServiceWorker />}
<ManifestContainer />
{!isErrorPage && <WebVitals pageType={pageType} />}
<GlobalStyles />
Expand Down
125 changes: 125 additions & 0 deletions src/app/components/ServiceWorker/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React from 'react';
import onClient from '#app/lib/utilities/onClient';
import isLocal from '#app/lib/utilities/isLocal';
import { render } from '../react-testing-library-with-providers';
import { ServiceContext } from '../../contexts/ServiceContext';
import ServiceWorkerContainer from './index';

const contextStub = {
swPath: '/articles/sw.js',
service: 'news',
Fixed Show fixed Hide fixed
};

const mockServiceWorker = {
register: jest.fn(),
};

jest.mock('#app/lib/utilities/onClient', () =>
jest.fn().mockImplementation(() => true),
);

jest.mock('#app/lib/utilities/isLocal', () =>
jest.fn().mockImplementation(() => true),
);

describe('Service Worker', () => {
const originalNavigator = global.navigator;

afterEach(() => {
jest.resetAllMocks();

global.navigator = originalNavigator;
});

describe('Canonical', () => {
it('is registered when swPath, serviceWorker have values and onClient is true', () => {
// @ts-expect-error need to override the navigator.serviceWorker for testing purposes
global.navigator.serviceWorker = mockServiceWorker;
(onClient as jest.Mock).mockImplementationOnce(() => true);

render(
// @ts-expect-error only require a subset of properties on service context for testing purposes
<ServiceContext.Provider value={{ ...contextStub }}>
<ServiceWorkerContainer />
</ServiceContext.Provider>,
);
expect(navigator.serviceWorker.register).toHaveBeenCalledWith(
`/news/articles/sw.js`,
);
});

describe('is not registered', () => {
it.each`
swPath | serviceWorker | isOnClient
${undefined} | ${undefined} | ${true}
${undefined} | ${undefined} | ${false}
${undefined} | ${mockServiceWorker} | ${true}
${undefined} | ${mockServiceWorker} | ${false}
${contextStub.swPath} | ${mockServiceWorker} | ${false}
`(
'when swPath is $swPath, serviceWorker is $serviceWorker and isOnClient is $isOnClient',
({ swPath, serviceWorker, isOnClient }) => {
if (serviceWorker) {
// @ts-expect-error need to override the navigator.serviceWorker for testing purposes
global.navigator.serviceWorker = serviceWorker;
}

(onClient as jest.Mock).mockImplementationOnce(() => isOnClient);

render(
// @ts-expect-error only require a subset of properties on service context for testing purposes
<ServiceContext.Provider value={{ ...contextStub, swPath }}>
<ServiceWorkerContainer />
</ServiceContext.Provider>,
);
expect(navigator.serviceWorker.register).not.toHaveBeenCalled();
},
);
});
});

describe('Amp', () => {
it('is enabled when swPath has a value and not on local environment', () => {
(isLocal as jest.Mock).mockImplementationOnce(() => false);

const { container } = render(
// @ts-expect-error only require a subset of properties on service context for testing purposes
<ServiceContext.Provider value={{ ...contextStub }}>
<ServiceWorkerContainer />
</ServiceContext.Provider>,
{ isAmp: true },
);

expect(
container.querySelector('amp-install-serviceworker'),
).toBeInTheDocument();
});

describe('is disabled', () => {
it.each`
swPath | isLocalEnv | reason
${undefined} | ${false} | ${'swPath is undefined'}
${undefined} | ${false} | ${'swPath is null'}
${''} | ${false} | ${'swPath is empty'}
${'swPath'} | ${true} | ${'service worker not supported on local environment on amp as it requires https'}
`(
'when swPath is $swPath and isLocalEnv is $isLocalEnv because $reason',
({ swPath, isLocalEnv }) => {
(isLocal as jest.Mock).mockImplementationOnce(() => isLocalEnv);

const { container } = render(
// @ts-expect-error only require a subset of properties on service context for testing purposes
<ServiceContext.Provider value={{ ...contextStub, swPath }}>
<ServiceWorkerContainer />
</ServiceContext.Provider>,
{ isAmp: true },
);

expect(
container.querySelector('amp-install-serviceworker'),
).not.toBeInTheDocument();
},
);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
/** @jsxRuntime classic */
/** @jsx jsx */
/* @jsxFrag React.Fragment */
import React, { useContext, useEffect } from 'react';
import { string } from 'prop-types';
import { Helmet } from 'react-helmet';
import onClient from '#lib/utilities/onClient';
import { RequestContext } from '#contexts/RequestContext';
import { ServiceContext } from '../../../contexts/ServiceContext';
import { jsx } from '@emotion/react';
import isLocal from '#app/lib/utilities/isLocal';
import { ServiceContext } from '../../contexts/ServiceContext';

interface AmpServiceWorkerProps {
canonicalLink?: string;
swSrc?: string;
}

const AmpHead = () => (
<Helmet>
Expand All @@ -15,48 +24,35 @@ const AmpHead = () => (
</Helmet>
);

const AmpServiceWorker = ({ canonicalLink, swSrc }) => (
const AmpServiceWorker = ({
canonicalLink = '',
swSrc = '',
}: AmpServiceWorkerProps) => (
<amp-install-serviceworker
src={swSrc}
data-iframe-src={canonicalLink}
layout="nodisplay"
/>
);

const ServiceWorkerContainer = () => {
export default () => {
const { swPath, service } = useContext(ServiceContext);
const { isAmp, canonicalLink } = useContext(RequestContext);
const envIsProduction = process.env.NODE_ENV === 'production';
const swSrc = `${process.env.SIMORGH_BASE_URL}/${service}${swPath}`;

useEffect(() => {
const shouldInstallServiceWorker =
process.env.SIMORGH_APP_ENV !== 'local' &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the original reason for not having this on local?

swPath &&
onClient() &&
'serviceWorker' in navigator;
swPath && onClient() && 'serviceWorker' in navigator;

if (shouldInstallServiceWorker) {
navigator.serviceWorker.register(`/${service}${swPath}`);
}
}, [envIsProduction, swPath, service]);
}, [swPath, service]);

return isAmp && swPath && process.env.SIMORGH_APP_ENV !== 'local' ? (
return !isLocal() && isAmp && swPath ? (
<>
<AmpHead />
<AmpServiceWorker canonicalLink={canonicalLink} swSrc={swSrc} />
</>
) : null;
};

AmpServiceWorker.propTypes = {
canonicalLink: string,
swSrc: string,
};

AmpServiceWorker.defaultProps = {
canonicalLink: '',
swSrc: '',
};

export default ServiceWorkerContainer;
16 changes: 16 additions & 0 deletions src/app/components/ServiceWorker/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
declare namespace JSX {
/*
* AMP currently doesn't have built-in types for TypeScript, but it's in their roadmap (https://github.com/ampproject/amphtml/issues/13791).
* As a workaround you can manually create custom types (https://stackoverflow.com/a/50601125).
*/

interface AmpInstallServiceWorker {
src: string;
'data-iframe-src': string;
layout: string;
}

interface IntrinsicElements {
'amp-install-serviceworker': AmpInstallServiceWorker;
}
}
Loading
Loading