homeresume
 
   

Progressive Web Apps 101

March 5, 2022

Progressive Web Apps bring some advantages over native mobile apps

  • automatic updates can be implemented
  • the installed app takes less memory
  • installable on phones, tablets, desktops

Prerequisites for installation

  • web app is running over an HTTPS connection
  • service worker is registered
  • web app manifest (manifest.json) is included

Service worker

Read more about it on Caching with service worker and Workbox

Manifest

Following fields can be included

  • name is a full name used when the app is installed
  • short_name is a shorter version of the name that is shown when there is insufficient space to display the full name
  • background_color is used on a splash screen
  • description is shown on an installation pop-up
  • display customizes which browser UI is shown when the app is launched (standalone, fullscreen, minimal-ui, browser)
  • icons is a list of icons for the browser used in different places (home screen, app launcher, etc.)
  • scope specifies the navigation scope of the PWA. It should start with the URL from start_url value. If the user navigates outside the scope, PWA won't be open.
  • screenshots is a list of screenshots shown on the installation pop-up
  • start_url is a relative URL of the app which is loaded when the installed app is launched. PWA usage can be tracked by adding UTM parameters within the URL.
  • theme_color sets the color of the toolbar, it should match the meta theme color specified in the document head

Description and screenshots are shown only on mobile phones.

{
"name": "App name",
"short_name": "App short name",
"background_color": "#ffffff",
"description": "App description",
"display": "standalone",
"icons": [
{
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"scope": "/app",
"screenshots": [{
"src": "screenshots/main.jpg",
"sizes": "1080x2400",
"type": "image/jpg"
}],
"start_url": "/app?utm_source=pwa&utm_medium=pwa&utm_campaign=pwa",
"theme_color": "#3366cc"
}

Manifest file should be included via link tag

<link rel="manifest" href="/manifest.json">

In-app installation experience

It can be implemented on Google Chrome and Edge.

  • listen for the beforeinstallprompt event
  • save beforeinstallprompt event so it can be used to trigger the installation
  • provide a button to start the in-app installation flow
let deferredPrompt;
let installable = false;
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
deferredPrompt = event;
installable = true;
document.getElementById("installable-btn").innerHTML = "Install";
});
window.addEventListener("appinstalled", () => {
installable = false;
});
document.getElementById("installable-btn").addEventListener("click", () => {
if (installable) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === "accepted") {
document.getElementById("installable-btn").innerHTML = "click!";
}
});
} else {
alert("clicked!");
}
});

Notes

chrome://webapks page on mobile phones shows the list of installed PWAs with their details. Last Update Check Time is useful for checking when the manifest file was updated. The app is updated once a day if there are some manifest changes.

Example

A working example is available at https://github.com/zsevic/pwa-starter

2021

Spies and mocking with Jest

August 19, 2021

Unit testing, in addition to output testing, includes the usage of spies and mocking. Spies are functions that let you spy on the behavior of functions that are called indirectly by some other code. Spy can be created by using jest.fn(). Mocking is injecting test values into the code during the tests. Some of the use cases will be presented below.

  • Async function and its resolved value can be mocked using mockResolvedValue. Another way to mock it is by using mockImplementation and providing a function as an argument.
const calculationService = {
calculate: jest.fn(),
};
jest.spyOn(calculationService, "calculate").mockResolvedValue(value);
jest.spyOn(calculationService, "calculate").mockImplementation(async (a) => Promise.resolve(a));
  • Rejected async function can be mocked using mockRejectedValue and mockImplementation.
jest.spyOn(calculationService, "calculate").mockRejectedValue(value);
jest.spyOn(calculationService, "calculate")
.mockImplementation(async () => Promise.reject(new Error(errorMessage)));
await expect(calculateSomething(calculationService)).rejects.toThrowError(Error);
  • Sync function and its return value can be mocked using mockReturnValue and mockImplementation.
jest.spyOn(calculationService, "calculate").mockReturnValue(value);
jest.spyOn(calculationService, "calculate").mockImplementation((a) => a);
  • Chained methods can be mocked using mockReturnThis.
// calculationService.get().calculate();
jest.spyOn(calculationService, "get").mockReturnThis();
  • Async and sync functions which are called multiple times can be mocked with different values using mockResolvedValueOnce and mockReturnValueOnce respectively and mockImplementationOnce.
jest.spyOn(calculationService, "calculate").mockResolvedValueOnce(value)
.mockResolvedValueOnce(otherValue);
jest.spyOn(calculationService, "calculate").mockReturnValueOnce(value)
.mockReturnValueOnce(otherValue);
jest.spyOn(calculationService, "calculate").mockImplementationOnce((a) => a + 3)
.mockImplementationOnce((a) => a + 5);
  • External modules can be mocked similarly as spies. For the following example let's suppose axios package is already used in one function. The following example represents a test file where axios is mocked using jest.mock().
import axios from 'axios';
jest.mock('axios');
// within test case
axios.get.mockResolvedValue(data);
  • Manual mocks are resolved by writing corresponding modules in __mocks__ directory, e.g. fs/promises mock will be stored in __mocks__/fs/promises.js file. fs/promises mock will be resolved using jest.mock() in the test file.
jest.mock('fs/promises');
  • To assert called arguments for a mocked function, an assertion can be done using toHaveBeenCalledWith matcher.
expect(calculationService.calculate).toHaveBeenCalledWith(firstArgument, secondArgument);
  • To assert skipped call for a mocked function, an assertion can be done using not.toHaveBeenCalled matcher.
expect(calculationService.calculate).not.toHaveBeenCalled();
  • To assert called arguments for the exact call when a mocked function is called multiple times, an assertion can be done using toHaveBeenNthCalledWith matcher.
argumentsList.forEach((argumentList, index) => {
expect(calculationService.calculate).toHaveBeenNthCalledWith(
index + 1,
argumentList,
);
});
  • Mocks should be reset to their initial implementation before each test case.
beforeEach(() => {
jest.resetAllMocks();
});

Server-Sent Events 101

August 18, 2021

Server-Sent Events (SSE) is a unidirectional communication between client and server where the server sends the events in text/event-stream format to the client once the client-server connection is established by the client. The client initiates the connection with the server using EventSource API. Previously mentioned API can also be used for listening to the events from the server, listening for the errors, and closing the connection.

const eventSource = new EventSource(url);
eventSource.onmessage = ({ data }) => {
const eventData = JSON.parse(data);
// handling the data from the server
};
eventSource.onerror = () => {
// error handling
};
eventSource.close();

A server can filter clients by query parameter and send them only the appropriate events. In the following example server sends the events only to a specific client distinguished by its e-mail address.

@Sse('sse')
sse(@Query() sseQuery: SseQueryDto): Observable<MessageEvent> {
const subject$ = new Subject();
this.eventService.on(FILTER_VERIFIED, data => {
if (sseQuery.email !== data.email) return;
subject$.next({ isVerifiedFilter: true });
});
return subject$.pipe(
map((data: MessageEventData): MessageEvent => ({ data })),
);
}

 

© 2022