From 085ecc7f5ffd2d67abd4bbeac9bf1f5d55e1f9c4 Mon Sep 17 00:00:00 2001
From: Kerry
Date: Mon, 31 Jan 2022 12:54:14 +0100
Subject: [PATCH] Chat export parameter customisation (#7647)
* use export settings and hide fields
Signed-off-by: Kerry Archibald
* fix exporter tests
Signed-off-by: Kerry Archibald
* test ExportDialog with settings
Signed-off-by: Kerry Archibald
* tidy debugs, rename setting to Parameters
Signed-off-by: Kerry Archibald
* use reasonable 100gb limit
Signed-off-by: Kerry Archibald
* use normal setting instead of UIFeature
Signed-off-by: Kerry Archibald
* use a customisation
Signed-off-by: Kerry Archibald
* move validateNumberInRange to utils
Signed-off-by: Kerry Archibald
* use nullish coalesce
Signed-off-by: Kerry Archibald
* use 8gb size limit for customisation
Signed-off-by: Kerry Archibald
* update comments
Signed-off-by: Kerry Archibald
---
package.json | 1 +
res/css/views/dialogs/_ExportDialog.scss | 4 +
src/components/views/dialogs/ExportDialog.tsx | 195 ++++++++++++------
src/customisations/ChatExport.ts | 52 +++++
src/settings/UIFeature.ts | 2 +-
src/utils/exportUtils/Exporter.ts | 2 +-
src/utils/validate/index.ts | 1 +
src/utils/validate/numberInRange.ts | 9 +
.../views/dialogs/ExportDialog-test.tsx | 119 +++++++++--
.../__snapshots__/ExportDialog-test.tsx.snap | 128 ++++++------
test/utils/export-test.tsx | 47 ++---
test/utils/validate/numberInRange-test.ts | 26 +++
yarn.lock | 104 ++++++++--
13 files changed, 501 insertions(+), 189 deletions(-)
create mode 100644 src/customisations/ChatExport.ts
create mode 100644 src/utils/validate/index.ts
create mode 100644 src/utils/validate/numberInRange.ts
create mode 100644 test/utils/validate/numberInRange-test.ts
diff --git a/package.json b/package.json
index 1ac0c7b28d..f1e81966d1 100644
--- a/package.json
+++ b/package.json
@@ -191,6 +191,7 @@
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
+ "ts-jest": "^27.1.3",
"typescript": "4.5.3",
"walk": "^2.3.14"
},
diff --git a/res/css/views/dialogs/_ExportDialog.scss b/res/css/views/dialogs/_ExportDialog.scss
index 294daba2e8..71f4b32a03 100644
--- a/res/css/views/dialogs/_ExportDialog.scss
+++ b/res/css/views/dialogs/_ExportDialog.scss
@@ -89,3 +89,7 @@ limitations under the License.
padding: 9px 10px;
}
}
+
+.mx_ExportDialog_attachments-checkbox {
+ margin-top: $spacing-16;
+}
diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx
index cfbd6af1bf..918bc733c1 100644
--- a/src/components/views/dialogs/ExportDialog.tsx
+++ b/src/components/views/dialogs/ExportDialog.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useRef, useState } from "react";
+import React, { useRef, useState, Dispatch, SetStateAction } from "react";
import { Room } from "matrix-js-sdk/src";
import { logger } from "matrix-js-sdk/src/logger";
@@ -39,18 +39,70 @@ import { useStateCallback } from "../../../hooks/useStateCallback";
import Exporter from "../../../utils/exportUtils/Exporter";
import Spinner from "../elements/Spinner";
import InfoDialog from "./InfoDialog";
+import ChatExport from "../../../customisations/ChatExport";
+import { validateNumberInRange } from "../../../utils/validate";
interface IProps extends IDialogProps {
room: Room;
}
+interface ExportConfig {
+ exportFormat: ExportFormat;
+ exportType: ExportType;
+ numberOfMessages: number;
+ sizeLimit: number;
+ includeAttachments: boolean;
+ setExportFormat?: Dispatch>;
+ setExportType?: Dispatch>;
+ setAttachments?: Dispatch>;
+ setNumberOfMessages?: Dispatch>;
+ setSizeLimit?: Dispatch>;
+}
+
+/**
+ * Set up form state using "forceRoomExportParameters" or defaults
+ * Form fields configured in ForceRoomExportParameters are not allowed to be edited
+ * Only return change handlers for editable values
+ */
+const useExportFormState = (): ExportConfig => {
+ const config = ChatExport.getForceChatExportParameters();
+
+ const [exportFormat, setExportFormat] = useState(config.format ?? ExportFormat.Html);
+ const [exportType, setExportType] = useState(config.range ?? ExportType.Timeline);
+ const [includeAttachments, setAttachments] =
+ useState(config.includeAttachments ?? false);
+ const [numberOfMessages, setNumberOfMessages] = useState(config.numberOfMessages ?? 100);
+ const [sizeLimit, setSizeLimit] = useState(config.sizeMb ?? 8);
+
+ return {
+ exportFormat,
+ exportType,
+ includeAttachments,
+ numberOfMessages,
+ sizeLimit,
+ setExportFormat: !config.format ? setExportFormat : undefined,
+ setExportType: !config.range ? setExportType : undefined,
+ setNumberOfMessages: !config.numberOfMessages ? setNumberOfMessages : undefined,
+ setSizeLimit: !config.sizeMb ? setSizeLimit : undefined,
+ setAttachments: config.includeAttachments === undefined ? setAttachments : undefined,
+ };
+};
+
const ExportDialog: React.FC = ({ room, onFinished }) => {
- const [exportFormat, setExportFormat] = useState(ExportFormat.Html);
- const [exportType, setExportType] = useState(ExportType.Timeline);
- const [includeAttachments, setAttachments] = useState(false);
+ const {
+ exportFormat,
+ exportType,
+ includeAttachments,
+ numberOfMessages,
+ sizeLimit,
+ setExportFormat,
+ setExportType,
+ setNumberOfMessages,
+ setSizeLimit,
+ setAttachments,
+ } = useExportFormState();
+
const [isExporting, setExporting] = useState(false);
- const [numberOfMessages, setNumberOfMessages] = useState(100);
- const [sizeLimit, setSizeLimit] = useState(8);
const sizeLimitRef = useRef();
const messageCountRef = useRef();
const [exportProgressText, setExportProgressText] = useState(_t("Processing..."));
@@ -110,9 +162,10 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
};
const onExportClick = async () => {
- const isValidSize = await sizeLimitRef.current.validate({
+ const isValidSize = !setSizeLimit || (await sizeLimitRef.current.validate({
focused: false,
- });
+ }));
+
if (!isValidSize) {
sizeLimitRef.current.validate({ focused: true });
return;
@@ -147,10 +200,8 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
}, {
key: "number",
test: ({ value }) => {
- const parsedSize = parseFloat(value);
- const min = 1;
- const max = 2000;
- return !(isNaN(parsedSize) || min > parsedSize || parsedSize > max);
+ const parsedSize = parseInt(value as string, 10);
+ return validateNumberInRange(1, 2000)(parsedSize);
},
invalid: () => {
const min = 1;
@@ -187,11 +238,8 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
}, {
key: "number",
test: ({ value }) => {
- const parsedSize = parseFloat(value);
- const min = 1;
- const max = 10 ** 8;
- if (isNaN(parsedSize)) return false;
- return !(min > parsedSize || parsedSize > max);
+ const parsedSize = parseInt(value as string, 10);
+ return validateNumberInRange(1, 10 ** 8)(parsedSize);
},
invalid: () => {
const min = 1;
@@ -236,7 +284,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
});
let messageCount = null;
- if (exportType === ExportType.LastNMessages) {
+ if (exportType === ExportType.LastNMessages && setNumberOfMessages) {
messageCount = (
= ({ room, onFinished }) => {
) }
: null }
-
- { _t("Format") }
-
-
- setExportFormat(ExportFormat[key])}
- definitions={exportFormatOptions}
- />
+ { !!setExportFormat && <>
+
+ { _t("Format") }
+
-
- { _t("Messages") }
-
+ setExportFormat(ExportFormat[key])}
+ definitions={exportFormatOptions}
+ />
+ > }
- {
- setExportType(ExportType[e.target.value]);
- }}
- >
- { exportTypeOptions }
-
- { messageCount }
+ {
+ !!setExportType && <>
-
- { _t("Size Limit") }
-
+
+ { _t("Messages") }
+
- setSizeLimit(parseInt(e.target.value))}
- />
+ {
+ setExportType(ExportType[e.target.value]);
+ }}
+ >
+ { exportTypeOptions }
+
+ { messageCount }
+ >
+ }
+
+ { setSizeLimit && <>
+
+ { _t("Size Limit") }
+
+
+ setSizeLimit(parseInt(e.target.value))}
+ />
+ > }
+
+ { setAttachments && <>
+
+ setAttachments(
+ (e.target as HTMLInputElement).checked,
+ )
+ }
+ >
+ { _t("Include Attachments") }
+
+ > }
-
- setAttachments(
- (e.target as HTMLInputElement).checked,
- )
- }
- >
- { _t("Include Attachments") }
-
{ isExporting ? (
diff --git a/src/customisations/ChatExport.ts b/src/customisations/ChatExport.ts
new file mode 100644
index 0000000000..abb55a0748
--- /dev/null
+++ b/src/customisations/ChatExport.ts
@@ -0,0 +1,52 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { ExportFormat, ExportType } from "../utils/exportUtils/exportUtils";
+
+export type ForceChatExportParameters = {
+ format?: ExportFormat;
+ range?: ExportType;
+ // must be < 10**8
+ // only used when range is 'LastNMessages'
+ // default is 100
+ numberOfMessages?: number;
+ includeAttachments?: boolean;
+ // maximum size of exported archive
+ // must be > 0 and < 8000
+ sizeMb?: number;
+};
+
+/**
+ * Force parameters in room chat export
+ * fields returned here are forced
+ * and not allowed to be edited in the chat export form
+ */
+const getForceChatExportParameters = (): ForceChatExportParameters => {
+ return {};
+};
+
+// This interface summarises all available customisation points and also marks
+// them all as optional. This allows customisers to only define and export the
+// customisations they need while still maintaining type safety.
+export interface IChatExportCustomisations {
+ getForceChatExportParameters?: typeof getForceChatExportParameters;
+}
+
+// A real customisation module will define and export one or more of the
+// customisation points that make up `IChatExportCustomisations`.
+export default {
+ getForceChatExportParameters,
+} as IChatExportCustomisations;
diff --git a/src/settings/UIFeature.ts b/src/settings/UIFeature.ts
index 225f785614..f033994214 100644
--- a/src/settings/UIFeature.ts
+++ b/src/settings/UIFeature.ts
@@ -32,7 +32,7 @@ export enum UIFeature {
Communities = "UIFeature.communities",
AdvancedSettings = "UIFeature.advancedSettings",
RoomHistorySettings = "UIFeature.roomHistorySettings",
- TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates"
+ TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",
}
export enum UIComponent {
diff --git a/src/utils/exportUtils/Exporter.ts b/src/utils/exportUtils/Exporter.ts
index 3025be6492..7b4a7bf342 100644
--- a/src/utils/exportUtils/Exporter.ts
+++ b/src/utils/exportUtils/Exporter.ts
@@ -48,7 +48,7 @@ export default abstract class Exporter {
protected setProgressText: React.Dispatch
>,
) {
if (exportOptions.maxSize < 1 * 1024 * 1024|| // Less than 1 MB
- exportOptions.maxSize > 2000 * 1024 * 1024|| // More than ~ 2 GB
+ exportOptions.maxSize > 8000 * 1024 * 1024 || // More than 8 GB
exportOptions.numberOfMessages > 10**8
) {
throw new Error("Invalid export options");
diff --git a/src/utils/validate/index.ts b/src/utils/validate/index.ts
new file mode 100644
index 0000000000..f4357cbc17
--- /dev/null
+++ b/src/utils/validate/index.ts
@@ -0,0 +1 @@
+export * from "./numberInRange";
diff --git a/src/utils/validate/numberInRange.ts b/src/utils/validate/numberInRange.ts
new file mode 100644
index 0000000000..dda5af8f07
--- /dev/null
+++ b/src/utils/validate/numberInRange.ts
@@ -0,0 +1,9 @@
+
+/**
+ * Validates that a value is
+ * - a number
+ * - in a provided range (inclusive)
+ */
+export const validateNumberInRange = (min: number, max: number) => (value?: number) => {
+ return typeof value === 'number' && !(isNaN(value) || min > value || value > max);
+};
diff --git a/test/components/views/dialogs/ExportDialog-test.tsx b/test/components/views/dialogs/ExportDialog-test.tsx
index 8d6d36ad08..07d859358a 100644
--- a/test/components/views/dialogs/ExportDialog-test.tsx
+++ b/test/components/views/dialogs/ExportDialog-test.tsx
@@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import { mount } from 'enzyme';
+import { mocked } from 'ts-jest/utils';
import '../../../skinned-sdk';
import { act } from "react-dom/test-utils";
import { Room } from 'matrix-js-sdk';
@@ -25,13 +26,27 @@ import { ExportType, ExportFormat } from '../../../../src/utils/exportUtils/expo
import { createTestClient, mkStubRoom } from '../../../test-utils';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
import HTMLExporter from "../../../../src/utils/exportUtils/HtmlExport";
+import ChatExport from '../../../../src/customisations/ChatExport';
+import PlainTextExporter from '../../../../src/utils/exportUtils/PlainTextExport';
jest.useFakeTimers();
-const mockHtmlExporter = ({
+const htmlExporterInstance = ({
+ export: jest.fn().mockResolvedValue({}),
+});
+const plainTextExporterInstance = ({
export: jest.fn().mockResolvedValue({}),
});
jest.mock("../../../../src/utils/exportUtils/HtmlExport", () => jest.fn());
+jest.mock("../../../../src/utils/exportUtils/PlainTextExport", () => jest.fn());
+
+jest.mock('../../../../src/customisations/ChatExport', () => ({
+ getForceChatExportParameters: jest.fn().mockReturnValue({}),
+}));
+
+const ChatExportMock = mocked(ChatExport);
+const HTMLExporterMock = mocked(HTMLExporter);
+const PlainTextExporterMock = mocked(PlainTextExporter);
describe('', () => {
const mockClient = createTestClient();
@@ -81,8 +96,13 @@ describe('', () => {
});
beforeEach(() => {
- (HTMLExporter as jest.Mock).mockImplementation(jest.fn().mockReturnValue(mockHtmlExporter));
- mockHtmlExporter.export.mockClear();
+ HTMLExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(htmlExporterInstance));
+ PlainTextExporterMock.mockClear().mockImplementation(jest.fn().mockReturnValue(plainTextExporterInstance));
+ htmlExporterInstance.export.mockClear();
+ plainTextExporterInstance.export.mockClear();
+
+ // default setting value
+ ChatExportMock.getForceChatExportParameters.mockClear().mockReturnValue({});
});
it('renders export dialog', () => {
@@ -104,7 +124,7 @@ describe('', () => {
await submitForm(component);
// 4th arg is an component function
- const exportConstructorProps = (HTMLExporter as jest.Mock).mock.calls[0].slice(0, 3);
+ const exportConstructorProps = HTMLExporterMock.mock.calls[0].slice(0, 3);
expect(exportConstructorProps).toEqual([
defaultProps.room,
ExportType.Timeline,
@@ -114,7 +134,32 @@ describe('', () => {
numberOfMessages: 100,
},
]);
- expect(mockHtmlExporter.export).toHaveBeenCalled();
+ expect(htmlExporterInstance.export).toHaveBeenCalled();
+ });
+
+ it('exports room using values set from ForceRoomExportParameters', async () => {
+ ChatExportMock.getForceChatExportParameters.mockReturnValue({
+ format: ExportFormat.PlainText,
+ range: ExportType.Beginning,
+ sizeMb: 7000,
+ numberOfMessages: 30,
+ includeAttachments: true,
+ });
+ const component = getComponent();
+ await submitForm(component);
+
+ // 4th arg is an component function
+ const exportConstructorProps = PlainTextExporterMock.mock.calls[0].slice(0, 3);
+ expect(exportConstructorProps).toEqual([
+ defaultProps.room,
+ ExportType.Beginning,
+ {
+ attachmentsIncluded: true,
+ maxSize: 7000 * 1024 * 1024,
+ numberOfMessages: 30,
+ },
+ ]);
+ expect(plainTextExporterInstance.export).toHaveBeenCalled();
});
it('renders success screen when export is finished', async () => {
@@ -139,6 +184,19 @@ describe('', () => {
expect(getExportFormatInput(component, ExportFormat.PlainText).props().checked).toBeTruthy();
expect(getExportFormatInput(component, ExportFormat.Html).props().checked).toBeFalsy();
});
+
+ it('hides export format input when format is valid in ForceRoomExportParameters', () => {
+ const component = getComponent();
+ expect(getExportFormatInput(component, ExportFormat.Html).props().checked).toBeTruthy();
+ });
+
+ it('does not render export format when set in ForceRoomExportParameters', () => {
+ ChatExportMock.getForceChatExportParameters.mockReturnValue({
+ format: ExportFormat.PlainText,
+ });
+ const component = getComponent();
+ expect(getExportFormatInput(component, ExportFormat.Html).length).toBeFalsy();
+ });
});
describe('export type', () => {
@@ -153,6 +211,14 @@ describe('', () => {
expect(getExportTypeInput(component).props().value).toEqual(ExportType.Beginning);
});
+ it('does not render export type when set in ForceRoomExportParameters', () => {
+ ChatExportMock.getForceChatExportParameters.mockReturnValue({
+ range: ExportType.Beginning,
+ });
+ const component = getComponent();
+ expect(getExportTypeInput(component).length).toBeFalsy();
+ });
+
it('does not render message count input', async () => {
const component = getComponent();
expect(getMessageCountInput(component).length).toBeFalsy();
@@ -177,7 +243,7 @@ describe('', () => {
await setMessageCount(component, 0);
await submitForm(component);
- expect(mockHtmlExporter.export).not.toHaveBeenCalled();
+ expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it('does not export when export type is lastNMessages and message count is more than max', async () => {
@@ -186,7 +252,7 @@ describe('', () => {
await setMessageCount(component, 99999999999);
await submitForm(component);
- expect(mockHtmlExporter.export).not.toHaveBeenCalled();
+ expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it('exports when export type is NOT lastNMessages and message count is falsy', async () => {
@@ -196,7 +262,7 @@ describe('', () => {
await selectExportType(component, ExportType.Timeline);
await submitForm(component);
- expect(mockHtmlExporter.export).toHaveBeenCalled();
+ expect(htmlExporterInstance.export).toHaveBeenCalled();
});
});
@@ -217,7 +283,7 @@ describe('', () => {
await setSizeLimit(component, 0);
await submitForm(component);
- expect(mockHtmlExporter.export).not.toHaveBeenCalled();
+ expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it('does not export when size limit is larger than max', async () => {
@@ -225,7 +291,7 @@ describe('', () => {
await setSizeLimit(component, 2001);
await submitForm(component);
- expect(mockHtmlExporter.export).not.toHaveBeenCalled();
+ expect(htmlExporterInstance.export).not.toHaveBeenCalled();
});
it('exports when size limit is max', async () => {
@@ -233,11 +299,32 @@ describe('', () => {
await setSizeLimit(component, 2000);
await submitForm(component);
- expect(mockHtmlExporter.export).toHaveBeenCalled();
+ expect(htmlExporterInstance.export).toHaveBeenCalled();
+ });
+
+ it('does not render size limit input when set in ForceRoomExportParameters', () => {
+ ChatExportMock.getForceChatExportParameters.mockReturnValue({
+ sizeMb: 10000,
+ });
+ const component = getComponent();
+ expect(getSizeInput(component).length).toBeFalsy();
+ });
+
+ /**
+ * 2000mb size limit does not apply when higher limit is configured in config
+ */
+ it('exports when size limit set in ForceRoomExportParameters is larger than 2000', async () => {
+ ChatExportMock.getForceChatExportParameters.mockReturnValue({
+ sizeMb: 10000,
+ });
+ const component = getComponent();
+ await submitForm(component);
+
+ expect(htmlExporterInstance.export).toHaveBeenCalled();
});
});
- describe('include attachements', () => {
+ describe('include attachments', () => {
it('renders input with default value of false', () => {
const component = getComponent();
expect(getAttachmentsCheckbox(component).props().checked).toEqual(false);
@@ -248,6 +335,14 @@ describe('', () => {
await setIncludeAttachments(component, true);
expect(getAttachmentsCheckbox(component).props().checked).toEqual(true);
});
+
+ it('does not render input when set in ForceRoomExportParameters', () => {
+ ChatExportMock.getForceChatExportParameters.mockReturnValue({
+ includeAttachments: false,
+ });
+ const component = getComponent();
+ expect(getAttachmentsCheckbox(component).length).toBeFalsy();
+ });
});
});
diff --git a/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap
index 68081248d7..15f65b8497 100644
--- a/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap
+++ b/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap
@@ -105,14 +105,14 @@ Array [
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
Select from the options below to export chats from your timeline
-
- Format
-
+
+ Format
+
- new PlainTextExporter(mockRoom, ExportType.Beginning, exportOption, null),
- ).toThrowError("Invalid export options");
- }
+ const invalidExportOptions: [string, IExportOptions][] = [
+ ['numberOfMessages exceeds max', {
+ numberOfMessages: 10 ** 9,
+ maxSize: 1024 * 1024 * 1024,
+ attachmentsIncluded: false,
+ }],
+ ['maxSize exceeds 8GB', {
+ numberOfMessages: -1,
+ maxSize: 8001 * 1024 * 1024,
+ attachmentsIncluded: false,
+ }],
+ ['maxSize is less than 1mb', {
+ numberOfMessages: 0,
+ maxSize: 0,
+ attachmentsIncluded: false,
+ }],
+ ];
+ it.each(invalidExportOptions)('%s', (_d, options) => {
+ expect(
+ () =>
+ new PlainTextExporter(mockRoom, ExportType.Beginning, options, null),
+ ).toThrowError("Invalid export options");
});
it('tests the file extension splitter', function() {
diff --git a/test/utils/validate/numberInRange-test.ts b/test/utils/validate/numberInRange-test.ts
new file mode 100644
index 0000000000..f7ad2e8c1c
--- /dev/null
+++ b/test/utils/validate/numberInRange-test.ts
@@ -0,0 +1,26 @@
+import { validateNumberInRange } from '../../../src/utils/validate';
+
+describe('validateNumberInRange', () => {
+ const min = 1; const max = 10;
+ it('returns false when value is a not a number', () => {
+ expect(validateNumberInRange(min, max)('test' as unknown as number)).toEqual(false);
+ });
+ it('returns false when value is undefined', () => {
+ expect(validateNumberInRange(min, max)(undefined)).toEqual(false);
+ });
+ it('returns false when value is NaN', () => {
+ expect(validateNumberInRange(min, max)(NaN)).toEqual(false);
+ });
+ it('returns true when value is equal to min', () => {
+ expect(validateNumberInRange(min, max)(min)).toEqual(true);
+ });
+ it('returns true when value is equal to max', () => {
+ expect(validateNumberInRange(min, max)(max)).toEqual(true);
+ });
+ it('returns true when value is an int in range', () => {
+ expect(validateNumberInRange(min, max)(2)).toEqual(true);
+ });
+ it('returns true when value is a float in range', () => {
+ expect(validateNumberInRange(min, max)(2.2)).toEqual(true);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 1e91b21ada..5d3b5f2ab5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1334,6 +1334,17 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
+"@jest/types@^27.4.2":
+ version "27.4.2"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5"
+ integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^3.0.0"
+ "@types/node" "*"
+ "@types/yargs" "^16.0.0"
+ chalk "^4.0.0"
+
"@mapbox/geojson-rewind@^0.5.0":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.1.tgz#adbe16dc683eb40e90934c51a5e28c7bbf44f4e1"
@@ -1983,6 +1994,13 @@
dependencies:
"@types/yargs-parser" "*"
+"@types/yargs@^16.0.0":
+ version "16.0.4"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
+ integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==
+ dependencies:
+ "@types/yargs-parser" "*"
+
"@types/zxcvbn@^4.4.0":
version "4.4.1"
resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.1.tgz#46e42cbdcee681b22181478feaf4af2bc4c1abd2"
@@ -2634,6 +2652,13 @@ browserslist@^4.12.0, browserslist@^4.17.5, browserslist@^4.19.1:
node-releases "^2.0.1"
picocolors "^1.0.0"
+bs-logger@0.x:
+ version "0.2.6"
+ resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
+ integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
+ dependencies:
+ fast-json-stable-stringify "2.x"
+
bs58@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
@@ -2832,6 +2857,11 @@ ci-info@^2.0.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+ci-info@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2"
+ integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==
+
cjs-module-lexer@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f"
@@ -4079,7 +4109,7 @@ fast-glob@^3.2.5, fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
-fast-json-stable-stringify@^2.0.0:
+fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
@@ -5697,6 +5727,18 @@ jest-util@^26.6.2:
is-ci "^2.0.0"
micromatch "^4.0.2"
+jest-util@^27.0.0:
+ version "27.4.2"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.4.2.tgz#ed95b05b1adfd761e2cda47e0144c6a58e05a621"
+ integrity sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA==
+ dependencies:
+ "@jest/types" "^27.4.2"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ ci-info "^3.2.0"
+ graceful-fs "^4.2.4"
+ picomatch "^2.2.3"
+
jest-validate@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec"
@@ -5843,6 +5885,13 @@ json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+json5@2.x, json5@^2.1.2:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
+ integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
+ dependencies:
+ minimist "^1.2.5"
+
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
@@ -5850,13 +5899,6 @@ json5@^1.0.1:
dependencies:
minimist "^1.2.0"
-json5@^2.1.2:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
- integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
- dependencies:
- minimist "^1.2.5"
-
jsprim@^1.2.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
@@ -6028,6 +6070,11 @@ lodash.isequal@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+lodash.memoize@4.x:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+ integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
@@ -6099,6 +6146,11 @@ make-dir@^3.0.0:
dependencies:
semver "^6.0.0"
+make-error@1.x:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
makeerror@1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a"
@@ -7787,18 +7839,18 @@ semver@7.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
-semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
- integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
+semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
+semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+ integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -8484,6 +8536,20 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
+ts-jest@^27.1.3:
+ version "27.1.3"
+ resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.3.tgz#1f723e7e74027c4da92c0ffbd73287e8af2b2957"
+ integrity sha512-6Nlura7s6uM9BVUAoqLH7JHyMXjz8gluryjpPXxr3IxZdAXnU6FhjvVLHFtfd1vsE1p8zD1OJfskkc0jhTSnkA==
+ dependencies:
+ bs-logger "0.x"
+ fast-json-stable-stringify "2.x"
+ jest-util "^27.0.0"
+ json5 "2.x"
+ lodash.memoize "4.x"
+ make-error "1.x"
+ semver "7.x"
+ yargs-parser "20.x"
+
tsconfig-paths@^3.12.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b"
@@ -9057,6 +9123,11 @@ yaml@^1.10.0:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+yargs-parser@20.x, yargs-parser@^20.2.3:
+ version "20.2.9"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+ integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
@@ -9073,11 +9144,6 @@ yargs-parser@^18.1.2:
camelcase "^5.0.0"
decamelize "^1.2.0"
-yargs-parser@^20.2.3:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
yargs-parser@^21.0.0:
version "21.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55"