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

✏️ Rewrite KVStore, ClientStats, and CommHelper #1040

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
73a8259
add storage.ts, to replace KVStore
JGreenlee Sep 27, 2023
b99e001
use storage.ts everywhere instead of KVStore
JGreenlee Sep 27, 2023
6ffbf18
remove KVStore / ngStorage.js
JGreenlee Sep 27, 2023
cdabde4
use JSDOM as jest env so localStorage works
JGreenlee Sep 28, 2023
fd4f9d5
add tests for storage.ts, plus the needed mocks
JGreenlee Sep 28, 2023
8ada4f8
add clientStats.js, to replace ClientStats service
JGreenlee Sep 28, 2023
6e59af5
use clientStats.ts everywhere, not old ClientStats
JGreenlee Sep 28, 2023
25c7077
remove the old ClientStats / clientstats.js
JGreenlee Sep 28, 2023
1a47634
fix getAppVersion in clientstats
JGreenlee Sep 28, 2023
598d1fb
add some cordova mocks: cordova,device,appversion
JGreenlee Sep 28, 2023
47aceaf
make getAppVersion work async
JGreenlee Sep 28, 2023
c7b06b7
add tests for clientStats.ts
JGreenlee Sep 28, 2023
c16bde5
rewrite CommHelper service into commHelper.ts
JGreenlee Sep 28, 2023
1dc7451
use commHelper.ts everywhere, not old CommHelper
JGreenlee Sep 28, 2023
f78dab1
remove the old CommHelper service
JGreenlee Sep 28, 2023
5826213
Merge branch 'react_navigation_new_onboarding' of https://github.com/…
JGreenlee Sep 29, 2023
77fe30a
add commHelper.test.ts
JGreenlee Sep 29, 2023
a5fcbfe
remove backwards-compat munge in storage.ts
JGreenlee Oct 11, 2023
40ce229
don't displayErrorMsg in storage.ts
JGreenlee Oct 11, 2023
37ac065
remove unneeded comment
JGreenlee Oct 11, 2023
cfd7829
show error if 'db' not defined in clientStats.ts
JGreenlee Oct 11, 2023
ea2b8c5
re-implement functions of commHelper
JGreenlee Oct 12, 2023
641e8aa
don't "processErrorMessages" in commHelper
JGreenlee Oct 12, 2023
03dc94e
update comment in commHelper.test.ts
JGreenlee Oct 12, 2023
acb36aa
cordovaMocks: use cordova-ios version from json
JGreenlee Oct 12, 2023
e546d73
commHelper: promisify 'registerUser'
JGreenlee Oct 12, 2023
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
4 changes: 4 additions & 0 deletions jest.config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"testEnvironment": "jsdom",
"testPathIgnorePatterns": [
"/node_modules/",
"/platforms/",
Expand All @@ -9,6 +10,9 @@
"transform": {
"^.+\\.(ts|tsx|js|jsx)$": "ts-jest"
},
"transformIgnorePatterns": [
"/node_modules/(?!(@react-native|react-native|react-native-vector-icons))"
],
"moduleNameMapper": {
"^react-native$": "react-native-web"
}
Expand Down
1 change: 1 addition & 0 deletions package.serve.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"expose-loader": "^4.1.0",
"file-loader": "^6.2.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"phonegap": "9.0.0+cordova.9.0.0",
"process": "^0.11.10",
"sass": "^1.62.1",
Expand Down
95 changes: 95 additions & 0 deletions www/__mocks__/cordovaMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import packageJsonBuild from '../../package.cordovabuild.json';

export const mockCordova = () => {
window['cordova'] ||= {};
window['cordova'].platformId ||= 'ios';
window['cordova'].platformVersion ||= packageJsonBuild.dependencies['cordova-ios'];
window['cordova'].plugins ||= {};
}

export const mockDevice = () => {
window['device'] ||= {};
window['device'].platform ||= 'ios';
window['device'].version ||= '14.0.0';
}

export const mockGetAppVersion = () => {
const mockGetAppVersion = {
getAppName: () => new Promise((rs, rj) => setTimeout(() => rs('Mock App'), 10)),
getPackageName: () => new Promise((rs, rj) => setTimeout(() => rs('com.example.mockapp'), 10)),
getVersionCode: () => new Promise((rs, rj) => setTimeout(() => rs('123'), 10)),
getVersionNumber: () => new Promise((rs, rj) => setTimeout(() => rs('1.2.3'), 10)),
}
window['cordova'] ||= {};
window['cordova'].getAppVersion = mockGetAppVersion;
}

export const mockBEMUserCache = () => {
const _cache = {};
const messages = [];
const mockBEMUserCache = {
getLocalStorage: (key: string, isSecure: boolean) => {
return new Promise((rs, rj) =>
setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume that the setTimeout is to delay the response.

future fix: you could pass in the timeout as a parameter to the mock (assuming that something like mockBEMUserCache(delay) works and then we could test what happens if the plugins are slow.

rs(_cache[key]);
}, 100)
);
},
putLocalStorage: (key: string, value: any) => {
return new Promise<void>((rs, rj) =>
setTimeout(() => {
_cache[key] = value;
rs();
}, 100)
);
},
removeLocalStorage: (key: string) => {
return new Promise<void>((rs, rj) =>
setTimeout(() => {
delete _cache[key];
rs();
}, 100)
);
},
clearAll: () => {
return new Promise<void>((rs, rj) =>
setTimeout(() => {
for (let p in _cache) delete _cache[p];
rs();
}, 100)
);
},
listAllLocalStorageKeys: () => {
return new Promise<string[]>((rs, rj) =>
setTimeout(() => {
rs(Object.keys(_cache));
}, 100)
);
},
listAllUniqueKeys: () => {
return new Promise<string[]>((rs, rj) =>
setTimeout(() => {
rs(Object.keys(_cache));
shankari marked this conversation as resolved.
Show resolved Hide resolved
}, 100)
);
},
putMessage: (key: string, value: any) => {
return new Promise<void>((rs, rj) =>
setTimeout(() => {
messages.push({ key, value });
rs();
}, 100)
);
},
getAllMessages: (key: string, withMetadata?: boolean) => {
return new Promise<any[]>((rs, rj) =>
setTimeout(() => {
rs(messages.filter(m => m.key == key).map(m => m.value));
}, 100)
);
}
}
window['cordova'] ||= {};
window['cordova'].plugins ||= {};
window['cordova'].plugins.BEMUserCache = mockBEMUserCache;
}
3 changes: 3 additions & 0 deletions www/__mocks__/globalMocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const mockLogger = () => {
window['Logger'] = { log: console.log };
}
52 changes: 52 additions & 0 deletions www/__tests__/clientStats.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { mockBEMUserCache, mockDevice, mockGetAppVersion } from "../__mocks__/cordovaMocks";
import { addStatError, addStatEvent, addStatReading, getAppVersion, statKeys } from "../js/plugin/clientStats";

