(service-worker) add unit tests

Add unit tests for:
- ApiPlugin
- RequestSerializer
- SyncManager
This commit is contained in:
Anthony LC
2024-06-26 12:46:41 +02:00
committed by Anthony LC
parent d588ae847f
commit cfb979a411
3 changed files with 545 additions and 0 deletions

View File

@@ -0,0 +1,401 @@
/**
* @jest-environment node
*/
import '@testing-library/jest-dom';
import { ApiPlugin } from '../ApiPlugin';
import { RequestSerializer } from '../RequestSerializer';
const mockedGet = jest.fn().mockResolvedValue({});
const mockedGetAllKeys = jest.fn().mockResolvedValue([]);
const mockedPut = jest.fn().mockResolvedValue({});
const mockedDelete = jest.fn().mockResolvedValue({});
const mockedClose = jest.fn().mockResolvedValue({});
const mockedOpendDB = jest.fn().mockResolvedValue({
get: mockedGet,
getAllKeys: mockedGetAllKeys,
getAll: jest.fn().mockResolvedValue([]),
put: mockedPut,
delete: mockedDelete,
clear: jest.fn().mockResolvedValue({}),
close: mockedClose,
});
jest.mock('idb', () => ({
...jest.requireActual('idb'),
openDB: () => mockedOpendDB(),
}));
describe('ApiPlugin', () => {
afterEach(() => jest.clearAllMocks());
['doc-item', 'doc-list'].forEach((type) => {
it(`calls fetchDidSucceed with type ${type} and status 200`, async () => {
const apiPlugin = new ApiPlugin({
tableName: type as any,
type: 'list',
syncManager: jest.fn() as any,
});
const body = { lastName: 'Doe' };
const bodyBuffer = RequestSerializer.objectToArrayBuffer(body);
const response = await apiPlugin.fetchDidSucceed?.({
request: {
url: 'test-url',
body,
} as unknown as Request,
response: new Response(bodyBuffer, {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'application/json',
},
}),
} as any);
expect(mockedPut).toHaveBeenCalledWith(type, body, 'test-url');
expect(mockedClose).toHaveBeenCalled();
expect(response?.status).toBe(200);
});
it(`calls fetchDidSucceed with type ${type} and status other that 200`, async () => {
const apiPlugin = new ApiPlugin({
tableName: type as any,
type: 'list',
syncManager: jest.fn() as any,
});
const body = { lastName: 'Doe' };
const bodyBuffer = RequestSerializer.objectToArrayBuffer(body);
const response = await apiPlugin.fetchDidSucceed?.({
request: {
url: 'test-url',
body,
} as unknown as Request,
response: new Response(bodyBuffer, {
status: 400,
statusText: 'OK',
headers: {
'Content-Type': 'application/json',
},
}),
} as any);
expect(mockedPut).not.toHaveBeenCalled();
expect(response?.status).toBe(400);
});
});
[
{ type: 'update', withClone: true },
{ type: 'delete', withClone: true },
{ type: 'create', withClone: true },
{ type: 'list', withClone: false },
{ type: 'item', withClone: false },
].forEach(({ type, withClone }) => {
it(`calls requestWillFetch with type ${type}`, async () => {
const mockedSync = jest.fn().mockResolvedValue({});
const apiPlugin = new ApiPlugin({
type: 'update',
syncManager: {
sync: () => mockedSync(),
} as any,
});
const mockedClone = jest.fn().mockResolvedValue({});
const requestInit = {
request: {
url: 'test-url',
clone: () => mockedClone(),
} as unknown as Request,
} as any;
const request = await apiPlugin.requestWillFetch?.(requestInit);
if (withClone) {
// eslint-disable-next-line jest/no-conditional-expect
expect(mockedClone).toHaveBeenCalled();
}
expect(mockedSync).toHaveBeenCalled();
expect(request?.url).toBe('test-url');
});
});
it(`checks getApiCatchHandler`, async () => {
const response = ApiPlugin.getApiCatchHandler();
expect(await response.json()).toEqual({ error: 'Network is unavailable.' });
});
[
{ type: 'list', tableName: 'doc-list' },
{ type: 'item', tableName: 'doc-item' },
].forEach(({ type, tableName }) => {
it(`checks handlerDidError with type ${type}`, async () => {
const requestInit = {
request: {
url: 'test-url',
} as unknown as Request,
} as any;
const apiPlugin = new ApiPlugin({
type: type as 'list' | 'item' | 'update' | 'create' | 'delete',
tableName: tableName as 'doc-list' | 'doc-item',
syncManager: {} as any,
});
await apiPlugin.fetchDidFail?.({} as any);
const response = await apiPlugin.handlerDidError?.(requestInit);
expect(mockedGet).toHaveBeenCalledWith(tableName, 'test-url');
expect(response?.status).toBe(200);
});
});
it(`checks handlerDidError with type update`, async () => {
const requestInit = {
request: {
url: 'http://test.jest/documents/123456/',
clone: () => mockedClone(),
headers: new Headers({
'Content-Type': 'application/json',
}),
arrayBuffer: () =>
RequestSerializer.objectToArrayBuffer({
test: 'test',
}),
json: () => ({
test: 'test',
}),
} as unknown as Request,
} as any;
const mockedClone = jest.fn().mockReturnValue(requestInit.request);
const mockedSync = jest.fn().mockResolvedValue({});
const apiPlugin = new ApiPlugin({
type: 'update',
syncManager: {
sync: () => mockedSync(),
} as any,
});
mockedGetAllKeys.mockResolvedValue(['http://test.jest/documents/?page=1']);
mockedGet.mockResolvedValue({
results: [
{
id: '123456',
title: 'test',
},
],
});
await apiPlugin.requestWillFetch?.(requestInit);
await apiPlugin.fetchDidFail?.({} as any);
const response = await apiPlugin.handlerDidError?.(requestInit);
expect(mockedGet).toHaveBeenCalledWith(
'doc-item',
'http://test.jest/documents/123456/',
);
expect(mockedGetAllKeys).toHaveBeenCalledWith('doc-list');
expect(mockedPut).toHaveBeenCalledWith(
'doc-mutation',
expect.objectContaining({
key: expect.any(String),
requestData: expect.objectContaining({
url: 'http://test.jest/documents/123456/',
headers: {
'content-type': 'application/json',
},
}),
}),
expect.any(String),
);
expect(mockedPut).toHaveBeenCalledWith(
'doc-item',
{ results: [{ id: '123456', title: 'test' }], test: 'test' },
'http://test.jest/documents/123456/',
);
expect(mockedPut).toHaveBeenCalledWith(
'doc-list',
{ results: [{ id: '123456', test: 'test', title: 'test' }] },
'http://test.jest/documents/?page=1',
);
expect(mockedPut).toHaveBeenCalledTimes(3);
expect(mockedClose).toHaveBeenCalled();
expect(response?.status).toBe(200);
});
it(`checks handlerDidError with type delete`, async () => {
const requestInit = {
request: {
url: 'http://test.jest/documents/123456/',
clone: () => mockedClone(),
headers: new Headers({
'Content-Type': 'application/json',
}),
arrayBuffer: () =>
RequestSerializer.objectToArrayBuffer({
test: 'test',
}),
json: () => ({
test: 'test',
}),
} as unknown as Request,
} as any;
const mockedClone = jest.fn().mockReturnValue(requestInit.request);
const mockedSync = jest.fn().mockResolvedValue({});
const apiPlugin = new ApiPlugin({
type: 'delete',
syncManager: {
sync: () => mockedSync(),
} as any,
});
mockedGetAllKeys.mockResolvedValue(['http://test.jest/documents/?page=1']);
mockedGet.mockResolvedValue({
results: [
{
id: '123456',
title: 'test',
},
{
id: 'another-id',
title: 'test-2',
},
],
});
await apiPlugin.requestWillFetch?.(requestInit);
await apiPlugin.fetchDidFail?.({} as any);
const response = await apiPlugin.handlerDidError?.(requestInit);
expect(mockedDelete).toHaveBeenCalledWith(
'doc-item',
'http://test.jest/documents/123456/',
);
expect(mockedGetAllKeys).toHaveBeenCalledWith('doc-list');
expect(mockedGet).toHaveBeenCalledWith(
'doc-list',
'http://test.jest/documents/?page=1',
);
expect(mockedPut).toHaveBeenCalledWith(
'doc-mutation',
expect.objectContaining({
key: expect.any(String),
requestData: expect.objectContaining({
url: 'http://test.jest/documents/123456/',
}),
}),
expect.any(String),
);
expect(mockedPut).toHaveBeenCalledWith(
'doc-list',
expect.objectContaining({
results: expect.arrayContaining([
{
id: 'another-id',
title: 'test-2',
},
]),
}),
'http://test.jest/documents/?page=1',
);
expect(mockedPut).toHaveBeenCalledTimes(2);
expect(mockedClose).toHaveBeenCalled();
expect(response?.status).toBe(204);
});
it(`checks handlerDidError with type create`, async () => {
Object.defineProperty(global, 'self', {
value: {
crypto: {
randomUUID: jest.fn().mockReturnValue('444555'),
},
},
});
const requestInit = {
request: {
url: 'http://test.jest/documents/',
clone: () => mockedClone(),
headers: new Headers({
'Content-Type': 'application/json',
}),
arrayBuffer: () =>
RequestSerializer.objectToArrayBuffer({
title: 'my new doc',
}),
json: () => ({
title: 'my new doc',
}),
} as unknown as Request,
} as any;
const mockedClone = jest.fn().mockReturnValue(requestInit.request);
const mockedSync = jest.fn().mockResolvedValue({});
const apiPlugin = new ApiPlugin({
type: 'create',
syncManager: {
sync: () => mockedSync(),
} as any,
});
mockedGetAllKeys.mockResolvedValue(['http://test.jest/documents/?page=1']);
mockedGet.mockResolvedValue({
results: [
{
id: '123456',
title: 'test',
},
],
});
await apiPlugin.requestWillFetch?.(requestInit);
await apiPlugin.fetchDidFail?.({} as any);
const response = await apiPlugin.handlerDidError?.(requestInit);
expect(mockedPut).toHaveBeenCalledWith(
'doc-mutation',
expect.objectContaining({
key: expect.any(String),
requestData: expect.any(Object),
}),
expect.any(String),
);
expect(mockedPut).toHaveBeenCalledWith(
'doc-item',
expect.objectContaining({
title: 'my new doc',
}),
'http://test.jest/documents/444555/',
);
expect(mockedPut).toHaveBeenCalledWith(
'doc-list',
expect.objectContaining({
results: expect.arrayContaining([
expect.objectContaining({
id: '444555',
title: 'my new doc',
}),
]),
}),
'http://test.jest/documents/?page=1',
);
expect(mockedGetAllKeys).toHaveBeenCalledWith('doc-list');
expect(mockedGet).toHaveBeenCalledWith(
'doc-list',
'http://test.jest/documents/?page=1',
);
expect(mockedPut).toHaveBeenCalledTimes(3);
expect(mockedClose).toHaveBeenCalled();
expect(response?.status).toBe(201);
});
});

