Chat export parameter customisation (#7647)

* use export settings and hide fields

Signed-off-by: Kerry Archibald <kerrya@element.io>

* fix exporter tests

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test ExportDialog with settings

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy debugs, rename setting to Parameters

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use reasonable 100gb limit

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use normal setting instead of UIFeature

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use a customisation

Signed-off-by: Kerry Archibald <kerrya@element.io>

* move validateNumberInRange to utils

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use nullish coalesce

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use 8gb size limit for customisation

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update comments

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-01-31 12:54:14 +01:00 committed by GitHub
parent ad87ee0a0f
commit 085ecc7f5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 501 additions and 189 deletions

View File

@ -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"
},

View File

@ -89,3 +89,7 @@ limitations under the License.
padding: 9px 10px;
}
}
.mx_ExportDialog_attachments-checkbox {
margin-top: $spacing-16;
}

View File

@ -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<SetStateAction<ExportFormat>>;
setExportType?: Dispatch<SetStateAction<ExportType>>;
setAttachments?: Dispatch<SetStateAction<boolean>>;
setNumberOfMessages?: Dispatch<SetStateAction<number>>;
setSizeLimit?: Dispatch<SetStateAction<number>>;
}
/**
* 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<number>(config.numberOfMessages ?? 100);
const [sizeLimit, setSizeLimit] = useState<number | null>(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<IProps> = ({ 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<number>(100);
const [sizeLimit, setSizeLimit] = useState<number | null>(8);
const sizeLimitRef = useRef<Field>();
const messageCountRef = useRef<Field>();
const [exportProgressText, setExportProgressText] = useState(_t("Processing..."));
@ -110,9 +162,10 @@ const ExportDialog: React.FC<IProps> = ({ 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<IProps> = ({ 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<IProps> = ({ 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<IProps> = ({ room, onFinished }) => {
});
let messageCount = null;
if (exportType === ExportType.LastNMessages) {
if (exportType === ExportType.LastNMessages && setNumberOfMessages) {
messageCount = (
<Field
id="message-count"
@ -319,61 +367,74 @@ const ExportDialog: React.FC<IProps> = ({ room, onFinished }) => {
) }
</p> : null }
<span className="mx_ExportDialog_subheading">
{ _t("Format") }
</span>
<div className="mx_ExportDialog_options">
<StyledRadioGroup
name="exportFormat"
value={exportFormat}
onChange={(key) => setExportFormat(ExportFormat[key])}
definitions={exportFormatOptions}
/>
{ !!setExportFormat && <>
<span className="mx_ExportDialog_subheading">
{ _t("Format") }
</span>
<span className="mx_ExportDialog_subheading">
{ _t("Messages") }
</span>
<StyledRadioGroup
name="exportFormat"
value={exportFormat}
onChange={(key) => setExportFormat(ExportFormat[key])}
definitions={exportFormatOptions}
/>
</> }
<Field
id="export-type"
element="select"
value={exportType}
onChange={(e) => {
setExportType(ExportType[e.target.value]);
}}
>
{ exportTypeOptions }
</Field>
{ messageCount }
{
!!setExportType && <>
<span className="mx_ExportDialog_subheading">
{ _t("Size Limit") }
</span>
<span className="mx_ExportDialog_subheading">
{ _t("Messages") }
</span>
<Field
id="size-limit"
type="number"
autoComplete="off"
onValidate={onValidateSize}
element="input"
ref={sizeLimitRef}
value={sizeLimit.toString()}
postfixComponent={sizePostFix}
onChange={(e) => setSizeLimit(parseInt(e.target.value))}
/>
<Field
id="export-type"
element="select"
value={exportType}
onChange={(e) => {
setExportType(ExportType[e.target.value]);
}}
>
{ exportTypeOptions }
</Field>
{ messageCount }
</>
}
{ setSizeLimit && <>
<span className="mx_ExportDialog_subheading">
{ _t("Size Limit") }
</span>
<Field
id="size-limit"
type="number"
autoComplete="off"
onValidate={onValidateSize}
element="input"
ref={sizeLimitRef}
value={sizeLimit.toString()}
postfixComponent={sizePostFix}
onChange={(e) => setSizeLimit(parseInt(e.target.value))}
/>
</> }
{ setAttachments && <>
<StyledCheckbox
className="mx_ExportDialog_attachments-checkbox"
id="include-attachments"
checked={includeAttachments}
onChange={(e) =>
setAttachments(
(e.target as HTMLInputElement).checked,
)
}
>
{ _t("Include Attachments") }
</StyledCheckbox>
</> }
<StyledCheckbox
id="include-attachments"
checked={includeAttachments}
onChange={(e) =>
setAttachments(
(e.target as HTMLInputElement).checked,
)
}
>
{ _t("Include Attachments") }
</StyledCheckbox>
</div>
{ isExporting ? (
<div data-test-id='export-progress' className="mx_ExportDialog_progress">

View File

@ -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;

View File

@ -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 {

View File

@ -48,7 +48,7 @@ export default abstract class Exporter {
protected setProgressText: React.Dispatch<React.SetStateAction<string>>,
) {
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");

View File

@ -0,0 +1 @@
export * from "./numberInRange";

View File

@ -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);
};

View File

@ -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('<ExportDialog />', () => {
const mockClient = createTestClient();
@ -81,8 +96,13 @@ describe('<ExportDialog />', () => {
});
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
await selectExportType(component, ExportType.Timeline);
await submitForm(component);
expect(mockHtmlExporter.export).toHaveBeenCalled();
expect(htmlExporterInstance.export).toHaveBeenCalled();
});
});
@ -217,7 +283,7 @@ describe('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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('<ExportDialog />', () => {
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();
});
});
});

View File

@ -105,14 +105,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<div
class="mx_ExportDialog_options"
>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
@ -235,7 +235,7 @@ Array [
</span>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="include-attachments"
@ -326,14 +326,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<div
class="mx_ExportDialog_options"
>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
@ -456,7 +456,7 @@ Array [
</span>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="include-attachments"
@ -563,14 +563,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<div
className="mx_ExportDialog_options"
>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<StyledRadioGroup
definitions={
Array [
@ -790,12 +790,12 @@ Array [
</Field>
<StyledCheckbox
checked={false}
className=""
className="mx_ExportDialog_attachments-checkbox"
id="include-attachments"
onChange={[Function]}
>
<span
className="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
className="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked={false}
@ -962,14 +962,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<div
class="mx_ExportDialog_options"
>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
@ -1092,7 +1092,7 @@ Array [
</span>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="include-attachments"
@ -1183,14 +1183,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<div
class="mx_ExportDialog_options"
>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
@ -1313,7 +1313,7 @@ Array [
</span>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="include-attachments"
@ -1420,14 +1420,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<div
className="mx_ExportDialog_options"
>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<StyledRadioGroup
definitions={
Array [
@ -1647,12 +1647,12 @@ Array [
</Field>
<StyledCheckbox
checked={false}
className=""
className="mx_ExportDialog_attachments-checkbox"
id="include-attachments"
onChange={[Function]}
>
<span
className="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
className="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked={false}
@ -1806,14 +1806,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<div
class="mx_ExportDialog_options"
>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
@ -1936,7 +1936,7 @@ Array [
</span>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="include-attachments"
@ -2027,14 +2027,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<div
class="mx_ExportDialog_options"
>
<span
class="mx_ExportDialog_subheading"
>
Format
</span>
<label
class="mx_StyledRadioButton mx_StyledRadioButton_enabled mx_StyledRadioButton_checked"
>
@ -2157,7 +2157,7 @@ Array [
</span>
</div>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
class="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
id="include-attachments"
@ -2264,14 +2264,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<div
className="mx_ExportDialog_options"
>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<StyledRadioGroup
definitions={
Array [
@ -2491,12 +2491,12 @@ Array [
</Field>
<StyledCheckbox
checked={false}
className=""
className="mx_ExportDialog_attachments-checkbox"
id="include-attachments"
onChange={[Function]}
>
<span
className="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
className="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked={false}
@ -2613,14 +2613,14 @@ Array [
<p>
Select from the options below to export chats from your timeline
</p>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<div
className="mx_ExportDialog_options"
>
<span
className="mx_ExportDialog_subheading"
>
Format
</span>
<StyledRadioGroup
definitions={
Array [
@ -2840,12 +2840,12 @@ Array [
</Field>
<StyledCheckbox
checked={false}
className=""
className="mx_ExportDialog_attachments-checkbox"
id="include-attachments"
onChange={[Function]}
>
<span
className="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
className="mx_Checkbox mx_ExportDialog_attachments-checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked={false}

View File

@ -50,24 +50,6 @@ describe('export', function() {
attachmentsIncluded: false,
};
const invalidExportOptions: IExportOptions[] = [
{
numberOfMessages: 10**9,
maxSize: 1024 * 1024 * 1024,
attachmentsIncluded: false,
},
{
numberOfMessages: -1,
maxSize: 4096 * 1024 * 1024,
attachmentsIncluded: false,
},
{
numberOfMessages: 0,
maxSize: 0,
attachmentsIncluded: false,
},
];
function createRoom() {
const room = new Room(generateRoomId(), null, client.getUserId());
return room;
@ -212,13 +194,28 @@ describe('export', function() {
).toBeTruthy();
});
it('checks if the export options are valid', function() {
for (const exportOption of invalidExportOptions) {
expect(
() =>
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() {

View File

@ -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);
});
});

104
yarn.lock
View File

@ -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"