diff --git a/jest.config.js b/jest.config.js index 8afda31e240..f3ab24b95c4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,6 +20,7 @@ module.exports = { } }, setupFiles: ["./jestSetup.js"], + globalSetup: "./jestGlobalSetup.js", setupFilesAfterEnv: ["@testing-library/jest-native/extend-expect"], collectCoverage: true }; diff --git a/jestGlobalSetup.js b/jestGlobalSetup.js new file mode 100644 index 00000000000..26b8eba0f79 --- /dev/null +++ b/jestGlobalSetup.js @@ -0,0 +1,3 @@ +export default () => { + process.env.TZ = "UTC"; +}; diff --git a/locales/en/index.yml b/locales/en/index.yml index 1e7e6c6f829..400505bae5c 100644 --- a/locales/en/index.yml +++ b/locales/en/index.yml @@ -106,7 +106,7 @@ global: tomorrow: "tomorrow" dateFormats: shortFormat: "%d/%m/%Y" - shortFormatWithTime: "%d/%m/%Y, %H.%M" + shortFormatWithTime: "%d/%m/%Y, %H:%M" fullFormatShortMonthLiteral: "%d %b %Y" fullFormatShortMonthLiteralWithTime: "%d %b %Y, %H:%M" fullFormatShortMonthLiteralWithoutTime: "%d %b %Y" @@ -115,7 +115,8 @@ global: fullMonthLiteralWithYear: "%B %Y" numericMonthYear: "%m/%Y" shortNumericMonthYear: "%m/%-y" - timeFormat: "%H.%M" + timeFormat: "%H:%M" + timeFormatWithTimezone: "%H:%M:%S (%z)" dayMonthWithTime: "%d %b, %H:%M" dayMonthWithoutTime: "%d %b" dayFullMonth: "%d %B" @@ -2858,6 +2859,18 @@ features: body: plain: "plain text" html: "html" + metadata: + title: "the message has legal value" + description: "On {{date}} at {{time}} the message \"{{subject}}\" was sent to you by \"{{sender}}\"" + messageId: "Message identifier:" + links: + copy: "Copy message identifier" + recipients: "Show full list of recipients" + signature: "Show signature details" + certificates: "Show certificates" + originalMessage: "Download original message" + datiCert: "Download daticert.xml" + smime: "Download smime.p7s" euCovidCertificate: save: noPermission: "It seems that the app does not have permissions to save images. Please check your device settings." diff --git a/locales/it/index.yml b/locales/it/index.yml index 25f05ae82ec..c2a34788a2b 100644 --- a/locales/it/index.yml +++ b/locales/it/index.yml @@ -106,14 +106,15 @@ global: tomorrow: "domani" dateFormats: shortFormat: "%d/%m/%Y" - shortFormatWithTime: "%d/%m/%Y, %H.%M" + shortFormatWithTime: "%d/%m/%Y, %H:%M" fullFormatShortMonthLiteral: "%d %b %Y" fullFormatShortMonthLiteralWithTime: "%d %b %Y, %H:%M" fullFormatShortMonthLiteralWithoutTime: "%d %b %Y" fullFormatFullMonthLiteral: "%d %B %Y" fullMonthLiteral: "%B" fullMonthLiteralWithYear: "%B %Y" - timeFormat: "%H.%M" + timeFormat: "%H:%M" + timeFormatWithTimezone: "%H:%M:%S (%z)" dayMonthWithTime: "%d %b, %H:%M" dayMonthWithoutTime: "%d %b" dayFullMonth: "%d %B" @@ -2886,6 +2887,18 @@ features: body: plain: "testo semplice" html: "html" + metadata: + title: "il messaggio ha valore legale" + description: "Il giorno {{date}} alle ore {{time}} il messaggio \"{{subject}}\" ti è stato inviato da \"{{sender}}\"" + messageId: "Identificativo messaggio:" + links: + copy: "Copia identificativo messaggio" + recipients: "Mostra elenco completo dei destinatari" + signature: "Mostra dettagli della firma" + certificates: "Mostra i certificati" + originalMessage: "Scarica il messaggio originale" + datiCert: "Scarica daticert.xml" + smime: "Scarica smime.p7s" euCovidCertificate: save: noPermission: "Sembra che l'app non abbia i permessi per salvare immagini. Controlla le impostazioni del tuo dispositivo." diff --git a/package.json b/package.json index 5a8e60e3a78..836ecdda2c0 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "start-breaking-cycle": "yarn pre-cycle && standard-version -t \"\" --prerelease rc --release-as major", "attach": "react-native attach", "postinstall": "patch-package && rn-nodeify --install path,buffer && chmod +x ./bin/add-ios-source-maps.sh && ./bin/add-ios-source-maps.sh && chmod +x ./bin/add-ios-env-config.sh && ./bin/add-ios-env-config.sh", - "test:ci": "TZ=utc jest --ci --runInBand", - "test:dev": "TZ=utc jest --detectOpenHandles --coverage=false", + "test:ci": "jest --ci --runInBand", + "test:dev": "jest --detectOpenHandles --coverage=false", "prettify": "prettier --write \"ts/**/*.(ts|tsx)\"", "prettier:check": "prettier --check \"ts/**/*.(ts|tsx)\"", "packager:clear": "rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*", diff --git a/ts/components/core/accordion/IOAccordion.tsx b/ts/components/core/accordion/IOAccordion.tsx new file mode 100644 index 00000000000..36332200936 --- /dev/null +++ b/ts/components/core/accordion/IOAccordion.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { StyleSheet } from "react-native"; +import themeVariables from "../../../theme/variables"; +import { H3 } from "../typography/H3"; +import { IOStyles } from "../variables/IOStyles"; +import { RawAccordion } from "./RawAccordion"; + +type Props = Omit, "header"> & { + title: string; +}; + +const styles = StyleSheet.create({ + header: { + marginVertical: themeVariables.contentPadding + } +}); + +/** + * A simplified accordion that accepts a title and one child and uses {@link RawAccordion} + * @param props + * @constructor + */ +export const IOAccordion = (props: Props): React.ReactElement => ( + + {props.title} + + } + > + {props.children} + +); diff --git a/ts/components/core/accordion/RawAccordion.tsx b/ts/components/core/accordion/RawAccordion.tsx new file mode 100644 index 00000000000..a1b565a775e --- /dev/null +++ b/ts/components/core/accordion/RawAccordion.tsx @@ -0,0 +1,125 @@ +import { View } from "native-base"; +import * as React from "react"; +import { useEffect, useRef, useState } from "react"; +import { + AccessibilityProps, + Animated, + Easing, + LayoutAnimation, + StyleSheet, + TouchableWithoutFeedback, + UIManager +} from "react-native"; +import I18n from "../../../i18n"; +import themeVariables from "../../../theme/variables"; +import customVariables from "../../../theme/variables"; +import { isAndroid } from "../../../utils/platform"; +import IconFont from "../../ui/IconFont"; +import { IOStyles } from "../variables/IOStyles"; + +// TODO: handle external initial open/closed state +type Props = { + // The header component, an arrow indicating the open/closed state will be added on the right + header: React.ReactElement; + // The accordion component must accept one children + children: React.ReactElement; + // The component should be animated? default: true + animated?: boolean; + headerStyle?: React.ComponentProps["style"]; + accessibilityLabel?: AccessibilityProps["accessibilityLabel"]; +}; + +const styles = StyleSheet.create({ + headerIcon: { + alignSelf: "center" + }, + row: { + ...IOStyles.row, + justifyContent: "space-between" + }, + internalHeader: { + flex: 1, + paddingRight: themeVariables.contentPadding + } +}); + +/** + * Obtains the degree starting from the open state + * @param isOpen + */ +const getDegree = (isOpen: boolean) => (isOpen ? "-90deg" : "-270deg"); + +/** + * The base accordion component, implements the opening and closing logic for viewing the children + * @param props + * @constructor + */ +export const RawAccordion: React.FunctionComponent = props => { + const [isOpen, setOpen] = useState(false); + const animatedController = useRef(new Animated.Value(1)).current; + const shouldAnimate = props.animated ?? true; + const headerStyle = props.headerStyle ?? {}; + const accessibilityLabel = props.accessibilityLabel + ? `${props.accessibilityLabel}, ` + : ""; + + const arrowAngle = shouldAnimate + ? animatedController.interpolate({ + inputRange: [0, 1], + outputRange: ["-90deg", "-270deg"] + }) + : getDegree(isOpen); + + useEffect(() => { + if (isAndroid) { + UIManager.setLayoutAnimationEnabledExperimental(shouldAnimate); + } + }, [shouldAnimate]); + + const onPress = () => { + if (shouldAnimate) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + Animated.timing(animatedController, { + duration: 300, + toValue: isOpen ? 1 : 0, + useNativeDriver: true, + easing: Easing.linear + }).start(); + } + setOpen(!isOpen); + }; + + return ( + + + + {props.header} + + + + + + {isOpen && props.children} + + ); +}; diff --git a/ts/components/core/accordion/__test__/RawAccordion.test.tsx b/ts/components/core/accordion/__test__/RawAccordion.test.tsx new file mode 100644 index 00000000000..5bed802f606 --- /dev/null +++ b/ts/components/core/accordion/__test__/RawAccordion.test.tsx @@ -0,0 +1,131 @@ +import { fireEvent, render } from "@testing-library/react-native"; +import { View } from "native-base"; +import * as React from "react"; +import Fingerprint from "../../../../../img/test/fingerprint.svg"; +import I18n from "../../../../i18n"; +import { Body } from "../../typography/Body"; +import { H3 } from "../../typography/H3"; +import { IOColors } from "../../variables/IOColors"; +import { IOStyles } from "../../variables/IOStyles"; +import { RawAccordion } from "../RawAccordion"; + +const bodyText = "This is a body text"; +const headerText = "This is an header text"; + +describe("RawAccordion", () => { + jest.useFakeTimers(); + + // eslint-disable-next-line sonarjs/cognitive-complexity + describe("When a RawAccordion is rendered for the first time", () => { + it("Should be in the closed state", () => { + const res = renderRawAccordion(); + expect(res.queryByText(headerText)).not.toBeNull(); + expect(res.queryByText(bodyText)).toBeNull(); + }); + describe("And the header is tapped", () => { + it("Should expand and display the content", () => { + const res = renderRawAccordion(); + const header = res.queryByText(headerText); + if (header !== null) { + fireEvent.press(header); + expect(res.queryByText(headerText)).not.toBeNull(); + expect(res.queryByText(bodyText)).not.toBeNull(); + } else { + fail("header not found"); + } + }); + describe("And the header is tapped again", () => { + it("Should close and hide the content", () => { + const res = renderRawAccordion(); + const header = res.queryByText(headerText); + if (header !== null) { + fireEvent.press(header); + fireEvent.press(header); + expect(res.queryByText(bodyText)).toBeNull(); + } else { + fail("header not found"); + } + }); + }); + }); + describe("And no accessibilityLabel is used", () => { + it("Should use only global.accessibility.collapsed", () => { + const res = renderRawAccordion(); + expect( + res.queryByA11yLabel(I18n.t("global.accessibility.collapsed")) + ).not.toBeNull(); + }); + describe("And the header is tapped", () => { + it("Should use only global.accessibility.expanded", () => { + const res = renderRawAccordion(); + const header = res.queryByText(headerText); + if (header !== null) { + fireEvent.press(header); + expect( + res.queryByA11yLabel(I18n.t("global.accessibility.expanded")) + ).not.toBeNull(); + } else { + fail("header not found"); + } + }); + }); + }); + describe("Accessibility", () => { + describe("And an accessibilityLabel is used", () => { + it("Should use accessibilityLabel and global.accessibility.collapsed", () => { + const res = renderRawAccordion({ + accessibilityLabel: "CustomAccessibilityLabel" + }); + expect( + res.queryByA11yLabel( + `CustomAccessibilityLabel, ${I18n.t( + "global.accessibility.collapsed" + )}` + ) + ).not.toBeNull(); + }); + describe("And the header is tapped", () => { + it("Should use accessibilityLabel and global.accessibility.expanded", () => { + const res = renderRawAccordion({ + accessibilityLabel: "CustomAccessibilityLabel" + }); + const header = res.queryByText(headerText); + if (header !== null) { + fireEvent.press(header); + expect( + res.queryByA11yLabel( + `CustomAccessibilityLabel, ${I18n.t( + "global.accessibility.expanded" + )}` + ) + ).not.toBeNull(); + } else { + fail("header not found"); + } + }); + }); + }); + }); + }); +}); + +const renderRawAccordion = ( + props?: Pick, "accessibilityLabel"> +) => + render( + + +