View File

@@ -0,0 +1,74 @@
/**
* @jest-environment node
*/
import '@testing-library/jest-dom';
import { RequestSerializer } from '../RequestSerializer';
describe('RequestSerializer', () => {
it('checks RequestSerializer.fromRequest', async () => {
const request = new Request('http://test.jest', {
method: 'GET',
referrer: 'http://test.jest/referer',
referrerPolicy: 'no-referrer',
mode: 'cors',
credentials: 'omit',
cache: 'default',
redirect: 'follow',
integrity: 'integrity',
keepalive: true,
});
const requestSerializer = await RequestSerializer.fromRequest(request);
expect(requestSerializer.toObject()).toStrictEqual({
cache: 'default',
credentials: 'omit',
headers: {},
integrity: 'integrity',
keepalive: true,
method: 'GET',
mode: 'cors',
redirect: 'follow',
referrer: 'http://test.jest/referer',
referrerPolicy: 'no-referrer',
url: 'http://test.jest/',
});
expect(requestSerializer.toRequest()).toBeInstanceOf(Request);
expect(requestSerializer.clone()).toBeInstanceOf(RequestSerializer);
});
it('checks RequestSerializer.arrayBufferToString', async () => {
const request = new Request('http://test.jest', {
body: JSON.stringify({ test: 'test' }),
method: 'POST',
});
const body = await request.clone().arrayBuffer();
const bodyString = RequestSerializer.arrayBufferToString(body);
expect(bodyString).toBe(JSON.stringify({ test: 'test' }));
});
it('checks RequestSerializer.arrayBufferToJson', async () => {
const request = new Request('http://test.jest', {
body: JSON.stringify({ test: 'test' }),
method: 'POST',
});
const body = await request.clone().arrayBuffer();
const bodyJson = RequestSerializer.arrayBufferToJson(body);
expect(bodyJson).toStrictEqual({ test: 'test' });
});
it('checks RequestSerializer.stringToArrayBuffer', () => {
const bodyString = RequestSerializer.stringToArrayBuffer(
JSON.stringify({ test: 'test' }),
);
expect(bodyString).toBeInstanceOf(ArrayBuffer);
});
});

