-
Notifications
You must be signed in to change notification settings - Fork 114
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 - notifScheduler.ts #1092
‼️📅 Rewrite - notifScheduler.ts #1092
Conversation
Translated notifScheduler.js from Angular to React Typescript commHelper.ts - Added a temporary "any" return type for getUser because the TS needed it ProfileSettings.jsx - Imported the new useSchedulerHelper function (name in progress - need to find a good name for this hook) - Replaced any instances of the old Angular NotificationScheduler module with the new schedulerHelper hook notifScheduler.ts - Translated the file from Angular to React TS - This file exports a single function which initializes the appConfig and then returns an object containing the functions that the Notification Scheduler functions need (only ProfileSettings.jsx is using this file currently) - Replaced any instances of moment with Luxon equivalent
@JGreenlee Added this to your review channel and @ you for visibility If you have a moment, could you take a look at this at a high level and let me know if the structure needs to be changed at all? I also have another question: Is notifScheduler at a high level only being used by programs and studies that use |
Sorry, my finger slipped and it accidentally closed the PR 😖 |
To-Do:
@JGreenlee can you think of anything else I might want to add to this to-do list? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left my suggestions! In short, I think the React hook is unnecessary because ProfileSettings
already has access to appConfig
, so you can pass its reminderSchemes
into the notifScheduler
functions.
And here's my answer to that question:
I also have another question: Is notifScheduler at a high level only being used by programs and studies that use
reminderSchemes
in their config?
Correct- if reminderSchemes
is not present, nothing in notifScheduler
should run
www/js/commHelper.ts
Outdated
@@ -147,7 +147,7 @@ export function updateUser(updateDoc) { | |||
}); | |||
} | |||
|
|||
export function getUser() { | |||
export function getUser(): any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you said, we'd ideally type this out properly. I understand if need to use any
in the short term, but instead of adding an any
return type here, I think you should be able to do this at the place where getUser()
is being called.
Like so:
const user = getUser() as any;
Let me know if that works!
www/js/splash/notifScheduler.ts
Outdated
export function useSchedulerHelper() { | ||
const appConfig = useAppConfig(); | ||
const [reminderSchemes, setReminderSchemes] = useState(); | ||
|
||
useEffect(() => { | ||
if (!appConfig) { | ||
logDebug("No reminder schemes found in config, not scheduling notifications"); | ||
return; | ||
} | ||
setReminderSchemes(appConfig.reminderSchemes); | ||
}, [appConfig]); | ||
|
||
//setUpActions(); | ||
update(reminderSchemes); | ||
|
||
return { | ||
setReminderPrefs: (newPrefs) => setReminderPrefs(newPrefs, reminderSchemes), | ||
getReminderPrefs: () => getReminderPrefs(reminderSchemes), | ||
getScheduledNotifs: () => getScheduledNotifs(), | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After seeing the code, I actually think this would be simpler and more concise without using a React hook.
There isn't anything about this code that is necessarily React-based or having to do with the UI components. It's really a matter of conditionally formatting data, which we can do just as well in plain TS.
We already have access to the appConfig
/uiConfig
in ProfileSettings.jsx
, so it's a bit redundant to invoke useAppConfig()
again in this hook.
And reminderSchemes
is just part of appConfig
- it doesn't really need to be its own state.
www/js/splash/notifScheduler.ts
Outdated
} | ||
|
||
// determines when notifications are needed, and schedules them if not already scheduled | ||
const update = async (reminderSchemes) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you already accept reminderSchemes
as an argument to all the functions that need it, which a decision that I think is fine.
So why not export this function and call it directly from ProfileSettings
? ProfileSettings
has appConfig
, which is all you need
www/js/control/ProfileSettings.jsx
Outdated
@@ -25,6 +25,7 @@ import { AppContext } from "../App"; | |||
import { shareQR } from "../components/QrCode"; | |||
import { storageClear } from "../plugin/storage"; | |||
import { getAppVersion } from "../plugin/clientStats"; | |||
import { useSchedulerHelper } from "../splash/notifScheduler"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you export update
, getScheduledNotifs
, getReminderPrefs
, and setReminderPrefs
directly from notifScheduler.ts
, you can import them as individual functions instead of useSchedulerHelper
www/js/control/ProfileSettings.jsx
Outdated
@@ -173,13 +175,13 @@ const ProfileSettings = () => { | |||
const newNotificationSettings ={}; | |||
|
|||
if (uiConfig?.reminderSchemes) { | |||
const prefs = await NotificationScheduler.getReminderPrefs(); | |||
const prefs = await schedulerHelper.getReminderPrefs(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will become await getReminderPrefs(uiConfig.reminderSchemes)
www/js/splash/notifScheduler.ts
Outdated
}, [appConfig]); | ||
|
||
//setUpActions(); | ||
update(reminderSchemes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you remove the React hook, you'll need to do this update
call somewhere else. You can do it in ProfileSettings
somewhere in the whenReady
function.
www/js/splash/notifScheduler.ts
Outdated
} catch (e) { | ||
console.log("ERROR: Could not find reminder scheme for assignment " + reminderSchemes + " - " + reminder_assignment); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appreciate the addition of this catch block - but instead of console.log
let's displayErrorMsg
(from logger.ts)
In order to not mess with commHelper.ts as it is outside of the scope of this issue - related to e-mission#1092 (comment)
During the prettier changes, a bunch of the imports got merged. Undoing in this commit: e-mission@1bfb113
www/js/control/ProfileSettings.jsx
Outdated
@@ -187,13 +201,13 @@ const ProfileSettings = () => { | |||
const newNotificationSettings = {}; | |||
|
|||
if (uiConfig?.reminderSchemes) { | |||
const prefs = await NotificationScheduler.getReminderPrefs(); | |||
const prefs = await getReminderPrefs(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function takes reminderSchemes as a parameter, doesn't it?
const prefs = await getReminderPrefs(); | |
const prefs = await getReminderPrefs(uiConfig.reminderSchemes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did forget this, good catch! It was slipping by in my testing without this, because reminderSchemes
is only used in getReminderPrefs()
if the user object does not already exist on the server and it needs to do the initialization of notification-scheduling for a new user
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@JGreenlee do we also want to add unit testing within our rewrite PRs? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I almost forgot!
We should include unit tests for as much of this as possible.
Although unit tests can't actually verify that native notifications will display on a phone, you can verify that the correct notifications are getting scheduled for the right times.
You could have a mock implementation for the native plugin which keeps track of all the scheduled notifications.
I'm also having a significant problem: my initial testing after pushing the last commits was working fine, but now I have 1. Created a new OPcode, and 2. reloaded the app, and now the app is crashing when I so much as open the profile settings screen: Untitled.mov@JGreenlee do you have any tips for debugging an issue that causes the app to crash? Maybe a log dump or something that can give me insight into what is happening to make the app crash? The phone serve does not crash, just the app itself |
I also had to adjust the error coming from notifScheduler because it wasn't helping much with the way it was defined before
This tests the final functionality and longest path through this function
…-mission-phone into notifScheduler-rewrite
Resolved merge conflicts |
I am getting a bit stuck on the next test and was wondering if anyone had any experience or ideas in terms of changing a Jest mocked function implementation for only one test (I commented the word THIS to show what I'm talking about: // ...Somewhere earlier in the Jest file (outside of any of the `describes`)
jest.mock('../js/services/commHelper', () => ({
...jest.requireActual('../js/services/commHelper'),
getUser: jest.fn(() =>
Promise.resolve({
// These values are **important**...
// reminder_assignment: must match a key from the reminder scheme above,
// reminder_join_date: must match the first day of the mocked notifs below in the tests,
// reminder_time_of_day: must match the defaultTime from the chosen reminder_assignment in the reminder scheme above
reminder_assignment: 'weekly',
reminder_join_date: '2023-11-14',
reminder_time_of_day: '21:00',
}),
),
updateUser: jest.fn(() => Promise.resolve()),
}));
// ...Inside the test for getReminderPrefs
it('should resolve with newly initialilzed reminder prefs when user does not exist', async () => {
// getReminderPrefs arguments
const reminderSchemes: any = exampleReminderSchemes;
let isScheduling: boolean = true;
const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val));
const scheduledPromise: Promise<any> = Promise.resolve();
// create the expected result
const expectedResult = {
reminder_assignment: 'weekly',
reminder_join_date: '2023-11-14',
reminder_time_of_day: '21:00',
};
// --------------
// THIS
// --------------
// mock the return value of getUser (a promise that resolves with empty object {}
// this way, the getReminderPrefs skips the logic for "user already exists" and tries to set them up for the first time
jest.doMock('../js/commHelper', () => ({
...jest.requireActual('../js/commHelper'),
getUser: jest.fn(() => Promise.resolve({})),
updateUser: jest.fn(() => Promise.resolve()),
}));
// call the function
const { reminder_assignment, reminder_join_date, reminder_time_of_day } =
await getReminderPrefs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise);
expect(logDebug).toHaveBeenCalledWith('User just joined, Initializing reminder prefs');
}); The issue is that when I set a I looked at this stack overflow discussion but it doesn't appear to have a direct answer (or a few other similar threads), aside from changing the way the mock is implemented entirely and just mocking within each individual test (which I don't think is a good solution because it will make this file HUGE). |
@sebastianbarry, I think I've worked on similar situations, and I would recommend checking how the user is stored (some things are cached locally, and so even if you overwrite the fetching process, it will use what was fetched in another test). Maybe you'd be able to get some ideas from the tests I wrote in #1063 or the discussion in that PR, I know changing the mocks out was something that Jack and I discussed/worked on in review. And in #1093 with the tests for |
For re-mocking, you may want to check out the .mockImplementationOnce() function! FWIW, I've personally had some issues making it run smoothly - but if I understand your problem correctly, this may be a solution. You could try something like... jest.mock('../js/services/commHelper', () => ({
...jest.requireActual('../js/services/commHelper'),
getUser: jest.fn(() =>
Promise.resolve({ // This is the default mock
// These values are **important**...
// reminder_assignment: must match a key from the reminder scheme above,
// reminder_join_date: must match the first day of the mocked notifs below in the tests,
// reminder_time_of_day: must match the defaultTime from the chosen reminder_assignment in the reminder scheme above
reminder_assignment: 'weekly',
reminder_join_date: '2023-11-14',
reminder_time_of_day: '21:00',
}) // The "Once" will get called before the default, so you may need to reorder the tests...
.mockImplementationOnce(() => {Promise.resolve({})),
),
updateUser: jest.fn(() => Promise.resolve()), Obviously it doesn't feel the best mocking functions on a one-off basis, but this would let you keep using the original mock at least! If not this, I found a workaround by mocking the higher-order functions, so that different inputs returned different values (The code for those mocks is here for how I got around using |
…-mission-phone into notifScheduler-rewrite
If there are no reminderSchemes in the appConfig, notifScheduler doesn't need to do anything so let's not invoke it. Also, let's create the type definition for reminderSchemes and use it here in notifScheduler.
Just resolved merge conflicts and fixed a bug. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rewrite itself looks good to me.
Not every function is tested yet, but the tests we do have are passing. I suggest we merge this as-is to unblock #1106 and then fill in the rest of the tests in a later PR.
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## service_rewrite_2023 #1092 +/- ##
========================================================
+ Coverage 58.80% 62.11% +3.30%
========================================================
Files 26 27 +1
Lines 1420 1544 +124
Branches 320 334 +14
========================================================
+ Hits 835 959 +124
Misses 585 585
Flags with carried forward coverage won't be shown. Click here to find out more.
|
notifScheduler.test.ts - Imported getUser and updateUser so that we can mock them in the tests - Imported addStatReading so we can mock it to do nothing - Removed the old way of mocking the implementation of getUser, and the new way allows us to change them per-test - Added automatic mock for clientStats - added beforeEach statements to mock getUser inside the describe blocks -
- Tests to see if setReminderPrefs works when called in the way that ProfileSettings.jsx calls it
After committing those final tests, I believe this PR is ready for review! 🎉 @JGreenlee the main things I'm interested in is:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of tests is to verify that when a function is called, the expected result is achieved. It looks to me like after calling updateScheduledNotifs
, we don't actually make sure that those notifications were scheduled.
I think we need to revise the approach for testing that function, but I am still going to recommend merging this now and bulking up the tests in a later PR.
www/__tests__/notifScheduler.test.ts
Outdated
await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); | ||
|
||
expect(setIsScheduling).toHaveBeenCalledWith(true); | ||
expect(logDebug).toHaveBeenCalledWith('After cancelling, there are no scheduled notifications'); | ||
expect(logDebug).toHaveBeenCalledWith('After scheduling, there are no scheduled notifications'); | ||
expect(setIsScheduling).toHaveBeenCalledWith(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests should be checking the functionality of notifScheduler
. In this instance we want to see that the appropriate notifications are scheduled. The logDebug
statements are good for dev purposes but I don't think they should be the focus of our testing strategy.
After calling updateScheduledNotifs(...)
we should verify that the function did what it was supposed to do.
In this case, I think the best way to do that is to then call getScheduledNotifs(...)
and check the result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JGreenlee I added the test you suggested, to test getScheduledNotifs(...)
in ec9729d
Great suggestion, thanks!
expect(logDebug).toHaveBeenCalledWith( | ||
'ERROR: Already scheduling notifications, not scheduling again', | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this instance, though, I think checking the logDebug
does make sense because we expect updateScheduledNotifs
to have quit early and not scheduled any notifications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with:
The logDebug statements are good for dev purposes but I don't think they should be the focus of our testing strategy.
In a future fix, I would suggest changing the underlying function to return a value that you can check, and then changing the test to check it
// call the function | ||
await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); | ||
|
||
expect(logDebug).toHaveBeenCalledWith('Error: Reminder scheme not found'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is fine too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
… scheduled - There were many places where I used to/fromMillis instead of to/fromJSDate, causing a type difference that was causing errors - Added a i18n mock because not having a language code return was messing with the debugScheduledNotifs log outputs - Added a funcitonality for mockNotifs in the test, so that it can be adjusted, cleared, added to, etc. so that we can test with getScheduledNotifs if it successfully scheduled the notifs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some comments/questions, but am going to go ahead and merge this so that we can get the release out and clean this up later, consistent with
I suggest we merge this as-is to unblock #1106 and then fill in the rest of the tests in a later PR.
promiseList.push( | ||
getReminderPrefs(uiConfig.reminderSchemes, isScheduling, setIsScheduling, scheduledPromise), | ||
); | ||
promiseList.push(getScheduledNotifs(isScheduling, scheduledPromise)); | ||
let resultList = await Promise.all(promiseList); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, future fix: can you add a comment about why we are reading both the values together instead of two separate awaits? It is fine if the answer is "it is cleaner" - I just wanted to understand the reason for the change.
@@ -1,5 +1,11 @@ | |||
export const mockLogger = () => { | |||
window['Logger'] = { log: console.log }; | |||
window.alert = (msg) => { | |||
console.log(msg); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are keeping the logDebug
tests, we should change this from console.log
to logDebug
so we can test the alerting behavior.
isScheduling: boolean, | ||
setIsScheduling: Function, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where are these actually used? I tried to follow it through, but it looks like a circular invocation:
updateScheduledNotifs
-> getReminderPrefs
-> setReminderPrefs
-> updateScheduledNotifs
and getReminderPrefs
but I don't see where this is actually invoked!?
let reminderSchemes: any = exampleReminderSchemes; | ||
delete reminderSchemes.weekly; // delete the weekly reminder scheme, to create a missing reminder scheme error | ||
let isScheduling: boolean = false; | ||
const setIsScheduling: Function = jest.fn((val: boolean) => (isScheduling = val)); | ||
const scheduledPromise: Promise<any> = Promise.resolve(); | ||
// call the function | ||
await updateScheduledNotifs(reminderSchemes, isScheduling, setIsScheduling, scheduledPromise); | ||
|
||
expect(logDebug).toHaveBeenCalledWith('Error: Reminder scheme not found'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this. you are removing the weekly
reminderScheme from the local in-memory variable.
There are no existing reminders, right? So how would updateScheduledNotifs
know that there was supposed to have been a weekly scheme?
After this merge, @sebastianbarry please fix ASAP
|
Translated notifScheduler.js from Angular to React Typescript
commHelper.ts
ProfileSettings.jsx
notifScheduler.ts