diff --git a/package-lock.json b/package-lock.json index b6e8225838..54ed827b29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1542,18 +1542,18 @@ } }, "apidoc": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.23.0.tgz", - "integrity": "sha512-bn2QNaqyyL5ihRUlBBqwffElJg+hAgxAWPDdjJiLxDJ66Jiw+jf8uucFLYC9XHFqjdM7YTqLluM2PPshrqzObg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.24.0.tgz", + "integrity": "sha512-1aKEMOJaX9coPTwbVcDtCcxtv7VIhR6YEWxWMtoKouYaaMqmScTBikq8umIH8pjr77F2AjrNWFiLhfwsHLsvkA==", "requires": { "apidoc-core": "^0.11.1", "commander": "^2.20.0", "fs-extra": "^8.1.0", "handlebars": "^4.7.6", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "markdown-it": "^10.0.0", - "nodemon": "^2.0.3", - "winston": "^3.2.1" + "nodemon": "^2.0.4", + "winston": "^3.3.3" } }, "apidoc-core": { @@ -3008,6 +3008,15 @@ "check-error": "^1.0.2" } }, + "chai-moment": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chai-moment/-/chai-moment-0.1.0.tgz", + "integrity": "sha1-SpFoDPo6dc/aGEULK6tltIyQJAE=", + "dev": true, + "requires": { + "moment": "^2.10.6" + } + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -9227,9 +9236,9 @@ } }, "mongoose": { - "version": "5.9.24", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.24.tgz", - "integrity": "sha512-uxTLy/ExYmOfKvvbjn1PHbjSJg0SQzff+dW6jbnywtbBcfPRC/3etnG9hPv6KJe/5TFZQGxCyiSezkqa0+iJAQ==", + "version": "5.9.25", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.9.25.tgz", + "integrity": "sha512-vz/DqJ3mrHqEIlfRbKmDZ9TzQ1a0hCtSQpjHScIxr4rEtLs0tjsXDeEWcJ/vEEc3oLfP6vRx9V+uYSprXDUvFQ==", "requires": { "bson": "^1.1.4", "kareem": "2.3.1", @@ -11106,9 +11115,9 @@ } }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" }, "regenerator-transform": { "version": "0.14.5", diff --git a/package.json b/package.json index 5e9d762719..dc86f1cbee 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "accepts": "^1.3.5", "amazon-payments": "^0.2.8", "amplitude": "^3.5.0", - "apidoc": "^0.23.0", + "apidoc": "^0.24.0", "apn": "^2.2.0", "apple-auth": "^1.0.6", "bcrypt": "^5.0.0", @@ -48,7 +48,7 @@ "method-override": "^3.0.0", "moment": "^2.27.0", "moment-recur": "^1.0.7", - "mongoose": "^5.9.23", + "mongoose": "^5.9.25", "morgan": "^1.10.0", "nconf": "^0.10.0", "node-gcm": "^1.0.3", @@ -62,7 +62,7 @@ "ps-tree": "^1.0.0", "rate-limiter-flexible": "^2.1.7", "redis": "^3.0.2", - "regenerator-runtime": "^0.13.5", + "regenerator-runtime": "^0.13.7", "remove-markdown": "^0.3.0", "rimraf": "^3.0.2", "short-uuid": "^3.0.0", @@ -112,6 +112,7 @@ "axios": "^0.19.2", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", + "chai-moment": "^0.1.0", "chalk": "^4.1.0", "cross-spawn": "^7.0.3", "expect.js": "^0.3.1", diff --git a/test/api/unit/libs/cron.test.js b/test/api/unit/libs/cron.test.js index 1544a8c373..ee0271d82f 100644 --- a/test/api/unit/libs/cron.test.js +++ b/test/api/unit/libs/cron.test.js @@ -42,13 +42,13 @@ describe('cron', () => { }); it('updates user.preferences.timezoneOffsetAtLastCron', () => { - const timezoneOffsetFromUserPrefs = 1; + const timezoneUtcOffsetFromUserPrefs = -1; cron({ - user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs, + user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs, }); - expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs); + expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1); }); it('resets user.items.lastDrop.count', () => { @@ -240,7 +240,7 @@ describe('cron', () => { user1.purchased.plan.consecutive.gemCapExtra = 0; it('does not increment consecutive benefits after the first month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -256,7 +256,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits after the second month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -272,7 +272,7 @@ describe('cron', () => { }); it('increments consecutive benefits after the third month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -288,7 +288,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits after the fourth month', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months') .add(2, 'days') .toDate()); // Add 1 month to simulate what happens a month after the subscription was created. @@ -304,7 +304,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months') .add(2, 'days') .toDate()); cron({ @@ -339,7 +339,7 @@ describe('cron', () => { user3.purchased.plan.consecutive.gemCapExtra = 5; it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -352,7 +352,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); cron({ @@ -365,7 +365,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); cron({ @@ -378,7 +378,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the second paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months') .add(2, 'days') .toDate()); cron({ @@ -391,7 +391,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months') .add(2, 'days') .toDate()); cron({ @@ -404,7 +404,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months') .add(2, 'days') .toDate()); cron({ @@ -417,7 +417,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the third paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months') .add(2, 'days') .toDate()); cron({ @@ -430,7 +430,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(10, 'months') .add(2, 'days') .toDate()); cron({ @@ -465,7 +465,7 @@ describe('cron', () => { user6.purchased.plan.consecutive.gemCapExtra = 10; it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -478,7 +478,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(6, 'months') .add(2, 'days') .toDate()); cron({ @@ -491,7 +491,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the second paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months') .add(2, 'days') .toDate()); cron({ @@ -504,7 +504,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the third paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months') .add(2, 'days') .toDate()); cron({ @@ -517,7 +517,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(19, 'months') .add(2, 'days') .toDate()); cron({ @@ -552,7 +552,7 @@ describe('cron', () => { user12.purchased.plan.consecutive.gemCapExtra = 20; it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -565,7 +565,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(12, 'months') .add(2, 'days') .toDate()); cron({ @@ -578,7 +578,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the second paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months') .add(2, 'days') .toDate()); cron({ @@ -591,7 +591,7 @@ describe('cron', () => { }); it('increments consecutive benefits the month after the third paid period has started', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(25, 'months') .add(2, 'days') .toDate()); cron({ @@ -604,7 +604,7 @@ describe('cron', () => { }); it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(37, 'months') .add(2, 'days') .toDate()); cron({ @@ -641,7 +641,7 @@ describe('cron', () => { user3g.purchased.plan.consecutive.gemCapExtra = 5; it('does not increment consecutive benefits in the first month of the gift subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -654,7 +654,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the second month of the gift subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); cron({ @@ -667,7 +667,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the third month of the gift subscription', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); cron({ @@ -680,7 +680,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the month after the gift subscription has ended', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months') .add(2, 'days') .toDate()); cron({ @@ -717,7 +717,7 @@ describe('cron', () => { user6x.purchased.plan.consecutive.gemCapExtra = 15; it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months') .add(2, 'days') .toDate()); cron({ @@ -730,7 +730,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the second month after the fix goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months') .add(2, 'days') .toDate()); cron({ @@ -743,7 +743,7 @@ describe('cron', () => { }); it('does not increment consecutive benefits in the third month after the fix goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months') .add(2, 'days') .toDate()); cron({ @@ -756,7 +756,7 @@ describe('cron', () => { }); it('increments consecutive benefits in the seventh month after the fix goes live', () => { - clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months') + clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months') .add(2, 'days') .toDate()); cron({ diff --git a/test/api/unit/libs/preening.test.js b/test/api/unit/libs/preening.test.js index c2448c5df9..d458595fe2 100644 --- a/test/api/unit/libs/preening.test.js +++ b/test/api/unit/libs/preening.test.js @@ -9,7 +9,7 @@ describe('preenHistory', () => { beforeEach(() => { // Replace system clocks so we can get predictable results clock = sinon.useFakeTimers({ - now: Number(moment('2013-10-20').zone(0).startOf('day').toDate()), + now: Number(moment('2013-10-20').utcOffset(0).startOf('day').toDate()), toFake: ['Date'], }); }); diff --git a/test/api/unit/models/group_tasks.test.js b/test/api/unit/models/group_tasks.test.js index 848b4b0792..1c60fa947b 100644 --- a/test/api/unit/models/group_tasks.test.js +++ b/test/api/unit/models/group_tasks.test.js @@ -235,15 +235,16 @@ describe('Group Task Methods', () => { }); }); - it('removes an assigned task and unlinks assignees', async () => { + it('removes assigned tasks when master task is deleted', async () => { await guild.syncTask(task, leader); await guild.removeTask(task); const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); + const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType }); const syncedTask = find(updatedLeadersTasks, findLinkedTask); - expect(syncedTask.group.broken).to.equal('TASK_DELETED'); + expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id); + expect(syncedTask).to.not.exist; }); it('unlinks and deletes group tasks for a user when remove-all is specified', async () => { diff --git a/test/api/unit/models/user.test.js b/test/api/unit/models/user.test.js index 50c5f8cc8a..f3345e5478 100644 --- a/test/api/unit/models/user.test.js +++ b/test/api/unit/models/user.test.js @@ -761,7 +761,7 @@ describe('User Model', () => { }); }); - context('days missed', () => { + describe('daysUserHasMissed', () => { // http://forbrains.co.uk/international_tools/earth_timezones let user; @@ -769,24 +769,51 @@ describe('User Model', () => { user = new User(); }); - it('should not cron early when going back a timezone', () => { - const yesterday = moment('2017-12-05T00:00:00.000-06:00'); // 11 pm on 4 Texas - const timezoneOffset = moment().zone('-06:00').zone(); - user.lastCron = yesterday; - user.preferences.timezoneOffset = timezoneOffset; + it('correctly calculates days missed since lastCron', () => { + const now = moment(); + user.lastCron = moment(now).subtract(5, 'days'); - const today = moment('2017-12-06T00:00:00.000-06:00'); // 11 pm on 4 Texas - const req = {}; - req.header = () => timezoneOffset + 60; + const { daysMissed } = user.daysUserHasMissed(now); - const { daysMissed } = user.daysUserHasMissed(today, req); + expect(daysMissed).to.eql(5); + }); + it('uses timezone from preferences to calculate days missed', () => { + const now = moment('2017-07-08 01:00:00Z'); + user.lastCron = moment('2017-07-04 13:00:00Z'); + user.preferences.timezoneOffset = 120; + + const { daysMissed } = user.daysUserHasMissed(now); + + expect(daysMissed).to.eql(3); + }); + + it('uses timezone at last cron to calculate days missed', () => { + const now = moment('2017-09-08 13:00:00Z'); + user.lastCron = moment('2017-09-06 01:00:00+02:00'); + user.preferences.timezoneOffset = 0; + user.preferences.timezoneOffsetAtLastCron = -120; + + const { daysMissed } = user.daysUserHasMissed(now); + + expect(daysMissed).to.eql(2); + }); + + it('respects new timezone that drags time into same day', () => { + user.lastCron = moment('2017-12-05T00:00:00.000-06:00'); + user.preferences.timezoneOffset = 360; + const today = moment('2017-12-06T00:00:00.000-06:00'); + const requestWithMinus7Timezone = { header: () => 420 }; + + const { daysMissed } = user.daysUserHasMissed(today, requestWithMinus7Timezone); + + expect(user.preferences.timezoneOffset).to.eql(420); expect(daysMissed).to.eql(0); }); it('should not cron early when going back a timezone with a custom day start', () => { const yesterday = moment('2017-12-05T02:00:00.000-08:00'); - const timezoneOffset = moment().zone('-08:00').zone(); + const timezoneOffset = 480; user.lastCron = yesterday; user.preferences.timezoneOffset = timezoneOffset; user.preferences.dayStart = 2; diff --git a/test/api/v3/integration/groups/POST-group_remove_manager.test.js b/test/api/v3/integration/groups/POST-group_remove_manager.test.js index e8cae4ab29..6c5d4cee9f 100644 --- a/test/api/v3/integration/groups/POST-group_remove_manager.test.js +++ b/test/api/v3/integration/groups/POST-group_remove_manager.test.js @@ -75,12 +75,7 @@ describe('POST /group/:groupId/remove-manager', () => { await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`); const memberTasks = await nonManager.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(nonManager.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await nonManager.post(`/tasks/${syncedTask._id}/score/up`); const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, { managerId: nonLeader._id, diff --git a/test/api/v3/integration/tasks/GET-tasks_user.test.js b/test/api/v3/integration/tasks/GET-tasks_user.test.js index e82a09e00b..85602ded26 100644 --- a/test/api/v3/integration/tasks/GET-tasks_user.test.js +++ b/test/api/v3/integration/tasks/GET-tasks_user.test.js @@ -153,12 +153,12 @@ describe('GET /tasks/user', () => { }); xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => { - const timezone = 420; + const timezoneOffset = 420; await user.update({ 'preferences.dayStart': 0, - 'preferences.timezoneOffset': timezone, + 'preferences.timezoneOffset': timezoneOffset, }); - const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day') + const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day') .toISOString(); await user.post('/tasks/user', [ { @@ -180,12 +180,12 @@ describe('GET /tasks/user', () => { }); xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => { - const timezone = 240; + const timezoneOffset = 240; await user.update({ 'preferences.dayStart': 0, - 'preferences.timezoneOffset': timezone, + 'preferences.timezoneOffset': timezoneOffset, }); - const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day') + const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day') .toISOString(); await user.post('/tasks/user', [ { @@ -207,12 +207,12 @@ describe('GET /tasks/user', () => { }); xit('returns dailies with isDue for the date specified and will add CDS offset if time is not supplied and assumes timezones', async () => { - const timezone = 540; + const timezoneOffset = 540; await user.update({ 'preferences.dayStart': 0, - 'preferences.timezoneOffset': timezone, + 'preferences.timezoneOffset': timezoneOffset, }); - const startDate = moment().zone(timezone).subtract('4', 'days').startOf('day') + const startDate = moment().utcOffset(-timezoneOffset).subtract('4', 'days').startOf('day') .toISOString(); await user.post('/tasks/user', [ { diff --git a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js index 59dba9f5ed..73828accdc 100644 --- a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js +++ b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js @@ -73,12 +73,7 @@ describe('Groups DELETE /tasks/:id', () => { }); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.sync(); await member2.sync(); @@ -96,16 +91,16 @@ describe('Groups DELETE /tasks/:id', () => { expect(member2.notifications.length).to.equal(1); }); - it('unlinks assigned user', async () => { + it('deletes task from assigned user', async () => { await user.del(`/tasks/${task._id}`); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - expect(syncedTask.group.broken).to.equal('TASK_DELETED'); + expect(syncedTask).to.not.exist; }); - it('unlinks all assigned users', async () => { + it('deletes task from all assigned users', async () => { await user.del(`/tasks/${task._id}`); const memberTasks = await member.get('/tasks/user'); @@ -114,8 +109,8 @@ describe('Groups DELETE /tasks/:id', () => { const member2Tasks = await member2.get('/tasks/user'); const member2SyncedTask = find(member2Tasks, findAssignedTask); - expect(syncedTask.group.broken).to.equal('TASK_DELETED'); - expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED'); + expect(syncedTask).to.not.exist; + expect(member2SyncedTask).to.not.exist; }); it('prevents a user from deleting a task they are assigned to', async () => { @@ -130,22 +125,6 @@ describe('Groups DELETE /tasks/:id', () => { }); }); - it('allows a user to delete a broken task', async () => { - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - await user.del(`/tasks/${task._id}`); - - await member.del(`/tasks/${syncedTask._id}`); - - await expect(member.get(`/tasks/${syncedTask._id}`)) - .to.eventually.be.rejected.and.eql({ - code: 404, - error: 'NotFound', - message: 'Task not found.', - }); - }); - it('allows a user to delete a task after leaving a group', async () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js index 599086e463..1d23e5f85c 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js @@ -58,22 +58,14 @@ describe('POST /tasks/:id/approve/:userId', () => { let memberTasks = await member.get('/tasks/user'); let syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${task._id}/approve/${member._id}`); await member.sync(); - expect(member.notifications.length).to.equal(3); + expect(member.notifications.length).to.equal(2); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); - expect(member.notifications[2].type).to.equal('SCORED_TASK'); - expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); memberTasks = await member.get('/tasks/user'); syncedTask = find(memberTasks, findAssignedTask); @@ -93,21 +85,13 @@ describe('POST /tasks/:id/approve/:userId', () => { let memberTasks = await member.get('/tasks/user'); let syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await member2.post(`/tasks/${task._id}/approve/${member._id}`); await member.sync(); - expect(member.notifications.length).to.equal(3); + expect(member.notifications.length).to.equal(2); expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED'); expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); - expect(member.notifications[2].type).to.equal('SCORED_TASK'); - expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); memberTasks = await member.get('/tasks/user'); syncedTask = find(memberTasks, findAssignedTask); @@ -125,12 +109,7 @@ describe('POST /tasks/:id/approve/:userId', () => { await member2.post(`/tasks/${task._id}/assign/${member._id}`); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.sync(); await member2.sync(); @@ -157,14 +136,9 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await member2.post(`/tasks/${task._id}/approve/${member._id}`); + await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) .to.eventually.be.rejected.and.to.eql({ code: 401, @@ -197,13 +171,7 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); @@ -226,13 +194,7 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); const member2Tasks = await member2.get('/tasks/user'); @@ -258,13 +220,7 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); const groupTasks = await user.get(`/tasks/group/${guild._id}`); @@ -287,21 +243,10 @@ describe('POST /tasks/:id/approve/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); const member2Tasks = await member2.get('/tasks/user'); const member2SyncedTask = find(member2Tasks, findAssignedTask); - await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member2.post(`/tasks/${member2SyncedTask._id}/score/up`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js index 91026baeae..8472bc03aa 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js @@ -61,13 +61,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => { let syncedTask = find(memberTasks, findAssignedTask); // score task to require approval - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${task._id}/needs-work/${member._id}`); [memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]); @@ -114,12 +108,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => { let syncedTask = find(memberTasks, findAssignedTask); // score task to require approval - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); const initialNotifications = member.notifications.length; @@ -172,13 +161,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); - + await member.post(`/tasks/${syncedTask._id}/score/up`); await member2.post(`/tasks/${task._id}/approve/${member._id}`); await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) .to.eventually.be.rejected.and.to.eql({ diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js index 80710c701b..810ad517d1 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js @@ -44,12 +44,11 @@ describe('POST /tasks/:id/score/:direction', () => { const syncedTask = find(memberTasks, findAssignedTask); const direction = 'up'; - await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`); + + expect(response.data.approvalRequested).to.equal(true); + expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); + const updatedTask = await member.get(`/tasks/${syncedTask._id}`); await user.sync(); @@ -76,12 +75,7 @@ describe('POST /tasks/:id/score/:direction', () => { const syncedTask = find(memberTasks, findAssignedTask); const direction = 'up'; - await expect(member.post(`/tasks/${syncedTask._id}/score/${direction}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/${direction}`); const updatedTask = await member.get(`/tasks/${syncedTask._id}`); await user.sync(); await member2.sync(); @@ -111,12 +105,7 @@ describe('POST /tasks/:id/score/:direction', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) .to.eventually.be.rejected.and.eql({ @@ -130,12 +119,7 @@ describe('POST /tasks/:id/score/:direction', () => { const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + await member.post(`/tasks/${syncedTask._id}/score/up`); await user.post(`/tasks/${task._id}/approve/${member._id}`); diff --git a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js index 45443d4f06..74c76475a2 100644 --- a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js +++ b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js @@ -71,12 +71,10 @@ describe('PUT /tasks/:id', () => { const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id); // score up to trigger approval - await expect(member2.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalHasBeenRequested'), - }); + const response = await member2.post(`/tasks/${syncedTask._id}/score/up`); + + expect(response.data.approvalRequested).to.equal(true); + expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); }); it('member updates a group task value - not allowed', async () => { diff --git a/test/common/fns/getUtcOffset.test.js b/test/common/fns/getUtcOffset.test.js new file mode 100644 index 0000000000..a91c886407 --- /dev/null +++ b/test/common/fns/getUtcOffset.test.js @@ -0,0 +1,25 @@ +import getUtcOffset from '../../../website/common/script/fns/getUtcOffset'; + +describe('getUtcOffset', () => { + let user; + + beforeEach(() => { + user = { preferences: {} }; + }); + + it('returns 0 when user.timezoneOffset is not set', () => { + expect(getUtcOffset(user)).to.equal(0); + }); + + it('returns 0 when user.timezoneOffset is zero', () => { + user.preferences.timezoneOffset = 0; + + expect(getUtcOffset(user)).to.equal(0); + }); + + it('returns the opposite of user.timezoneOffset', () => { + user.preferences.timezoneOffset = -10; + + expect(getUtcOffset(user)).to.eql(10); + }); +}); diff --git a/test/common/libs/cron.test.js b/test/common/libs/cron.test.js new file mode 100644 index 0000000000..0894a6afa3 --- /dev/null +++ b/test/common/libs/cron.test.js @@ -0,0 +1,184 @@ +import moment from 'moment'; + +import { startOfDay, daysSince } from '../../../website/common/script/cron'; + +function localMoment (timeString, utcOffset) { + return moment(timeString).utcOffset(utcOffset, true); +} + +describe('cron utility functions', () => { + describe('startOfDay', () => { + it('is zero when no daystart configured', () => { + const options = { now: moment('2020-02-02 09:30:00Z'), timezoneOffset: 0 }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00Z'); + }); + + it('is zero when negative daystart configured', () => { + const options = { + now: moment('2020-02-02 09:30:00Z'), + timezoneOffset: 0, + daystart: -5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00Z'); + }); + + it('is zero when daystart over 24 is configured', () => { + const options = { + now: moment('2020-02-02 09:30:00Z'), + timezoneOffset: 0, + daystart: 25, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00Z'); + }); + + it('is equal to daystart o\'clock when daystart configured', () => { + const options = { + now: moment('2020-02-02 09:30:00Z'), + timezoneOffset: 0, + dayStart: 5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 05:00:00Z'); + }); + + it('is previous day daystart o\'clock when daystart is after current time', () => { + const options = { + now: moment('2020-02-02 04:30:00Z'), + timezoneOffset: 0, + dayStart: 5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-01 05:00:00Z'); + }); + + it('is daystart o\'clock when daystart is after current time due to timezone', () => { + const options = { + now: moment('2020-02-02 04:30:00Z'), + timezoneOffset: -120, + dayStart: 5, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 05:00:00+02:00'); + }); + + it('returns in default timezone if no timezone defined', () => { + const utcOffset = moment().utcOffset(); + const now = localMoment('2020-02-02 04:30:00', utcOffset).utc(); + + const result = startOfDay({ now }); + + expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset)); + }); + + it('returns in default timezone if timezone lower than -12:00', () => { + const utcOffset = moment().utcOffset(); + const options = { + now: localMoment('2020-02-02 17:30:00', utcOffset).utc(), + timezoneOffset: 721, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset)); + }); + + it('returns in default timezone if timezone higher than +14:00', () => { + const utcOffset = moment().utcOffset(); + const options = { + now: localMoment('2020-02-02 07:32:25.376', utcOffset).utc(), + timezoneOffset: -841, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment(localMoment('2020-02-02', utcOffset)); + }); + + it('returns in overridden timezone if override present', () => { + const options = { + now: moment('2020-02-02 13:30:27Z'), + timezoneOffset: 0, + timezoneUtcOffsetOverride: -240, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment('2020-02-02 00:00:00-04:00'); + }); + + it('returns start of yesterday if timezone difference carries it over datelines', () => { + const offset = 300; + const options = { + now: moment('2020-02-02 04:30:00Z'), + timezoneOffset: offset, + }; + + const result = startOfDay(options); + + expect(result).to.be.sameMoment(localMoment('2020-02-01', -offset)); + }); + }); + + describe('daysSince', () => { + it('correctly calculates days between two dates', () => { + const now = moment(); + const dayBeforeYesterday = moment(now).subtract({ days: 2 }); + + expect(daysSince(dayBeforeYesterday, { now })).to.equal(2); + }); + + it('is one lower if current time is before dayStart', () => { + const oneWeekAgoAtOnePm = moment().hour(13).subtract({ days: 7 }); + const thisMorningThreeAm = moment().hour(3); + const options = { + now: thisMorningThreeAm, + dayStart: 6, + }; + + const result = daysSince(oneWeekAgoAtOnePm, options); + + expect(result).to.equal(6); + }); + + it('is one higher if reference time is before dayStart and current time after dayStart', () => { + const oneWeekAgoAtEightAm = moment().hour(8).subtract({ days: 7 }); + const todayAtFivePm = moment().hour(17); + const options = { + now: todayAtFivePm, + dayStart: 11, + }; + + const result = daysSince(oneWeekAgoAtEightAm, options); + + expect(result).to.equal(8); + }); + + // Variations in timezone configuration options are already covered by startOfDay tests. + it('uses now in user timezone as configured in options', () => { + const timezoneOffset = 120; + const options = { + now: moment('1989-11-09 02:53:00+01:00'), + timezoneOffset, + }; + + const result = daysSince(localMoment('1989-11-08', -timezoneOffset), options); + + expect(result).to.equal(0); + }); + }); +}); diff --git a/test/common/libs/taskDefaults.test.js b/test/common/libs/taskDefaults.test.js index e69bafa45d..479a87fa30 100644 --- a/test/common/libs/taskDefaults.test.js +++ b/test/common/libs/taskDefaults.test.js @@ -1,6 +1,7 @@ import moment from 'moment'; import taskDefaults from '../../../website/common/script/libs/taskDefaults'; +import getUtcOffset from '../../../website/common/script/fns/getUtcOffset'; import { generateUser } from '../../helpers/common.helper'; describe('taskDefaults', () => { @@ -72,7 +73,7 @@ describe('taskDefaults', () => { expect(task.startDate).to.eql( moment() - .zone(user.preferences.timezoneOffset, 'hour') + .utcOffset(getUtcOffset(user)) .startOf('day') .subtract(1, 'day') .toDate(), diff --git a/test/common/shouldDo.test.js b/test/common/shouldDo.test.js index b8f9a604ad..d7250f871a 100644 --- a/test/common/shouldDo.test.js +++ b/test/common/shouldDo.test.js @@ -5,6 +5,8 @@ import 'moment-recur'; describe('shouldDo', () => { let day; let dailyTask; + // Options is a mapping of user.preferences, therefor `timezoneOffset` still holds old zone + // values instead of utcOffset values. let options = {}; let nextDue = []; @@ -80,17 +82,17 @@ describe('shouldDo', () => { it('returns true if the user\'s current time is after start date and Custom Day Start', () => { options.dayStart = 4; - day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours') .toDate(); - dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate(); + dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; - day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours') .toDate(); - dailyTask.startDate = moment().zone(options.timezoneOffset).startOf('day').toDate(); + dailyTask.startDate = moment().utcOffset(-options.timezoneOffset).startOf('day').toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); }); @@ -112,14 +114,14 @@ describe('shouldDo', () => { it('returns true if the user\'s current time is after Custom Day Start', () => { options.dayStart = 4; - day = moment().zone(options.timezoneOffset).startOf('day').add(6, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(6, 'hours') .toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(true); }); it('returns false if the user\'s current time is before Custom Day Start', () => { options.dayStart = 8; - day = moment().zone(options.timezoneOffset).startOf('day').add(2, 'hours') + day = moment().utcOffset(-options.timezoneOffset).startOf('day').add(2, 'hours') .toDate(); expect(shouldDo(day, dailyTask, options)).to.equal(false); }); diff --git a/test/helpers/globals.helper.js b/test/helpers/globals.helper.js index bca96eac2c..213efac698 100644 --- a/test/helpers/globals.helper.js +++ b/test/helpers/globals.helper.js @@ -8,8 +8,9 @@ //------------------------------ global._ = require('lodash'); global.chai = require('chai'); -chai.use(require('sinon-chai')); chai.use(require('chai-as-promised')); +chai.use(require('chai-moment')); +chai.use(require('sinon-chai')); global.expect = chai.expect; global.sinon = require('sinon'); diff --git a/website/client/config/storybook/config.js b/website/client/config/storybook/config.js index 01bba95033..fb3957a25e 100644 --- a/website/client/config/storybook/config.js +++ b/website/client/config/storybook/config.js @@ -1,6 +1,8 @@ /* eslint-disable import/no-extraneous-dependencies */ import { configure } from '@storybook/vue'; +import './margin.css'; import '../../src/assets/scss/index.scss'; +import '../../src/assets/scss/spacing.scss'; import '../../src/assets/css/sprites.css'; import '../../src/assets/css/sprites/spritesmith-main-0.css'; diff --git a/website/client/config/storybook/margin.css b/website/client/config/storybook/margin.css new file mode 100644 index 0000000000..eaad293c27 --- /dev/null +++ b/website/client/config/storybook/margin.css @@ -0,0 +1,13 @@ +.background { + background: teal; + display: inline-block; +} + +.content { + color: white; + background: grey; +} + +.inline-block { + display: inline-block; +} diff --git a/website/client/package-lock.json b/website/client/package-lock.json index 661c0bb9f4..9da6df7550 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -6336,6 +6336,13 @@ "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "assert": { @@ -7074,9 +7081,9 @@ "integrity": "sha512-j4nzWIqEFpLSbdhUApHRGDwfXbV8ALhqOn+FY5L6XBdKPAXU9BpGgFSbDsgqogfqPPR9R2WooseWCsfhfEC6uQ==" }, "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz", + "integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==" }, "body-parser": { "version": "1.19.0", @@ -7355,20 +7362,46 @@ "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz", + "integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==", "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.2", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "browserify-zlib": { @@ -8490,6 +8523,13 @@ "requires": { "bn.js": "^4.1.0", "elliptic": "^6.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "create-hash": { @@ -9110,6 +9150,13 @@ "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "dir-glob": { @@ -9371,9 +9418,9 @@ } }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -9382,6 +9429,13 @@ "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "emoji-regex": { @@ -9426,9 +9480,9 @@ } }, "enhanced-resolve": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", - "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", "requires": { "graceful-fs": "^4.1.2", "memory-fs": "^0.5.0", @@ -10021,9 +10075,9 @@ "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" }, "events": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", - "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==" }, "eventsource": { "version": "1.0.7", @@ -11647,12 +11701,30 @@ } }, "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "hash-sum": { @@ -13556,6 +13628,13 @@ "requires": { "bn.js": "^4.0.0", "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "mime": { @@ -14998,9 +15077,9 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -16060,6 +16139,13 @@ "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "pump": { @@ -19829,13 +19915,123 @@ } }, "watchpack": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", - "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", + "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "requires": { - "chokidar": "^2.1.8", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" + }, + "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "optional": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz", + "integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==", + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "optional": true + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "optional": true + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "watchpack-chokidar2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", + "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "optional": true, + "requires": { + "chokidar": "^2.1.8" } }, "wbuf": { @@ -19860,9 +20056,9 @@ "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" }, "webpack": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz", - "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.0.tgz", + "integrity": "sha512-wAuJxK123sqAw31SpkPiPW3iKHgFUiKvO7E7UZjtdExcsRe3fgav4mvoMM7vvpjLHVoJ6a0Mtp2fzkoA13e0Zw==", "requires": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -19872,7 +20068,7 @@ "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", + "enhanced-resolve": "^4.3.0", "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", "loader-runner": "^2.4.0", @@ -19885,7 +20081,7 @@ "schema-utils": "^1.0.0", "tapable": "^1.1.3", "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.6.1", + "watchpack": "^1.7.4", "webpack-sources": "^1.4.1" }, "dependencies": { @@ -19902,21 +20098,29 @@ "minimist": "^1.2.5" } }, + "serialize-javascript": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "requires": { + "randombytes": "^2.1.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz", + "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==", "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", + "serialize-javascript": "^3.1.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", diff --git a/website/client/package.json b/website/client/package.json index 3615b2ef54..efcb07f72f 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -60,6 +60,6 @@ "vue-template-compiler": "^2.6.11", "vuedraggable": "^2.24.0", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", - "webpack": "^4.43.0" + "webpack": "^4.44.0" } } diff --git a/website/client/src/app.vue b/website/client/src/app.vue index 08c79f349a..21d00e8c93 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -297,7 +297,7 @@ export default { }; }, computed: { - ...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded', 'notificationsRemoved']), + ...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']), ...mapState({ user: 'user.data' }), isStaticPage () { return this.$route.meta.requiresLogin === false; @@ -493,9 +493,10 @@ export default { this.hideLoadingScreen(); // Adjust the timezone offset - if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) { + const browserTimezoneOffset = -this.browserTimezoneUtcOffset; + if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) { this.$store.dispatch('user:set', { - 'preferences.timezoneOffset': this.browserTimezoneOffset, + 'preferences.timezoneOffset': browserTimezoneOffset, }); } diff --git a/website/client/src/assets/scss/button.scss b/website/client/src/assets/scss/button.scss index 482e731700..a19e96acf4 100644 --- a/website/client/src/assets/scss/button.scss +++ b/website/client/src/assets/scss/button.scss @@ -52,9 +52,11 @@ border-color: $purple-400; } - &:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus { - box-shadow: none; - border-color: $purple-400; + &:not(:disabled):not(.disabled) { + &:active:focus, &.active:focus { + box-shadow: none; + border-color: $purple-400; + } } &:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active { diff --git a/website/client/src/assets/scss/dropdown.scss b/website/client/src/assets/scss/dropdown.scss index 0acbce7864..4a078edd25 100644 --- a/website/client/src/assets/scss/dropdown.scss +++ b/website/client/src/assets/scss/dropdown.scss @@ -1,20 +1,27 @@ .dropdown > .btn { - padding: 9px 15.5px; + padding: 0.25rem 0.75rem; font-family: 'Roboto', sans-serif; font-size: 14px; font-weight: normal; line-height: 1.43; } +.dropdown-toggle:hover { + --caret-color: #{$purple-200}; +} + .dropdown.show > .dropdown-toggle:not(.btn-success) { color: $purple-200; - border-color: $purple-500 !important; + border-color: $purple-400 !important; box-shadow: none; + + &::after { + --caret-color: #{$purple-200}; + } } .dropdown-toggle::after { - margin-left: 16px; - border-top: 6px solid; + border-top-color: var(--caret-color); border-right: 5px solid transparent; border-left: 5px solid transparent; vertical-align: 0; @@ -23,14 +30,18 @@ .dropdown-menu { padding: 0px; border: none; - border-radius: 4px; - box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($white, 0.1); + border-radius: 2px; + box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24); + } +// shared dropdown-item styles .dropdown-item { + // header items & not selectList-items padding-left: 24px; padding-top: 8px; padding-bottom: 8px; + font-size: 14px; line-height: 1.71; color: $gray-50; @@ -42,8 +53,8 @@ } - &:active, &:hover, &.active { - background-color: rgba(#d5c8ff, 0.32); + &:active, &:hover, &:focus, &.active { + background-color: rgba($purple-600, 0.32); color: $purple-200; } @@ -86,16 +97,28 @@ .dropdown-toggle { width: 100% !important; + height: 32px; text-align: left; } .dropdown-toggle::after { position: absolute; - right: 16px; - top: 17px; + right: 12px; + top: 14px; } .dropdown-menu.show { width: 100% !important; } } + +// selectList.vue items sizing +.selectListItem .dropdown-item { + padding: 0.25rem 0.75rem; + height: 32px; + + &:active, &:hover, &:focus, &.active { + background-color: rgba($purple-600, 0.25); + color: $purple-300; + } +} diff --git a/website/client/src/assets/scss/form.scss b/website/client/src/assets/scss/form.scss index edfb99245e..f4c2dc3ba1 100644 --- a/website/client/src/assets/scss/form.scss +++ b/website/client/src/assets/scss/form.scss @@ -16,10 +16,10 @@ label small { } } -// Inputs and texteares +// Inputs and textareas input, textarea, input.form-control, textarea.form-control { - padding: 10px 16px; + padding: 10px 12px; border-radius: 2px; font-size: 14px; line-height: 1.43; @@ -31,14 +31,14 @@ input, textarea, input.form-control, textarea.form-control { } &:active:not(:disabled), &:focus:not(:disabled) { - border-color: $purple-500; + border-color: $purple-400; outline: 0; box-shadow: none; } &:disabled { opacity: 0.64; - background: $gray-500; + background: $gray-700; } &.input-search { @@ -68,11 +68,48 @@ input, textarea, input.form-control, textarea.form-control { } } -.input-group { - .input-group-prepend , .input-group-append { +.input-group-outer { + display: flex; + flex-direction: row; + + .input-group { + flex: 1; + } +} + +/** Colored Input-Groups, ignoring checklist */ +.input-group:not(.checklist-group) { + border-radius: 2px; + border: solid 1px $gray-400; + + &:hover { + border-color: $gray-300; + } + + &:focus, &:active, &:focus-within { + border: solid 1px $purple-400; + } + + .input-group-prepend , .input-group-append { background: $gray-600; - color: $gray-300; - border-radius: 2px; + } +} + +/** Generic Input Group Styles */ +.input-group { + height: 2rem; + + .input-group-prepend , .input-group-append { + color: $gray-200; + border: 0; + height: 30px; + width: 2rem; + margin: 0; + + &.grow { + width: initial; + min-width: 2rem; + } &.input-group-text { font-size: 14px; @@ -83,28 +120,30 @@ input, textarea, input.form-control, textarea.form-control { } &.input-group-icon { - border: solid 1px $gray-400; - border-right: none; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; + display: flex; + align-self: center; + align-items: center; + justify-items: center; + justify-content: center; + } + + .svg-icon { + margin: 0 !important; } &.streak-addon .svg-icon { width: 11.6px; height: 7.1px; - margin: 15px 13.4px 15.9px 13px; } &.positive-addon .svg-icon { width: 10px; height: 10px; - margin: 14px 14px; } &.negative-addon .svg-icon { width: 10px; height: 2px; - margin: 18px 14px; } } @@ -115,6 +154,19 @@ input, textarea, input.form-control, textarea.form-control { input:first-child { border-right: none !important; } + + input { + height: 30px; + border: 0; + background: $white !important; + } +} + +.input-group-spaced { + margin-left: 12px; + height: 2rem; + border-radius: 2px; + background-color: $gray-600; } .form-check { @@ -200,9 +252,13 @@ $bg-disabled-control: #34303a; align-items: center; } - .destroy-icon { - width: 14px; - height: 16px; + .destroy-icon.svg-icon { + margin-top: 1px !important; + + svg { + width: 14px; + height: 16px; + } } } diff --git a/website/client/src/assets/scss/icon.scss b/website/client/src/assets/scss/icon.scss index 5e5a9162b9..3d570b9ac0 100644 --- a/website/client/src/assets/scss/icon.scss +++ b/website/client/src/assets/scss/icon.scss @@ -1,14 +1,14 @@ .svg-icon { display: block; - transition: none !important; fill: currentColor; svg { display: block; - } + transition: none; - * { - transition: none !important; + path { + transition: none; + } } &.color { @@ -64,4 +64,4 @@ &:hover svg path { stroke: $gray-100; } -} \ No newline at end of file +} diff --git a/website/client/src/assets/scss/index.scss b/website/client/src/assets/scss/index.scss index 6842a80947..63dfcbc85f 100644 --- a/website/client/src/assets/scss/index.scss +++ b/website/client/src/assets/scss/index.scss @@ -37,3 +37,4 @@ @import './tiers'; @import './payments'; @import './datepicker.scss'; +@import './spacing'; diff --git a/website/client/src/assets/scss/misc.scss b/website/client/src/assets/scss/misc.scss index 164a05f0e7..410ddcd13a 100644 --- a/website/client/src/assets/scss/misc.scss +++ b/website/client/src/assets/scss/misc.scss @@ -14,4 +14,16 @@ border-right: 4px solid transparent; border-left: 4px solid transparent; border-bottom: 0; -} \ No newline at end of file +} + +* { + transition: none; +} + +.transition { + transition-duration: 0.15s; + transition-property: border-color, color; + transition-property: border-color, box-shadow, color; + transition-property: border-color, box-shadow, color; + transition-timing-function: ease-in; +} diff --git a/website/client/src/assets/scss/modal.scss b/website/client/src/assets/scss/modal.scss index efaf47bb0c..ae4ce79d9d 100644 --- a/website/client/src/assets/scss/modal.scss +++ b/website/client/src/assets/scss/modal.scss @@ -13,6 +13,7 @@ .modal { z-index: 1350; + padding-left: 0px !important; } .modal-dialog { diff --git a/website/client/src/assets/scss/spacing.scss b/website/client/src/assets/scss/spacing.scss new file mode 100644 index 0000000000..325c36003b --- /dev/null +++ b/website/client/src/assets/scss/spacing.scss @@ -0,0 +1,59 @@ +.m-75 { + margin: 0.75rem; // 12px +} + +.mx-75 { + margin-left: 0.75rem; + margin-right: 0.75rem; +} + +.my-75 { + margin-bottom: 0.75rem; + margin-top: 0.75rem; +} + +.mb-75 { + margin-bottom: 0.75rem; +} + +.ml-75 { + margin-left: 0.75rem; +} + +.mr-75 { + margin-right: 0.75rem; +} + +.mt-75 { + margin-top: 0.75rem; +} + +.p-75 { + padding: 0.75rem; +} + +.px-75 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.py-75 { + padding-bottom: 0.75rem; + padding-top: 0.75rem; +} + +.pb-75 { + padding-bottom: 0.75rem; +} + +.pl-75 { + padding-left: 0.75rem; +} + +.pr-75 { + padding-right: 0.75rem; +} + +.pt-75 { + padding-top: 0.75rem; +} diff --git a/website/client/src/assets/scss/task.scss b/website/client/src/assets/scss/task.scss index d651a574e7..66f6f35bf7 100644 --- a/website/client/src/assets/scss/task.scss +++ b/website/client/src/assets/scss/task.scss @@ -3,7 +3,8 @@ .habit-option-button { border: 2px solid $disabled-color; } - &:hover { + // TODO refactor to use more css-vars and less duplicate generated css code + &:hover, &:focus, &:active { .habit-option-button { border: 2px solid $active-color; } @@ -28,7 +29,7 @@ &-control { &-bg { background: $maroon-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $maroon-100 !important; } @@ -40,6 +41,7 @@ &-modal { &-bg { background: $maroon-100 !important; } + &-headings { color: $white; } &-icon { color: $maroon-100 !important; } &-text { color: $red-1 !important; } &-content { @@ -58,7 +60,7 @@ &-control { &-bg { background: $red-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $red-100 !important; } @@ -70,8 +72,8 @@ &-modal { &-bg { background: $red-100 !important; } + &-headings, &-text { color: $red-1 !important; } &-icon { color: $red-100 !important; } - &-text { color: $red-1 !important; } &-content { --svg-color: #{$red-100}; } @@ -89,7 +91,7 @@ &-control { &-bg { background: $orange-100 !important; - .habit-control:hover { background: rgba($orange-1, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $orange-100 !important; } @@ -101,8 +103,8 @@ &-modal { &-bg { background: $orange-100 !important; } + &-headings, &-text { color: $orange-1 !important; } &-icon { color: $orange-100 !important; } - &-text { color: $orange-1 !important; } &-content { --svg-color: #{$orange-100}; } @@ -120,7 +122,7 @@ &-control { &-bg { background: $yellow-100 !important; - .habit-control:hover { background: rgba($yellow-1, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $yellow-100 !important; } @@ -132,8 +134,8 @@ &-modal { &-bg { background: $yellow-100 !important; } + &-headings, &-text { color: $yellow-1 !important; } &-icon { color: $yellow-100 !important; } - &-text { color: $yellow-1 !important; } @include modal-text-input($yellow-1); &-option-disabled:hover { .svg-icon { color: $yellow-100 !important; } @@ -151,7 +153,7 @@ &-control { &-bg { background: $green-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $green-100 !important; } @@ -163,8 +165,8 @@ &-modal { &-bg { background: $green-100 !important; } + &-headings, &-text { color: $green-1 !important; } &-icon { color: $green-10 !important; } - &-text { color: $green-1 !important; } &-content { --svg-color: #{$green-100}; } @@ -183,7 +185,7 @@ &-control { &-bg { background: $teal-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $teal-100 !important; } @@ -195,8 +197,8 @@ &-modal { &-bg { background: $teal-100 !important; } + &-headings, &-text { color: $teal-1 !important; } &-icon { color: $teal-100 !important; } - &-text { color: $teal-1 !important; } &-content { --svg-color: #{$teal-100}; } @@ -214,7 +216,7 @@ &-control { &-bg { background: $blue-100 !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-bg-noninteractive { background: $blue-100 !important; } @@ -226,8 +228,8 @@ &-modal { &-bg { background: $blue-100 !important; } + &-headings, &-text { color: $blue-1 !important; } &-icon { color: $blue-100 !important; } - &-text { color: $blue-1 !important; } &-content { --svg-color: #{$blue-100}; } @@ -246,7 +248,7 @@ &-control { &-bg { background: $purple-task !important; - .habit-control:hover { background: rgba($black, 0.5) !important; } + .habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; } .daily-todo-control:hover { background: rgba($white, 0.75) !important; } } &-inner-habit { background: rgba($black, 0.25) !important; } @@ -256,6 +258,7 @@ &-modal { &-bg { background: $purple-300 !important; } + &-headings { color: $white; } &-icon { color: $purple-300 !important; } &-text { color: $black !important; } &-content { diff --git a/website/client/src/assets/scss/typography.scss b/website/client/src/assets/scss/typography.scss index a9caa6b285..267327b09b 100644 --- a/website/client/src/assets/scss/typography.scss +++ b/website/client/src/assets/scss/typography.scss @@ -53,7 +53,7 @@ h1 { h2 { font-size: 20px; - line-height: 1.2; + line-height: 1.4; margin-bottom: 16px; } @@ -73,3 +73,7 @@ h4 { font-family: 'Roboto Condensed', sans-serif; font-weight: bold; } + +.opacity-75 { + opacity: 0.75; +} diff --git a/website/client/src/assets/svg/information.svg b/website/client/src/assets/svg/information.svg index cb4a531a1e..c3d91e08ee 100644 --- a/website/client/src/assets/svg/information.svg +++ b/website/client/src/assets/svg/information.svg @@ -1,3 +1,3 @@ diff --git a/website/client/src/components/achievements/newStuff.vue b/website/client/src/components/achievements/newStuff.vue index e59592ee2a..53c427a383 100644 --- a/website/client/src/components/achievements/newStuff.vue +++ b/website/client/src/components/achievements/newStuff.vue @@ -13,20 +13,20 @@ v-html="html" > -