View File

@@ -0,0 +1,70 @@
/**
* @jest-environment node
*/
import '@testing-library/jest-dom';
import { SyncManager } from '../SyncManager';
const mockedSleep = jest.fn();
jest.mock('@/utils/system', () => ({
sleep: jest.fn().mockImplementation((ms) => mockedSleep(ms)),
}));
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe('SyncManager', () => {
afterEach(() => jest.clearAllMocks());
it('checks SyncManager no sync to do', async () => {
const toSync = jest.fn();
const hasSyncToDo = jest.fn().mockResolvedValue(false);
new SyncManager(toSync, hasSyncToDo);
await delay(100);
expect(hasSyncToDo).toHaveBeenCalled();
expect(toSync).not.toHaveBeenCalled();
});
it('checks SyncManager sync to do', async () => {
const toSync = jest.fn();
const hasSyncToDo = jest.fn().mockResolvedValue(true);
new SyncManager(toSync, hasSyncToDo);
await delay(100);
expect(hasSyncToDo).toHaveBeenCalled();
expect(toSync).toHaveBeenCalled();
});
it('checks SyncManager sync to do trigger error', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const toSync = jest.fn().mockRejectedValue(new Error('error'));
const hasSyncToDo = jest.fn().mockResolvedValue(true);
new SyncManager(toSync, hasSyncToDo);
await delay(100);
expect(hasSyncToDo).toHaveBeenCalled();
expect(toSync).toHaveBeenCalled();
expect(console.error).toHaveBeenCalledWith(
'SW-DEV: SyncManager.sync failed:',
new Error('error'),
);
});
it('checks SyncManager multiple sync to do', async () => {
const toSync = jest.fn().mockReturnValue(delay(200));
const hasSyncToDo = jest.fn().mockResolvedValue(true);
const syncManager = new SyncManager(toSync, hasSyncToDo);
await syncManager.sync();
expect(hasSyncToDo).toHaveBeenCalled();
expect(mockedSleep).toHaveBeenCalledWith(300);
expect(mockedSleep).toHaveBeenCalledTimes(15);
expect(toSync).toHaveBeenCalledTimes(1);
});
});