{headerText}

+ + } + > + {bodyText} +
+ ); diff --git a/ts/components/messages/paginated/MessageDetail/__tests__/__snapshots__/Content.test.tsx.snap b/ts/components/messages/paginated/MessageDetail/__tests__/__snapshots__/Content.test.tsx.snap index 70dfdf5ae8d..7b8b7696757 100644 --- a/ts/components/messages/paginated/MessageDetail/__tests__/__snapshots__/Content.test.tsx.snap +++ b/ts/components/messages/paginated/MessageDetail/__tests__/__snapshots__/Content.test.tsx.snap @@ -43,7 +43,7 @@ exports[`MessageDetailData component when service's contact detail are not defin ] } > - 18/10/2021, 16.00 + 18/10/2021, 16:00 = (props: Props) => { const [expanded, setExpanded] = React.useState(false); diff --git a/ts/features/mvl/saga/networking/handleGetMvlDetails.ts b/ts/features/mvl/saga/networking/handleGetMvlDetails.ts index 398dc0bca82..911c8d43fa3 100644 --- a/ts/features/mvl/saga/networking/handleGetMvlDetails.ts +++ b/ts/features/mvl/saga/networking/handleGetMvlDetails.ts @@ -6,7 +6,12 @@ import { getGenericError, getNetworkError } from "../../../../utils/errors"; import { readablePrivacyReport } from "../../../../utils/reporters"; import { BackendMvlClient } from "../../api/backendMvl"; import { LegalMessageWithContent } from "../../../../../definitions/backend/LegalMessageWithContent"; -import { Mvl, MvlAttachment, MvlAttachmentId } from "../../types/mvlData"; +import { + Mvl, + MvlAttachment, + MvlAttachmentId, + MvlId +} from "../../types/mvlData"; import { toUIMessageDetails } from "../../../../store/reducers/entities/messages/transformers"; import { UIMessageId } from "../../../../store/reducers/entities/messages/types"; import { Attachment } from "../../../../../definitions/backend/Attachment"; @@ -42,7 +47,9 @@ const convertMvlDetail = ( ): Mvl => { // TODO some values are forced or mocked, specs should be improved https://pagopa.atlassian.net/browse/IAMVL-31 const eml = legalMessageWithContent.legal_message.eml; - const certDataHeader = legalMessageWithContent.legal_message.cert_data.header; + const certData = legalMessageWithContent.legal_message.cert_data; + const certDataHeader = certData.header; + const msgId = certData.data.msg_id as string; return { message: toUIMessageDetails(legalMessageWithContent), legalMessage: { @@ -52,6 +59,9 @@ const convertMvlDetail = ( }, attachments: eml.attachments.map(convertMvlAttachment), metadata: { + id: msgId as MvlId, + timestamp: certData.data.timestamp, + subject: eml.subject, sender: EmailAddress.decode(certDataHeader.sender).getOrElse( valueNotAvailable as EmailAddress ), diff --git a/ts/features/mvl/screens/details/MvlDetailsScreen.tsx b/ts/features/mvl/screens/details/MvlDetailsScreen.tsx index f643a152a5c..907f5d9c6b9 100644 --- a/ts/features/mvl/screens/details/MvlDetailsScreen.tsx +++ b/ts/features/mvl/screens/details/MvlDetailsScreen.tsx @@ -43,7 +43,7 @@ export const MvlDetailsScreen = (props: Props): React.ReactElement => { diff --git a/ts/features/mvl/screens/details/components/MvlDetailsHeader.tsx b/ts/features/mvl/screens/details/components/MvlDetailsHeader.tsx index c6251d4e88e..25f433e112d 100644 --- a/ts/features/mvl/screens/details/components/MvlDetailsHeader.tsx +++ b/ts/features/mvl/screens/details/components/MvlDetailsHeader.tsx @@ -4,18 +4,18 @@ import { StyleSheet } from "react-native"; import Svg from "react-native-svg"; import Attachment from "../../../../../../img/features/mvl/attachment.svg"; import LegalMessage from "../../../../../../img/features/mvl/legalMessage.svg"; +import { H1 } from "../../../../../components/core/typography/H1"; import { H5 } from "../../../../../components/core/typography/H5"; import { IOColors } from "../../../../../components/core/variables/IOColors"; import { HeaderDueDateBar } from "../../../../../components/messages/paginated/MessageDetail/common/HeaderDueDateBar"; -import { MessageTitle } from "../../../../../components/messages/paginated/MessageDetail/common/MessageTitle"; import OrganizationHeader from "../../../../../components/OrganizationHeader"; import I18n from "../../../../../i18n"; -import { UIMessageDetails } from "../../../../../store/reducers/entities/messages/types"; import { UIService } from "../../../../../store/reducers/entities/services/types"; import themeVariables from "../../../../../theme/variables"; +import { Mvl } from "../../../types/mvlData"; type Props = { - message: UIMessageDetails; + mvl: Mvl; hasAttachments: boolean; service?: UIService; }; @@ -101,7 +101,7 @@ export const MvlDetailsHeader = (props: Props) => ( )} - +

{props.mvl.legalMessage.metadata.subject}

@@ -110,7 +110,10 @@ export const MvlDetailsHeader = (props: Props) => ( {/* TODO: TMP, how is calculated hasPaidBadge without using the paginated data? https://pagopa.atlassian.net/browse/IAMVL-22 */} - + ); diff --git a/ts/features/mvl/screens/details/components/MvlMetadata.tsx b/ts/features/mvl/screens/details/components/MvlMetadata.tsx index 63539d13da4..4f0a1f74921 100644 --- a/ts/features/mvl/screens/details/components/MvlMetadata.tsx +++ b/ts/features/mvl/screens/details/components/MvlMetadata.tsx @@ -1,21 +1,156 @@ -import { View } from "native-base"; +import { Toast, View } from "native-base"; import * as React from "react"; -import { H2 } from "../../../../../components/core/typography/H2"; +import { StyleSheet } from "react-native"; +import LegalMessage from "../../../../../../img/features/mvl/legalMessage.svg"; +import { RawAccordion } from "../../../../../components/core/accordion/RawAccordion"; +import { Body } from "../../../../../components/core/typography/Body"; +import { H3 } from "../../../../../components/core/typography/H3"; +import { H4 } from "../../../../../components/core/typography/H4"; +import { Link } from "../../../../../components/core/typography/Link"; import { IOColors } from "../../../../../components/core/variables/IOColors"; +import { IOStyles } from "../../../../../components/core/variables/IOStyles"; +import ItemSeparatorComponent from "../../../../../components/ItemSeparatorComponent"; +import I18n from "../../../../../i18n"; +import themeVariables from "../../../../../theme/variables"; +import { clipboardSetStringWithFeedback } from "../../../../../utils/clipboard"; +import { localeDateFormat } from "../../../../../utils/locale"; import { MvlMetadata } from "../../../types/mvlData"; type Props = { metadata: MvlMetadata; }; +const styles = StyleSheet.create({ + background: { + backgroundColor: IOColors.greyLight, + marginHorizontal: -themeVariables.contentPadding + }, + headerContent: { + marginVertical: themeVariables.contentPadding, + marginHorizontal: themeVariables.contentPadding + }, + link: { + paddingVertical: 16 + } +}); + +const Header = (): React.ReactElement => ( + + + +

+ {I18n.t("features.mvl.details.metadata.title")} +

+
+); + +const Description = (props: Props): React.ReactElement => ( + + + {I18n.t("features.mvl.details.metadata.description", { + date: localeDateFormat( + props.metadata.timestamp, + I18n.t("global.dateFormats.shortFormat") + ), + time: localeDateFormat( + props.metadata.timestamp, + I18n.t("global.dateFormats.timeFormatWithTimezone") + ), + subject: props.metadata.subject, + sender: props.metadata.sender + })} + +

{I18n.t("features.mvl.details.metadata.messageId")}

+ {props.metadata.id} +
+); + +type LinkRepresentation = { + text: string; + action: () => void; +}; + +const tmpNotImplementedFeedback = () => { + Toast.show({ + text: I18n.t("wallet.incoming.title") + }); +}; + +const generateLinks = (props: Props): ReadonlyArray => { + const sender = + props.metadata.cc.length > 0 + ? [ + { + text: I18n.t("features.mvl.details.metadata.links.recipients"), + action: tmpNotImplementedFeedback + } + ] + : []; + + return [ + { + text: I18n.t("features.mvl.details.metadata.links.copy"), + action: () => clipboardSetStringWithFeedback(props.metadata.id) + }, + ...sender, + { + text: I18n.t("features.mvl.details.metadata.links.signature"), + action: tmpNotImplementedFeedback + }, + { + text: I18n.t("features.mvl.details.metadata.links.certificates"), + action: tmpNotImplementedFeedback + }, + { + text: I18n.t("features.mvl.details.metadata.links.originalMessage"), + action: tmpNotImplementedFeedback + }, + { + text: I18n.t("features.mvl.details.metadata.links.datiCert"), + action: tmpNotImplementedFeedback + }, + { + text: I18n.t("features.mvl.details.metadata.links.smime"), + action: tmpNotImplementedFeedback + } + ]; +}; + +const Links = (props: Props): React.ReactElement => { + const links = generateLinks(props); + return ( + <> + {links.map((l, index) => ( + + + {l.text} + + {index < links.length - 1 && ( + + )} + + ))} + + ); +}; + /** * An accordion that allows the user to navigate and see all the legal message related metadata - * TODO: this is a placeholder, will be implemented in https://pagopa.atlassian.net/browse/IAMVL-20 * @constructor - * @param _ + * @param props */ -export const MvlMetadataComponent = (_: Props): React.ReactElement => ( - -

{"Metadata placeholder"}

+export const MvlMetadataComponent = (props: Props): React.ReactElement => ( + + } + headerStyle={styles.headerContent} + accessibilityLabel={I18n.t("features.mvl.details.metadata.title")} + > + + + + + + ); diff --git a/ts/features/mvl/screens/details/components/__test__/MvlDetailsHeader.test.tsx b/ts/features/mvl/screens/details/components/__test__/MvlDetailsHeader.test.tsx index ac62b7b04c5..478905a380e 100644 --- a/ts/features/mvl/screens/details/components/__test__/MvlDetailsHeader.test.tsx +++ b/ts/features/mvl/screens/details/components/__test__/MvlDetailsHeader.test.tsx @@ -9,7 +9,7 @@ describe("MvlDetailsHeader", () => { describe("When the mvl has attachments", () => { it("Should be rendered the hasAttachments HeaderItem", () => { const component = renderComponent({ - message: mvlMock.message, + mvl: mvlMock, hasAttachments: true }); expect( @@ -19,7 +19,7 @@ describe("MvlDetailsHeader", () => { it("Should be rendered the legalMessage HeaderItem", () => { const component = renderComponent({ - message: mvlMock.message, + mvl: mvlMock, hasAttachments: true }); expect( @@ -31,7 +31,7 @@ describe("MvlDetailsHeader", () => { describe("When the mvl has no attachments", () => { it("Should not be rendered the hasAttachments HeaderItem", () => { const component = renderComponent({ - message: mvlMock.message, + mvl: mvlMock, hasAttachments: false }); expect( @@ -41,7 +41,7 @@ describe("MvlDetailsHeader", () => { it("Should be rendered the legalMessage HeaderItem", () => { const component = renderComponent({ - message: mvlMock.message, + mvl: mvlMock, hasAttachments: false }); expect( diff --git a/ts/features/mvl/screens/details/components/__test__/MvlMetadata.test.tsx b/ts/features/mvl/screens/details/components/__test__/MvlMetadata.test.tsx new file mode 100644 index 00000000000..5687613a8c7 --- /dev/null +++ b/ts/features/mvl/screens/details/components/__test__/MvlMetadata.test.tsx @@ -0,0 +1,92 @@ +import { fireEvent, render } from "@testing-library/react-native"; +import * as React from "react"; +import I18n from "../../../../../../i18n"; +import { localeDateFormat } from "../../../../../../utils/locale"; +import { mvlMockMetadata } from "../../../../types/__mock__/mvlMock"; +import { MvlMetadata } from "../../../../types/mvlData"; +import { MvlMetadataComponent } from "../MvlMetadata"; + +describe("MvlMetadata", () => { + jest.useFakeTimers(); + describe("When MvlMetadataComponent is rendered for the first time", () => { + it("Should be visible only the header", () => { + const res = renderMvlMetadata(); + expect( + res.queryByText(I18n.t("features.mvl.details.metadata.title")) + ).not.toBeNull(); + }); + describe("And the header is tapped", () => { + it("Should display the metadata information", () => { + const res = renderMvlMetadata(); + const header = res.queryByText( + I18n.t("features.mvl.details.metadata.title") + ); + if (header !== null) { + fireEvent.press(header); + + expect( + res.queryByText( + I18n.t("features.mvl.details.metadata.description", { + date: localeDateFormat( + mvlMockMetadata.timestamp, + I18n.t("global.dateFormats.shortFormat") + ), + time: localeDateFormat( + mvlMockMetadata.timestamp, + I18n.t("global.dateFormats.timeFormatWithTimezone") + ), + subject: mvlMockMetadata.subject, + sender: mvlMockMetadata.sender + }) + ) + ).not.toBeNull(); + } else { + fail("header not found"); + } + }); + describe("And there are at least one cc", () => { + it("Should display all the 7 link including the recipients link", () => { + const res = renderMvlMetadata(); + const header = res.queryByText( + I18n.t("features.mvl.details.metadata.title") + ); + if (header !== null) { + fireEvent.press(header); + expect(res.queryAllByA11yRole("link").length).toBe(7); + expect( + res.queryByText( + I18n.t("features.mvl.details.metadata.links.recipients") + ) + ).not.toBeNull(); + } else { + fail("header not found"); + } + }); + }); + describe("And there aren't any cc", () => { + it("Should display only 6 link without the recipients link", () => { + const res = renderMvlMetadata({ ...mvlMockMetadata, cc: [] }); + const header = res.queryByText( + I18n.t("features.mvl.details.metadata.title") + ); + if (header !== null) { + fireEvent.press(header); + expect(res.queryAllByA11yRole("link").length).toBe(6); + expect( + res.queryByText( + I18n.t("features.mvl.details.metadata.links.recipients") + ) + ).toBeNull(); + } else { + fail("header not found"); + } + }); + }); + }); + }); +}); + +const renderMvlMetadata = (metadata?: MvlMetadata) => { + const metadataParams = metadata ?? mvlMockMetadata; + return render(); +}; diff --git a/ts/features/mvl/types/__mock__/mvlMock.ts b/ts/features/mvl/types/__mock__/mvlMock.ts index 74b8481eb2c..78f76e598a7 100644 --- a/ts/features/mvl/types/__mock__/mvlMock.ts +++ b/ts/features/mvl/types/__mock__/mvlMock.ts @@ -10,6 +10,7 @@ import { MvlAttachmentId, MvlBody, MvlData, + MvlId, MvlMetadata } from "../mvlData"; @@ -50,6 +51,9 @@ export const mvlMockOtherAttachment: MvlAttachment = { }; export const mvlMockMetadata: MvlMetadata = { + id: "opec2951.20210927163605.31146.306.1.66@pec.poc.it" as MvlId, + timestamp: new Date("2021-11-09T01:30:00.000Z"), + subject: "Legal Message subject", sender: "sender@mailpec.com" as EmailAddress, receiver: "receiver@emailpec.com" as EmailAddress, cc: ["cc1@emailpec.com" as EmailAddress, "cc2@emailpec.com" as EmailAddress], diff --git a/ts/features/mvl/types/mvlData.ts b/ts/features/mvl/types/mvlData.ts index be538d028d9..3b3fb201753 100644 --- a/ts/features/mvl/types/mvlData.ts +++ b/ts/features/mvl/types/mvlData.ts @@ -18,6 +18,8 @@ export type MvlBody = { export type MvlAttachmentId = string & IUnitTag<"MvlAttachmentId">; +export type MvlId = string & IUnitTag<"MvlId">; + /** * Represent an attachment with the metadata and resourceUrl to retrieve the attachment */ @@ -38,6 +40,9 @@ export type MvlAttachment = { * TODO: Just an initial stub, should be completed and refined */ export type MvlMetadata = { + id: MvlId; + subject: string; + timestamp: Date; sender: EmailAddress; receiver: EmailAddress; cc: ReadonlyArray; diff --git a/ts/screens/showroom/OthersShowroom.tsx b/ts/screens/showroom/OthersShowroom.tsx index 841b33ed18a..b7e7d9a4f19 100644 --- a/ts/screens/showroom/OthersShowroom.tsx +++ b/ts/screens/showroom/OthersShowroom.tsx @@ -5,7 +5,11 @@ import AlphaChannel from "../../../img/test/alphaChannel.svg"; import Fingerprint from "../../../img/test/fingerprint.svg"; import Analytics from "../../../img/test/analytics.svg"; import { InfoBox } from "../../components/box/InfoBox"; +import { IOAccordion } from "../../components/core/accordion/IOAccordion"; +import { RawAccordion } from "../../components/core/accordion/RawAccordion"; import { Body } from "../../components/core/typography/Body"; +import { H3 } from "../../components/core/typography/H3"; +import { H5 } from "../../components/core/typography/H5"; import { Label } from "../../components/core/typography/Label"; import { IOColors } from "../../components/core/variables/IOColors"; import { IOStyles } from "../../components/core/variables/IOStyles"; @@ -23,8 +27,8 @@ const styles = StyleSheet.create({ export const OthersShowroom = () => ( + - Lorem ipsum dolor sit amet, consectetur adipisci elit, sed do eiusmod @@ -53,5 +57,60 @@ export const OthersShowroom = () => ( + + + + + + + + + + <> + + + + + + + + + + + +

{"Custom header "}

+
{"Purgatorio, Canto VI"}
+
+ } + > + + {"Ahi serva Italia, di dolore ostello, \n" + + "nave sanza nocchiere in gran tempesta, \n" + + "non donna di province, ma bordello!" + + "\n\n" + + "Quell’anima gentil fu così presta, \n" + + "sol per lo dolce suon de la sua terra, \n" + + "di fare al cittadin suo quivi festa;" + + "\n\n" + + "e ora in te non stanno sanza guerra \n" + + "li vivi tuoi, e l’un l’altro si rode \n" + + "di quei ch’un muro e una fossa serra." + + "\n\n" + + "Cerca, misera, intorno da le prode \n" + + "le tue marine, e poi ti guarda in seno, \n" + + "s’alcuna parte in te di pace gode." + + "\n\n" + + "Che val perché ti racconciasse il freno \n" + + "Iustiniano, se la sella è vota? \n" + + "Sanz’esso fora la vergogna meno."} + + +
);