mockDevice();
// this mocks cordova-plugin-app-version, generating a "Mock App", version "1.2.3"
mockGetAppVersion();
// clientStats.ts uses BEMUserCache to store the stats, so we need to mock that too
shankari marked this conversation as resolved.
Show resolved Hide resolved
mockBEMUserCache();
const db = window['cordova']?.plugins?.BEMUserCache;

it('gets the app version', async () => {
const ver = await getAppVersion();
expect(ver).toEqual('1.2.3');
});

it('stores a client stats reading', async () => {
const reading = { a: 1, b: 2 };
await addStatReading(statKeys.REMINDER_PREFS, reading);
const storedMessages = await db.getAllMessages('stats/client_time', false);
expect(storedMessages).toContainEqual({
name: statKeys.REMINDER_PREFS,
ts: expect.any(Number),
reading,
client_app_version: '1.2.3',
client_os_version: '14.0.0'
});
});

it('stores a client stats event', async () => {
await addStatEvent(statKeys.BUTTON_FORCE_SYNC);
const storedMessages = await db.getAllMessages('stats/client_nav_event', false);
expect(storedMessages).toContainEqual({
name: statKeys.BUTTON_FORCE_SYNC,
ts: expect.any(Number),
reading: null,
client_app_version: '1.2.3',
client_os_version: '14.0.0'
});
});

it('stores a client stats error', async () => {
const errorStr = 'test error';
await addStatError(statKeys.MISSING_KEYS, errorStr);
const storedMessages = await db.getAllMessages('stats/client_error', false);
expect(storedMessages).toContainEqual({
name: statKeys.MISSING_KEYS,
ts: expect.any(Number),
reading: errorStr,
client_app_version: '1.2.3',
client_os_version: '14.0.0'
});
});
42 changes: 42 additions & 0 deletions www/__tests__/commHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mockLogger } from '../__mocks__/globalMocks';
import { fetchUrlCached } from '../js/commHelper';

mockLogger();

// mock for JavaScript 'fetch'
// we emulate a 100ms delay when i) fetching data and ii) parsing it as text
global.fetch = (url: string) => new Promise((rs, rj) => {
setTimeout(() => rs({
text: () => new Promise((rs, rj) => {
setTimeout(() => rs('mock data for ' + url), 100);
})
}));
}) as any;

it('fetches text from a URL and caches it so the next call is faster', async () => {
const tsBeforeCalls = Date.now();
const text1 = await fetchUrlCached('https://raw.githubusercontent.com/e-mission/e-mission-phone/master/README.md');
const tsBetweenCalls = Date.now();
const text2 = await fetchUrlCached('https://raw.githubusercontent.com/e-mission/e-mission-phone/master/README.md');
const tsAfterCalls = Date.now();
expect(text1).toEqual(expect.stringContaining('mock data'));
expect(text2).toEqual(expect.stringContaining('mock data'));
expect(tsAfterCalls - tsBetweenCalls).toBeLessThan(tsBetweenCalls - tsBeforeCalls);
});

/* The following functions from commHelper.ts are not tested because they are just wrappers
around the native functions in BEMServerComm.
If we wanted to test them, we would need to mock the native functions in BEMServerComm.
It would be better to do integration tests that actually call the native functions.

* - getRawEntries
* - getRawEntriesForLocalDate
* - getPipelineRangeTs
* - getPipelineCompleteTs
* - getMetrics
* - getAggregateData
* - registerUser
* - updateUser
* - getUser
* - putOne
*/
74 changes: 74 additions & 0 deletions www/__tests__/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { mockBEMUserCache } from "../__mocks__/cordovaMocks";
import { mockLogger } from "../__mocks__/globalMocks";
import { storageClear, storageGet, storageRemove, storageSet } from "../js/plugin/storage";

// mocks used - storage.ts uses BEMUserCache and logging.
// localStorage is already mocked for us by Jest :)
mockLogger();
mockBEMUserCache();

it('stores a value and retrieves it back', async () => {
await storageSet('test1', 'test value 1');
const retVal = await storageGet('test1');
expect(retVal).toEqual('test value 1');
});

it('stores a value, removes it, and checks that it is gone', async () => {
await storageSet('test2', 'test value 2');
await storageRemove('test2');
const retVal = await storageGet('test2');
expect(retVal).toBeUndefined();
});

it('can store objects too', async () => {
const obj = { a: 1, b: 2 };
await storageSet('test6', obj);
const retVal = await storageGet('test6');
expect(retVal).toEqual(obj);
});

it('can also store complex nested objects with arrays', async () => {
const obj = { a: 1, b: { c: [1, 2, 3] } };
await storageSet('test7', obj);
const retVal = await storageGet('test7');
expect(retVal).toEqual(obj);
});

it('preserves values if local gets cleared', async () => {
await storageSet('test3', 'test value 3');
await storageClear({ local: true });
const retVal = await storageGet('test3');
expect(retVal).toEqual('test value 3');
});

it('preserves values if native gets cleared', async () => {
await storageSet('test4', 'test value 4');
await storageClear({ native: true });
const retVal = await storageGet('test4');
expect(retVal).toEqual('test value 4');
});

it('does not preserve values if both local and native are cleared', async () => {
await storageSet('test5', 'test value 5');
await storageClear({ local: true, native: true });
const retVal = await storageGet('test5');
expect(retVal).toBeUndefined();
});

it('preserves values if local gets cleared, then retrieved, then native gets cleared', async () => {
await storageSet('test8', 'test value 8');
await storageClear({ local: true });
await storageGet('test8');
await storageClear({ native: true });
const retVal = await storageGet('test8');
expect(retVal).toEqual('test value 8');
});

it('preserves values if native gets cleared, then retrieved, then local gets cleared', async () => {
shankari marked this conversation as resolved.
Show resolved Hide resolved
await storageSet('test9', 'test value 9');
await storageClear({ native: true });
await storageGet('test9');
await storageClear({ local: true });
const retVal = await storageGet('test9');
expect(retVal).toEqual('test value 9');
});
2 changes: 0 additions & 2 deletions www/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import './css/main.diary.css';
import 'leaflet/dist/leaflet.css';

import './js/ngApp.js';
import './js/stats/clientstats.js';
import './js/splash/referral.js';
import './js/splash/customURL.js';
import './js/splash/startprefs.js';
Expand All @@ -31,4 +30,3 @@ import './js/control/uploadService.js';
import './js/metrics-factory.js';
import './js/metrics-mappings.js';
import './js/plugin/logger.ts';
import './js/plugin/storage.js';
55 changes: 0 additions & 55 deletions www/js/angular-react-helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,6 @@
// Modified to use React 18 and wrap elements with the React Native Paper Provider

import angular from 'angular';
import { createRoot } from 'react-dom/client';
import React from 'react';
import { Provider as PaperProvider, MD3LightTheme as DefaultTheme, MD3Colors } from 'react-native-paper';
import { getTheme } from './appTheme';

function toBindings(propTypes) {
const bindings = {};
Object.keys(propTypes).forEach(key => bindings[key] = '<');
return bindings;
}

function toProps(propTypes, controller) {
const props = {};
Object.keys(propTypes).forEach(key => props[key] = controller[key]);
return props;
}

export function angularize(component, name, modulePath) {
component.module = modulePath;
const nameCamelCase = name[0].toLowerCase() + name.slice(1);
angular
.module(modulePath, [])
.component(nameCamelCase, makeComponentProps(component));
}

const theme = getTheme();
export function makeComponentProps(Component) {
const propTypes = Component.propTypes || {};
return {
bindings: toBindings(propTypes),
controller: ['$element', function($element) {
/* TODO: once the inf scroll list is converted to React and no longer uses
collection-repeat, we can just set the root here one time
and will not have to reassign it in $onChanges. */
/* Until then, React will complain everytime we reassign an element's root */
let root;
this.$onChanges = () => {
root = createRoot($element[0]);
const props = toProps(propTypes, this);
root.render(
<PaperProvider theme={theme}>
<style type="text/css">{`
@font-face {
font-family: 'MaterialCommunityIcons';
src: url(${require('react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf')}) format('truetype');
}`}
</style>
<Component { ...props } />
</PaperProvider>
);
};
this.$onDestroy = () => root.unmount();
}]
};
}

export function getAngularService(name: string) {
const injector = angular.element(document.body).injector();
Expand Down
Loading