From 9e011b1904cdd9217bc4eba6543218c569a4bd76 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Feb 2023 12:42:58 +0100 Subject: [PATCH 1/9] Rename all files to `.ts`/`.tsx` --- packages/output/src/components/withLink/{index.js => index.tsx} | 0 .../output/src/components/withLink/test/{output.js => output.tsx} | 0 packages/output/src/{constants.js => constants.ts} | 0 packages/output/src/{element.js => element.tsx} | 0 packages/output/src/{index.js => index.ts} | 0 packages/output/src/{page.js => page.tsx} | 0 packages/output/src/{story.js => story.tsx} | 0 packages/output/src/test/_utils/{constants.js => constants.ts} | 0 packages/output/src/test/{page.js => page.tsx} | 0 packages/output/src/test/{story.js => story.tsx} | 0 packages/output/src/test/{textElement.js => textElement.tsx} | 0 .../output/src/utils/{ampBoilerplate.js => ampBoilerplate.tsx} | 0 .../output/src/utils/{backgroundAudio.js => backgroundAudio.tsx} | 0 .../src/utils/{fontDeclarations.js => fontDeclarations.tsx} | 0 .../src/utils/{getAutoAdvanceAfter.js => getAutoAdvanceAfter.ts} | 0 .../{getLongestMediaElement.js => getLongestMediaElement.ts} | 0 .../src/utils/{getPreloadResources.js => getPreloadResources.ts} | 0 .../output/src/utils/{getStoryMarkup.js => getStoryMarkup.tsx} | 0 .../{getTextElementTagNames.js => getTextElementTagNames.ts} | 0 .../utils/{getUsedAmpExtensions.js => getUsedAmpExtensions.ts} | 0 packages/output/src/utils/{outlink.js => outlink.tsx} | 0 .../src/utils/{shoppingAttachment.js => shoppingAttachment.tsx} | 0 packages/output/src/utils/{styles.js => styles.tsx} | 0 .../src/utils/test/{fontDeclarations.js => fontDeclarations.tsx} | 0 .../utils/test/{getAutoAdvanceAfter.js => getAutoAdvanceAfter.ts} | 0 .../test/{getLongestMediaElement.js => getLongestMediaElement.ts} | 0 .../utils/test/{getPreloadResources.js => getPreloadResources.ts} | 0 .../src/utils/test/{getStoryMarkup.js => getStoryMarkup.ts} | 0 .../test/{getTextElementTagNames.js => getTextElementTagNames.ts} | 0 .../test/{getUsedAmpExtensions.js => getUsedAmpExtensions.ts} | 0 .../{populateElementFontData.js => populateElementFontData.ts} | 0 31 files changed, 0 insertions(+), 0 deletions(-) rename packages/output/src/components/withLink/{index.js => index.tsx} (100%) rename packages/output/src/components/withLink/test/{output.js => output.tsx} (100%) rename packages/output/src/{constants.js => constants.ts} (100%) rename packages/output/src/{element.js => element.tsx} (100%) rename packages/output/src/{index.js => index.ts} (100%) rename packages/output/src/{page.js => page.tsx} (100%) rename packages/output/src/{story.js => story.tsx} (100%) rename packages/output/src/test/_utils/{constants.js => constants.ts} (100%) rename packages/output/src/test/{page.js => page.tsx} (100%) rename packages/output/src/test/{story.js => story.tsx} (100%) rename packages/output/src/test/{textElement.js => textElement.tsx} (100%) rename packages/output/src/utils/{ampBoilerplate.js => ampBoilerplate.tsx} (100%) rename packages/output/src/utils/{backgroundAudio.js => backgroundAudio.tsx} (100%) rename packages/output/src/utils/{fontDeclarations.js => fontDeclarations.tsx} (100%) rename packages/output/src/utils/{getAutoAdvanceAfter.js => getAutoAdvanceAfter.ts} (100%) rename packages/output/src/utils/{getLongestMediaElement.js => getLongestMediaElement.ts} (100%) rename packages/output/src/utils/{getPreloadResources.js => getPreloadResources.ts} (100%) rename packages/output/src/utils/{getStoryMarkup.js => getStoryMarkup.tsx} (100%) rename packages/output/src/utils/{getTextElementTagNames.js => getTextElementTagNames.ts} (100%) rename packages/output/src/utils/{getUsedAmpExtensions.js => getUsedAmpExtensions.ts} (100%) rename packages/output/src/utils/{outlink.js => outlink.tsx} (100%) rename packages/output/src/utils/{shoppingAttachment.js => shoppingAttachment.tsx} (100%) rename packages/output/src/utils/{styles.js => styles.tsx} (100%) rename packages/output/src/utils/test/{fontDeclarations.js => fontDeclarations.tsx} (100%) rename packages/output/src/utils/test/{getAutoAdvanceAfter.js => getAutoAdvanceAfter.ts} (100%) rename packages/output/src/utils/test/{getLongestMediaElement.js => getLongestMediaElement.ts} (100%) rename packages/output/src/utils/test/{getPreloadResources.js => getPreloadResources.ts} (100%) rename packages/output/src/utils/test/{getStoryMarkup.js => getStoryMarkup.ts} (100%) rename packages/output/src/utils/test/{getTextElementTagNames.js => getTextElementTagNames.ts} (100%) rename packages/output/src/utils/test/{getUsedAmpExtensions.js => getUsedAmpExtensions.ts} (100%) rename packages/output/src/utils/test/{populateElementFontData.js => populateElementFontData.ts} (100%) diff --git a/packages/output/src/components/withLink/index.js b/packages/output/src/components/withLink/index.tsx similarity index 100% rename from packages/output/src/components/withLink/index.js rename to packages/output/src/components/withLink/index.tsx diff --git a/packages/output/src/components/withLink/test/output.js b/packages/output/src/components/withLink/test/output.tsx similarity index 100% rename from packages/output/src/components/withLink/test/output.js rename to packages/output/src/components/withLink/test/output.tsx diff --git a/packages/output/src/constants.js b/packages/output/src/constants.ts similarity index 100% rename from packages/output/src/constants.js rename to packages/output/src/constants.ts diff --git a/packages/output/src/element.js b/packages/output/src/element.tsx similarity index 100% rename from packages/output/src/element.js rename to packages/output/src/element.tsx diff --git a/packages/output/src/index.js b/packages/output/src/index.ts similarity index 100% rename from packages/output/src/index.js rename to packages/output/src/index.ts diff --git a/packages/output/src/page.js b/packages/output/src/page.tsx similarity index 100% rename from packages/output/src/page.js rename to packages/output/src/page.tsx diff --git a/packages/output/src/story.js b/packages/output/src/story.tsx similarity index 100% rename from packages/output/src/story.js rename to packages/output/src/story.tsx diff --git a/packages/output/src/test/_utils/constants.js b/packages/output/src/test/_utils/constants.ts similarity index 100% rename from packages/output/src/test/_utils/constants.js rename to packages/output/src/test/_utils/constants.ts diff --git a/packages/output/src/test/page.js b/packages/output/src/test/page.tsx similarity index 100% rename from packages/output/src/test/page.js rename to packages/output/src/test/page.tsx diff --git a/packages/output/src/test/story.js b/packages/output/src/test/story.tsx similarity index 100% rename from packages/output/src/test/story.js rename to packages/output/src/test/story.tsx diff --git a/packages/output/src/test/textElement.js b/packages/output/src/test/textElement.tsx similarity index 100% rename from packages/output/src/test/textElement.js rename to packages/output/src/test/textElement.tsx diff --git a/packages/output/src/utils/ampBoilerplate.js b/packages/output/src/utils/ampBoilerplate.tsx similarity index 100% rename from packages/output/src/utils/ampBoilerplate.js rename to packages/output/src/utils/ampBoilerplate.tsx diff --git a/packages/output/src/utils/backgroundAudio.js b/packages/output/src/utils/backgroundAudio.tsx similarity index 100% rename from packages/output/src/utils/backgroundAudio.js rename to packages/output/src/utils/backgroundAudio.tsx diff --git a/packages/output/src/utils/fontDeclarations.js b/packages/output/src/utils/fontDeclarations.tsx similarity index 100% rename from packages/output/src/utils/fontDeclarations.js rename to packages/output/src/utils/fontDeclarations.tsx diff --git a/packages/output/src/utils/getAutoAdvanceAfter.js b/packages/output/src/utils/getAutoAdvanceAfter.ts similarity index 100% rename from packages/output/src/utils/getAutoAdvanceAfter.js rename to packages/output/src/utils/getAutoAdvanceAfter.ts diff --git a/packages/output/src/utils/getLongestMediaElement.js b/packages/output/src/utils/getLongestMediaElement.ts similarity index 100% rename from packages/output/src/utils/getLongestMediaElement.js rename to packages/output/src/utils/getLongestMediaElement.ts diff --git a/packages/output/src/utils/getPreloadResources.js b/packages/output/src/utils/getPreloadResources.ts similarity index 100% rename from packages/output/src/utils/getPreloadResources.js rename to packages/output/src/utils/getPreloadResources.ts diff --git a/packages/output/src/utils/getStoryMarkup.js b/packages/output/src/utils/getStoryMarkup.tsx similarity index 100% rename from packages/output/src/utils/getStoryMarkup.js rename to packages/output/src/utils/getStoryMarkup.tsx diff --git a/packages/output/src/utils/getTextElementTagNames.js b/packages/output/src/utils/getTextElementTagNames.ts similarity index 100% rename from packages/output/src/utils/getTextElementTagNames.js rename to packages/output/src/utils/getTextElementTagNames.ts diff --git a/packages/output/src/utils/getUsedAmpExtensions.js b/packages/output/src/utils/getUsedAmpExtensions.ts similarity index 100% rename from packages/output/src/utils/getUsedAmpExtensions.js rename to packages/output/src/utils/getUsedAmpExtensions.ts diff --git a/packages/output/src/utils/outlink.js b/packages/output/src/utils/outlink.tsx similarity index 100% rename from packages/output/src/utils/outlink.js rename to packages/output/src/utils/outlink.tsx diff --git a/packages/output/src/utils/shoppingAttachment.js b/packages/output/src/utils/shoppingAttachment.tsx similarity index 100% rename from packages/output/src/utils/shoppingAttachment.js rename to packages/output/src/utils/shoppingAttachment.tsx diff --git a/packages/output/src/utils/styles.js b/packages/output/src/utils/styles.tsx similarity index 100% rename from packages/output/src/utils/styles.js rename to packages/output/src/utils/styles.tsx diff --git a/packages/output/src/utils/test/fontDeclarations.js b/packages/output/src/utils/test/fontDeclarations.tsx similarity index 100% rename from packages/output/src/utils/test/fontDeclarations.js rename to packages/output/src/utils/test/fontDeclarations.tsx diff --git a/packages/output/src/utils/test/getAutoAdvanceAfter.js b/packages/output/src/utils/test/getAutoAdvanceAfter.ts similarity index 100% rename from packages/output/src/utils/test/getAutoAdvanceAfter.js rename to packages/output/src/utils/test/getAutoAdvanceAfter.ts diff --git a/packages/output/src/utils/test/getLongestMediaElement.js b/packages/output/src/utils/test/getLongestMediaElement.ts similarity index 100% rename from packages/output/src/utils/test/getLongestMediaElement.js rename to packages/output/src/utils/test/getLongestMediaElement.ts diff --git a/packages/output/src/utils/test/getPreloadResources.js b/packages/output/src/utils/test/getPreloadResources.ts similarity index 100% rename from packages/output/src/utils/test/getPreloadResources.js rename to packages/output/src/utils/test/getPreloadResources.ts diff --git a/packages/output/src/utils/test/getStoryMarkup.js b/packages/output/src/utils/test/getStoryMarkup.ts similarity index 100% rename from packages/output/src/utils/test/getStoryMarkup.js rename to packages/output/src/utils/test/getStoryMarkup.ts diff --git a/packages/output/src/utils/test/getTextElementTagNames.js b/packages/output/src/utils/test/getTextElementTagNames.ts similarity index 100% rename from packages/output/src/utils/test/getTextElementTagNames.js rename to packages/output/src/utils/test/getTextElementTagNames.ts diff --git a/packages/output/src/utils/test/getUsedAmpExtensions.js b/packages/output/src/utils/test/getUsedAmpExtensions.ts similarity index 100% rename from packages/output/src/utils/test/getUsedAmpExtensions.js rename to packages/output/src/utils/test/getUsedAmpExtensions.ts diff --git a/packages/output/src/utils/test/populateElementFontData.js b/packages/output/src/utils/test/populateElementFontData.ts similarity index 100% rename from packages/output/src/utils/test/populateElementFontData.js rename to packages/output/src/utils/test/populateElementFontData.ts From 64c9860a92352b63b5a26f0d4c29313e0d6c0c21 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Feb 2023 16:31:29 +0100 Subject: [PATCH 2/9] Convert test-utils package --- packages/test-utils/package.json | 3 ++- ...irePointerEvent.js => firePointerEvent.ts} | 8 ++++--- .../test-utils/src/{index.js => index.ts} | 0 ...ueryByAriaLabel.js => queryByAriaLabel.ts} | 10 ++++---- ...nceAfter.js => queryByAutoAdvanceAfter.ts} | 10 ++++---- .../src/{queryById.js => queryById.ts} | 8 +++---- ...renderWithTheme.js => renderWithTheme.tsx} | 15 +++++------- packages/test-utils/src/typings/svg.d.ts | 23 +++++++++++++++++++ packages/test-utils/tsconfig.json | 8 +++++++ tsconfig.json | 1 + 10 files changed, 59 insertions(+), 27 deletions(-) rename packages/test-utils/src/{firePointerEvent.js => firePointerEvent.ts} (73%) rename packages/test-utils/src/{index.js => index.ts} (100%) rename packages/test-utils/src/{queryByAriaLabel.js => queryByAriaLabel.ts} (74%) rename packages/test-utils/src/{queryByAutoAdvanceAfter.js => queryByAutoAdvanceAfter.ts} (75%) rename packages/test-utils/src/{queryById.js => queryById.ts} (72%) rename packages/test-utils/src/{renderWithTheme.js => renderWithTheme.tsx} (78%) create mode 100644 packages/test-utils/src/typings/svg.d.ts create mode 100644 packages/test-utils/tsconfig.json diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index bc87ab86c983..c01ef9696200 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -31,7 +31,8 @@ }, "main": "dist/index.js", "module": "dist-module/index.js", - "source": "src/index.js", + "types": "dist-types/index.d.ts", + "source": "src/index.ts", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/src/firePointerEvent.js b/packages/test-utils/src/firePointerEvent.ts similarity index 73% rename from packages/test-utils/src/firePointerEvent.js rename to packages/test-utils/src/firePointerEvent.ts index 0a2f7ee0c036..3d9dcc51092c 100644 --- a/packages/test-utils/src/firePointerEvent.js +++ b/packages/test-utils/src/firePointerEvent.ts @@ -19,7 +19,7 @@ */ import { fireEvent } from '@testing-library/react'; -function firePointerEvent(node, eventType, properties) { +function _firePointerEvent(node: Element | Node | Document | Window, eventType: string, properties: MouseEventInit) { fireEvent(node, new window.MouseEvent(eventType, properties)); } @@ -36,9 +36,11 @@ const pointerEventTypes = [ 'lostPointerCapture', ]; +const firePointerEvent: Record void> = {}; + pointerEventTypes.forEach((type) => { - firePointerEvent[type] = (node, properties) => - firePointerEvent(node, type.toLowerCase(), { + firePointerEvent[type] = (node: Element | Node | Document | Window, properties: MouseEventInit) => + _firePointerEvent(node, type.toLowerCase(), { bubbles: true, ...properties, }); diff --git a/packages/test-utils/src/index.js b/packages/test-utils/src/index.ts similarity index 100% rename from packages/test-utils/src/index.js rename to packages/test-utils/src/index.ts diff --git a/packages/test-utils/src/queryByAriaLabel.js b/packages/test-utils/src/queryByAriaLabel.ts similarity index 74% rename from packages/test-utils/src/queryByAriaLabel.js rename to packages/test-utils/src/queryByAriaLabel.ts index cb501f8e076d..5fd958ae1eb8 100644 --- a/packages/test-utils/src/queryByAriaLabel.js +++ b/packages/test-utils/src/queryByAriaLabel.ts @@ -17,14 +17,14 @@ /** * External dependencies */ -import { queryAllByAttribute, buildQueries } from '@testing-library/react'; +import { queryAllByAttribute, buildQueries, type GetErrorFunction, Matcher } from "@testing-library/react"; -const queryAllByAriaLabel = (...args) => - queryAllByAttribute('aria-label', ...args); +const queryAllByAriaLabel = (container: HTMLElement, id: Matcher) => + queryAllByAttribute('aria-label', container, id); -const getMultipleError = (c, value) => +const getMultipleError: GetErrorFunction = (_c: Element | null, value) => `Found multiple elements with the aria-label attribute of: ${value}`; -const getMissingError = (c, value) => +const getMissingError: GetErrorFunction = (_c: Element | null, value) => `Unable to find an element with the aria-label attribute of: ${value}`; const [ diff --git a/packages/test-utils/src/queryByAutoAdvanceAfter.js b/packages/test-utils/src/queryByAutoAdvanceAfter.ts similarity index 75% rename from packages/test-utils/src/queryByAutoAdvanceAfter.js rename to packages/test-utils/src/queryByAutoAdvanceAfter.ts index 3e87e14590e5..48ca3d20ef31 100644 --- a/packages/test-utils/src/queryByAutoAdvanceAfter.js +++ b/packages/test-utils/src/queryByAutoAdvanceAfter.ts @@ -17,14 +17,14 @@ /** * External dependencies */ -import { queryAllByAttribute, buildQueries } from '@testing-library/react'; +import { queryAllByAttribute, buildQueries, type GetErrorFunction, Matcher } from "@testing-library/react"; -const queryAllByAutoAdvanceAfter = (...args) => - queryAllByAttribute('auto-advance-after', ...args); +const queryAllByAutoAdvanceAfter = (container: HTMLElement, id: Matcher) => + queryAllByAttribute('auto-advance-after', container, id); -const getMultipleError = (c, value) => +const getMultipleError: GetErrorFunction = (_c: Element | null, value) => `Found multiple elements with the auto-advance-after attribute of: ${value}`; -const getMissingError = (c, value) => +const getMissingError: GetErrorFunction = (_c: Element | null, value) => `Unable to find an element with the auto-advance-after attribute of: ${value}`; const [ diff --git a/packages/test-utils/src/queryById.js b/packages/test-utils/src/queryById.ts similarity index 72% rename from packages/test-utils/src/queryById.js rename to packages/test-utils/src/queryById.ts index 7951ba12d2cf..3ec9936421c2 100644 --- a/packages/test-utils/src/queryById.js +++ b/packages/test-utils/src/queryById.ts @@ -17,13 +17,13 @@ /** * External dependencies */ -import { queryAllByAttribute, buildQueries } from '@testing-library/react'; +import { queryAllByAttribute, buildQueries, type GetErrorFunction, type Matcher } from "@testing-library/react"; -const queryAllById = (...args) => queryAllByAttribute('id', ...args); +const queryAllById = (container: HTMLElement, id: Matcher) => queryAllByAttribute('id', container, id); -const getMultipleError = (c, value) => +const getMultipleError: GetErrorFunction = (_c: Element | null, value) => `Found multiple elements with the id attribute of: ${value}`; -const getMissingError = (c, value) => +const getMissingError: GetErrorFunction = (_c: Element | null, value) => `Unable to find an element with the id attribute of: ${value}`; const [queryById, getAllById, getById, findAllById, findById] = buildQueries( diff --git a/packages/test-utils/src/renderWithTheme.js b/packages/test-utils/src/renderWithTheme.tsx similarity index 78% rename from packages/test-utils/src/renderWithTheme.js rename to packages/test-utils/src/renderWithTheme.tsx index 57c96669cc74..603859b9d43a 100644 --- a/packages/test-utils/src/renderWithTheme.js +++ b/packages/test-utils/src/renderWithTheme.tsx @@ -17,10 +17,10 @@ /** * External dependencies */ -import { render, queries } from '@testing-library/react'; +import { render, queries, RenderOptions } from '@testing-library/react'; import { ThemeProvider } from 'styled-components'; import { theme } from '@googleforcreators/design-system'; -import PropTypes from 'prop-types'; +import type { PropsWithChildren, ReactElement } from 'react'; /** * Internal dependencies @@ -29,16 +29,12 @@ import * as ariaLabelQueries from './queryByAriaLabel'; import * as autoAdvanceAfterQueries from './queryByAutoAdvanceAfter'; import * as idQueries from './queryById'; -const WithThemeProvider = ({ children }) => { +const WithThemeProvider = ({ children }: PropsWithChildren) => { return {children}; }; -WithThemeProvider.propTypes = { - children: PropTypes.element, -}; - -const renderWithTheme = (ui, options) => - render(ui, { +function renderWithTheme(ui: ReactElement, options: Partial>) { + return render(ui, { wrapper: WithThemeProvider, queries: { ...queries, @@ -48,5 +44,6 @@ const renderWithTheme = (ui, options) => }, ...options, }); +} export default renderWithTheme; diff --git a/packages/test-utils/src/typings/svg.d.ts b/packages/test-utils/src/typings/svg.d.ts new file mode 100644 index 000000000000..2b2af4b3434d --- /dev/null +++ b/packages/test-utils/src/typings/svg.d.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + * + * https://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. + */ + +declare module '*.svg' { + import type { FunctionComponent, SVGProps } from 'react'; + const ReactComponent: FunctionComponent< + SVGProps & { title?: string } + >; + export default ReactComponent; +} diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json new file mode 100644 index 000000000000..c9d87fc4143c --- /dev/null +++ b/packages/test-utils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "dist-types" + }, + "include": ["src/**/*"] +} diff --git a/tsconfig.json b/tsconfig.json index 1d0c27edfcf1..675cb01bd582 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ { "path": "packages/stickers" }, { "path": "packages/story-editor" }, { "path": "packages/templates" }, + { "path": "packages/test-utils" }, { "path": "packages/text-sets" }, { "path": "packages/tracking" }, { "path": "packages/transform" }, From 4e819e68d3c3c600e90156d58e624d8ba0d5a254 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Feb 2023 13:36:37 +0100 Subject: [PATCH 3/9] wip --- packages/animation/src/components/types.ts | 2 +- packages/element-library/src/types.ts | 8 + packages/elements/src/types/data.ts | 24 +- packages/elements/src/types/element.ts | 48 +- packages/elements/src/types/media.ts | 2 +- packages/elements/src/types/page.ts | 48 +- packages/elements/src/types/story.ts | 4 +- packages/elements/src/utils/elementIs.ts | 12 +- packages/elements/src/utils/getLayerName.ts | 9 +- packages/masks/src/output.tsx | 6 +- packages/masks/src/utils/elementBorder.ts | 16 +- packages/media/src/test/calculateSrcSet.ts | 18 +- .../media/src/test/getSmallestUrlForWidth.ts | 14 +- packages/media/src/types/audioResource.ts | 4 +- packages/output/package.json | 2 +- .../output/src/components/withLink/index.tsx | 24 +- .../src/components/withLink/test/output.tsx | 31 +- packages/output/src/element.tsx | 53 +- packages/output/src/page.tsx | 22 +- packages/output/src/story.tsx | 38 +- packages/output/src/test/_utils/constants.ts | 11 +- packages/output/src/test/page.tsx | 525 +++++++++++------- packages/output/src/test/story.tsx | 103 ++-- packages/output/src/test/textElement.tsx | 57 +- packages/output/src/types.ts | 14 +- packages/output/src/typings/global.d.ts | 120 ++++ packages/output/src/typings/jest.d.ts | 30 + packages/output/src/typings/svg.d.ts | 23 + packages/output/src/utils/ampBoilerplate.tsx | 2 +- packages/output/src/utils/backgroundAudio.tsx | 30 +- .../output/src/utils/fontDeclarations.tsx | 63 ++- .../output/src/utils/getAutoAdvanceAfter.ts | 18 +- .../src/utils/getLongestMediaElement.ts | 33 +- .../output/src/utils/getPreloadResources.ts | 31 +- packages/output/src/utils/getStoryMarkup.tsx | 19 +- .../src/utils/getTextElementTagNames.ts | 26 +- .../output/src/utils/getUsedAmpExtensions.ts | 43 +- packages/output/src/utils/outlink.tsx | 12 +- .../src/utils/populateElementFontData.ts | 1 + .../output/src/utils/shoppingAttachment.tsx | 21 +- .../src/utils/test/fontDeclarations.tsx | 178 +++--- .../src/utils/test/getAutoAdvanceAfter.ts | 136 ++++- .../src/utils/test/getLongestMediaElement.ts | 98 +++- .../src/utils/test/getPreloadResources.ts | 40 +- .../output/src/utils/test/getStoryMarkup.ts | 38 +- .../src/utils/test/getTextElementTagNames.ts | 28 +- .../src/utils/test/getUsedAmpExtensions.ts | 84 ++- .../src/utils/test/populateElementFontData.ts | 102 +++- packages/output/tsconfig.json | 4 +- packages/rich-text/src/formatters/util.ts | 22 +- packages/rich-text/src/getFontVariants.ts | 12 +- packages/test-utils/src/firePointerEvent.ts | 16 +- packages/test-utils/src/queryByAriaLabel.ts | 7 +- .../test-utils/src/queryByAutoAdvanceAfter.ts | 7 +- packages/test-utils/src/queryById.ts | 10 +- packages/test-utils/src/renderWithTheme.tsx | 7 +- 56 files changed, 1580 insertions(+), 776 deletions(-) create mode 100644 packages/output/src/typings/global.d.ts create mode 100644 packages/output/src/typings/jest.d.ts create mode 100644 packages/output/src/typings/svg.d.ts diff --git a/packages/animation/src/components/types.ts b/packages/animation/src/components/types.ts index 2b28f098e539..7b725a4d1b7c 100644 --- a/packages/animation/src/components/types.ts +++ b/packages/animation/src/components/types.ts @@ -61,7 +61,7 @@ export interface AnimationProviderState { } export type AnimationProviderProps = PropsWithChildren<{ - animations: StoryAnimation[]; + animations?: StoryAnimation[]; elements?: Element[]; onWAAPIFinish?: () => void; selectedElementIds?: string[]; diff --git a/packages/element-library/src/types.ts b/packages/element-library/src/types.ts index 05ab4772590f..4f017bdbe28f 100644 --- a/packages/element-library/src/types.ts +++ b/packages/element-library/src/types.ts @@ -18,7 +18,15 @@ // Adjust tsconfig.json and "types" field in package.json and then // delete this file once complete. +/** + * External dependencies + */ +import type { ElementDefinition } from '@googleforcreators/elements'; + export * from './constants'; +export * from './types'; export * from './utils/textMeasurements'; +export const elementTypes: ElementDefinition[] = []; + export {}; diff --git a/packages/elements/src/types/data.ts b/packages/elements/src/types/data.ts index 254651885bc4..8f1a0fe9910f 100644 --- a/packages/elements/src/types/data.ts +++ b/packages/elements/src/types/data.ts @@ -25,7 +25,10 @@ import type { AudioResource } from '@googleforcreators/media'; */ import type { Page } from './page'; +export type FontFamily = string; + export type FontStyle = 'normal' | 'italic' | 'regular'; + export enum FontVariantStyle { Normal = 0, Italic = 1, @@ -35,6 +38,12 @@ export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; export type FontVariant = [FontVariantStyle, FontWeight]; +export enum FontService { + Custom = 'custom', + GoogleFonts = 'fonts.google.com', + System = 'system', +} + export interface FontMetrics { upm: number; asc: number; @@ -54,7 +63,8 @@ export interface FontMetrics { } export interface BaseFontData { - family: string; + service: FontService; + family: FontFamily; weights?: FontWeight[]; styles?: FontStyle[]; variants?: FontVariant[]; @@ -63,15 +73,15 @@ export interface BaseFontData { } export interface GoogleFontData extends BaseFontData { - service: 'fonts.google.com'; + service: FontService.GoogleFonts; } export interface SystemFontData extends BaseFontData { - service: 'system'; + service: FontService.System; } export interface CustomFontData extends BaseFontData { - service: 'custom'; + service: FontService.Custom; url: string; } @@ -91,6 +101,12 @@ export interface ProductData { productPriceCurrency: string; productTitle: string; productUrl: string; + productIcon?: string; + aggregateRating?: { + ratingValue: number; + reviewCount: number; + reviewUrl: string; + }; } // Data retrieved as part of the raw data from the backend, used for example in the templates, in migration. diff --git a/packages/elements/src/types/element.ts b/packages/elements/src/types/element.ts index d6caaf6509bf..cb5a9113a7dd 100644 --- a/packages/elements/src/types/element.ts +++ b/packages/elements/src/types/element.ts @@ -17,11 +17,11 @@ /** * External dependencies */ -import type { Solid } from '@googleforcreators/patterns'; +import type { Solid, Pattern } from '@googleforcreators/patterns'; import type { - GifResource, Resource, SequenceResource, + GifResource, VideoResource, } from '@googleforcreators/media'; import type { ElementBox } from '@googleforcreators/units'; @@ -30,8 +30,13 @@ import type { ElementBox } from '@googleforcreators/units'; * Internal dependencies */ import type { ElementType } from './elementType'; -import type { FontMetrics, ProductData } from './data'; import type { Track } from './media'; +import type { + ProductData, + GoogleFontData, + SystemFontData, + CustomFontData, +} from './data'; export enum LinkType { Regular = 'regular', @@ -101,6 +106,10 @@ export interface Element extends ElementBox { isHidden?: boolean; } +export interface LinkableElement extends Element { + link: Link; +} + export interface DefaultBackgroundElement extends Element { isDefaultBackground: boolean; backgroundColor: Solid; @@ -110,10 +119,6 @@ export interface BackgroundableElement extends Element { isBackground?: boolean; } -export interface LinkableElement extends Element { - link: Link; -} - export interface MediaElement extends BackgroundableElement { resource: Resource; scale?: number; @@ -136,6 +141,10 @@ export interface GifElement extends SequenceMediaElement { resource: GifResource; } +export interface OverlayableElement extends Element { + overlay?: Pattern | null; +} + export interface ProductElement extends Element { type: ElementType.Product; product: ProductData; @@ -148,30 +157,7 @@ export interface StickerElement extends Element { }; } -interface BaseTextElementFont { - service: string; - family: string; - fallbacks: string[]; - metrics?: FontMetrics; -} - -export interface GoogleTextElementFont extends BaseTextElementFont { - service: 'fonts.google.com'; -} - -export interface SystemTextElementFont extends BaseTextElementFont { - service: 'system'; -} - -export interface CustomTextElementFont extends BaseTextElementFont { - service: 'custom'; - url: string; -} - -export type TextElementFont = - | GoogleTextElementFont - | SystemTextElementFont - | CustomTextElementFont; +export type TextElementFont = GoogleFontData | SystemFontData | CustomFontData; export interface Padding { horizontal: number; diff --git a/packages/elements/src/types/media.ts b/packages/elements/src/types/media.ts index 71e121b7113d..ab8aa8ac6348 100644 --- a/packages/elements/src/types/media.ts +++ b/packages/elements/src/types/media.ts @@ -19,7 +19,7 @@ export type Track = { trackId: number; trackName: string; id: string; - srcLang?: string; + srclang?: string; label?: string; kind: string; }; diff --git a/packages/elements/src/types/page.ts b/packages/elements/src/types/page.ts index bd32633dcfe9..732afc267d6d 100644 --- a/packages/elements/src/types/page.ts +++ b/packages/elements/src/types/page.ts @@ -19,6 +19,7 @@ */ import type { Pattern } from '@googleforcreators/patterns'; import type { StoryAnimation } from '@googleforcreators/animation'; +import type { AudioResource } from '@googleforcreators/media'; /** * Internal dependencies @@ -32,8 +33,28 @@ export interface Group { isCollapsed?: boolean; } +export type BackgroundAudio = { + resource: AudioResource; + tracks?: Track[]; + loop?: boolean; +}; + export type Groups = Record; +export type PageAttachment = { + url: string; + ctaText?: string; + theme?: 'light' | 'dark'; + icon?: string; + rel?: string[]; + needsProxy?: boolean; +}; + +export type ShoppingAttachment = { + ctaText: string; + theme?: 'light' | 'dark'; +}; + export interface Page { id: ElementId; elements: Element[]; @@ -41,24 +62,11 @@ export interface Page { animations?: StoryAnimation[]; backgroundColor: Pattern; groups?: Groups; - backgroundAudio?: { - resource: { - src: string; - id: number; - mimeType: string; - }; - tracks: Track[]; - loop: boolean; - }; - autoAdvance?: boolean; - defaultPageDuration?: number; - pageAttachment?: { - url: string; - ctaText: string; - theme: string; - }; - shoppingAttachment?: { - ctaText: string; - theme: string; - }; + backgroundAudio?: BackgroundAudio; + pageAttachment?: PageAttachment; + shoppingAttachment?: ShoppingAttachment; + advancement?: { + autoAdvance?: boolean; + pageDuration?: number; + } } diff --git a/packages/elements/src/types/story.ts b/packages/elements/src/types/story.ts index 7800c995c22a..348d11a2e7d5 100644 --- a/packages/elements/src/types/story.ts +++ b/packages/elements/src/types/story.ts @@ -31,8 +31,8 @@ interface FeaturedMedia { height: number; width: number; url: string; - needsProxy: boolean; - isExternal: boolean; + needsProxy?: boolean; + isExternal?: boolean; } interface PublisherLogo { id: number; diff --git a/packages/elements/src/utils/elementIs.ts b/packages/elements/src/utils/elementIs.ts index f40396117f58..fa253b755f79 100644 --- a/packages/elements/src/utils/elementIs.ts +++ b/packages/elements/src/utils/elementIs.ts @@ -26,14 +26,15 @@ import type { BackgroundableElement, DefaultBackgroundElement, Element, - GifElement, - LinkableElement, MediaElement, ProductElement, SequenceMediaElement, StickerElement, TextElement, VideoElement, + GifElement, + LinkableElement, + OverlayableElement, } from '../types'; import { ElementType } from '../types'; @@ -110,6 +111,12 @@ function isLinkable(e: Element): e is LinkableElement { ); } +function isOverlayable(e: Element): e is OverlayableElement; +function isOverlayable(e: Draft): e is Draft; +function isOverlayable(e: Element): e is OverlayableElement { + return 'overlay' in e && typeof e.overlay !== 'undefined'; +} + const elementIs = { media: isMediaElement, text: isTextElement, @@ -121,6 +128,7 @@ const elementIs = { video: isVideo, gif: isGif, linkable: isLinkable, + overlayable: isOverlayable, }; export default elementIs; diff --git a/packages/elements/src/utils/getLayerName.ts b/packages/elements/src/utils/getLayerName.ts index 905842f88d0a..ac5d3884825a 100644 --- a/packages/elements/src/utils/getLayerName.ts +++ b/packages/elements/src/utils/getLayerName.ts @@ -22,12 +22,9 @@ import { __ } from '@googleforcreators/i18n'; /** * Internal dependencies */ -import type { BackgroundableElement, Element } from '../types'; +import type { Element } from '../types'; import getDefinitionForType from './getDefinitionForType'; - -function isBackgroundableElement(e: Element): e is BackgroundableElement { - return 'isBackground' in e; -} +import elementIs from './elementIs'; /** * Returns the layer name based on the element properties. @@ -40,7 +37,7 @@ function getLayerName(element: Element) { return element.layerName; } - if (isBackgroundableElement(element) && element.isBackground) { + if (elementIs.backgroundable(element) && element.isBackground) { return __('Background', 'web-stories'); } diff --git a/packages/masks/src/output.tsx b/packages/masks/src/output.tsx index c71ccf55dd5c..c7ab94d3dfb2 100644 --- a/packages/masks/src/output.tsx +++ b/packages/masks/src/output.tsx @@ -34,14 +34,14 @@ import { DEFAULT_MASK } from './constants'; interface WithMaskProps { element: Element; style: CSSProperties; - fill: boolean; + fill?: boolean; children: ReactNode; - skipDefaultMask: boolean; + skipDefaultMask?: boolean; } export default function WithMask({ element, - fill, + fill = false, skipDefaultMask = false, ...rest }: WithMaskProps) { diff --git a/packages/masks/src/utils/elementBorder.ts b/packages/masks/src/utils/elementBorder.ts index 02b8a35e4fd7..414bd5b57d0d 100644 --- a/packages/masks/src/utils/elementBorder.ts +++ b/packages/masks/src/utils/elementBorder.ts @@ -29,7 +29,13 @@ import type { CSSProperties } from 'react'; */ import { canMaskHaveBorder, canSupportMultiBorder } from '../masks'; -function hasBorder({ border }: Element) { +interface ElementWithBorder extends Element { + border: Border; +} + +function hasBorder(element: Element): element is ElementWithBorder { + const { border } = element; + if (!border) { return false; } @@ -48,7 +54,9 @@ function hasBorder({ border }: Element) { * @param element Element object. * @return If should be displayed. */ -export function shouldDisplayBorder(element: Element) { +export function shouldDisplayBorder( + element: Element +): element is ElementWithBorder { return ( hasBorder(element) && canMaskHaveBorder(element) && @@ -63,7 +71,7 @@ interface SizeAndPosition { posLeft: string; } -type BorderPositionProps = Border & SizeAndPosition; +type BorderPositionProps = Border & Partial; /** * Gets the CSS values for an element with border. @@ -109,7 +117,7 @@ export function getBorderStyle(element: Element): CSSProperties { return getBorderRadius(element); } const { border } = element; - const { left, top, right, bottom } = border as Border; + const { left, top, right, bottom } = border; // We're making the border-width responsive just for the preview, // since the calculation is not 100% precise here, we're opting to the safe side by rounding the widths up diff --git a/packages/media/src/test/calculateSrcSet.ts b/packages/media/src/test/calculateSrcSet.ts index 22fe0d370426..9e919afe4801 100644 --- a/packages/media/src/test/calculateSrcSet.ts +++ b/packages/media/src/test/calculateSrcSet.ts @@ -19,7 +19,7 @@ */ import calculateSrcSet from '../calculateSrcSet'; import createResource from '../createResource'; -import { ResourceType } from '../types'; +import { type ImageResource, ResourceType } from '../types'; describe('calculateSrcSet', () => { it('should generate srcset properly', () => { @@ -45,7 +45,7 @@ describe('calculateSrcSet', () => { height: 800, }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe('URL2 400w,URL1 200w'); @@ -80,7 +80,7 @@ describe('calculateSrcSet', () => { height: 410, }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe('URL2 400w,URL1 200w'); @@ -121,7 +121,7 @@ describe('calculateSrcSet', () => { height: 1600, }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe('URL3 800w,URL2 400w,URL1 200w'); @@ -156,7 +156,7 @@ describe('calculateSrcSet', () => { sourceUrl: 'large url', }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe('large%20url 300w,medium%20url 200w,small%20url 100w'); @@ -192,7 +192,7 @@ describe('calculateSrcSet', () => { sourceUrl: 'large%2Furl', }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe( @@ -229,7 +229,7 @@ describe('calculateSrcSet', () => { sourceUrl: 'large url', }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe( @@ -266,7 +266,7 @@ describe('calculateSrcSet', () => { sourceUrl: '', }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe(''); @@ -304,7 +304,7 @@ describe('calculateSrcSet', () => { 'https://example.com/images/w_640,h_853,c_scale/image.jpg?_i=AA', }, }, - }); + }) as ImageResource; const srcSet = calculateSrcSet(resource); expect(srcSet).toBe( diff --git a/packages/media/src/test/getSmallestUrlForWidth.ts b/packages/media/src/test/getSmallestUrlForWidth.ts index 96ad13a58d13..6fee2e79b2e4 100644 --- a/packages/media/src/test/getSmallestUrlForWidth.ts +++ b/packages/media/src/test/getSmallestUrlForWidth.ts @@ -19,7 +19,7 @@ */ import getSmallestUrlForWidth from '../getSmallestUrlForWidth'; import createResource from '../createResource'; -import { ResourceType } from '../types'; +import { type ImageResource, ResourceType } from '../types'; describe('getSmallestUrlForWidth', () => { beforeEach(() => { @@ -59,7 +59,7 @@ describe('getSmallestUrlForWidth', () => { sourceUrl: 'large-url', }, }, - }); + }) as ImageResource; expect(getSmallestUrlForWidth(210, resource)).toBe('med-url'); }); @@ -93,12 +93,12 @@ describe('getSmallestUrlForWidth', () => { sourceUrl: 'large-url', }, }, - }); + }) as ImageResource; expect(getSmallestUrlForWidth(160, resource)).toBe('large-url'); }); it('should return an image with the same aspect ratio', () => { - const resource = createResource({ + const resource: ImageResource = createResource({ id: 123, type: ResourceType.Image, mimeType: 'image/jpeg', @@ -132,7 +132,7 @@ describe('getSmallestUrlForWidth', () => { sourceUrl: 'large-url', }, }, - }); + }) as ImageResource; expect(getSmallestUrlForWidth(150, resource)).toBe('med-url'); }); @@ -165,7 +165,7 @@ describe('getSmallestUrlForWidth', () => { sourceUrl: 'large-url', }, }, - }); + }) as ImageResource; expect(getSmallestUrlForWidth(440, resource)).toBe('default-url'); }); @@ -179,7 +179,7 @@ describe('getSmallestUrlForWidth', () => { width: 400, height: 200, sizes: {}, - }); + }) as ImageResource; expect(getSmallestUrlForWidth(200, resource)).toBe('default-url'); }); }); diff --git a/packages/media/src/types/audioResource.ts b/packages/media/src/types/audioResource.ts index d7ed404163a6..899cf448c2d9 100644 --- a/packages/media/src/types/audioResource.ts +++ b/packages/media/src/types/audioResource.ts @@ -17,11 +17,9 @@ /** * Internal dependencies */ -import type { ResourceType } from './resourceType'; import type { Resource } from './resource'; -export interface AudioResource extends Resource { - type: ResourceType.Audio; +export interface AudioResource extends Pick { /** Length in seconds. */ length: number; /** The formatted length, e.g. "01:17". */ diff --git a/packages/output/package.json b/packages/output/package.json index 31d9e7b8484c..b24feda00aa3 100644 --- a/packages/output/package.json +++ b/packages/output/package.json @@ -31,7 +31,7 @@ }, "main": "dist/index.js", "module": "dist-module/index.js", - "types": "dist-types/types.d.ts", + "types": "dist-types/index.d.ts", "source": "src/index.js", "publishConfig": { "access": "public" diff --git a/packages/output/src/components/withLink/index.tsx b/packages/output/src/components/withLink/index.tsx index 512a7e5b3e92..3c31acaf7ff5 100644 --- a/packages/output/src/components/withLink/index.tsx +++ b/packages/output/src/components/withLink/index.tsx @@ -17,11 +17,21 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; import { withProtocol } from '@googleforcreators/url'; -import { LinkType, StoryPropTypes } from '@googleforcreators/elements'; +import { LinkType } from '@googleforcreators/elements'; +import { type Element, elementIs } from '@googleforcreators/elements'; +import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react'; + +type WithLinkProps = PropsWithChildren<{ + element: Element; +}> & + HTMLAttributes; + +function WithLink({ element, children, ...rest }: WithLinkProps) { + if (!elementIs.linkable(element)) { + return children as unknown as ReactElement; + } -function WithLink({ element, children, ...rest }) { const link = element.link || {}; const { url, icon, desc, rel = [], type, pageId } = link; @@ -39,8 +49,9 @@ function WithLink({ element, children, ...rest }) { } if (!url) { - return children; + return children as unknown as ReactElement; } + const clonedRel = rel.concat(['noreferrer']); const urlWithProtocol = withProtocol(url); return ( @@ -58,9 +69,4 @@ function WithLink({ element, children, ...rest }) { ); } -WithLink.propTypes = { - element: StoryPropTypes.element.isRequired, - children: PropTypes.node.isRequired, -}; - export default WithLink; diff --git a/packages/output/src/components/withLink/test/output.tsx b/packages/output/src/components/withLink/test/output.tsx index 3e53aba5b9e6..df1849f58082 100644 --- a/packages/output/src/components/withLink/test/output.tsx +++ b/packages/output/src/components/withLink/test/output.tsx @@ -18,6 +18,7 @@ * External dependencies */ import { render, screen } from '@testing-library/react'; +import { ElementType, type Link } from '@googleforcreators/elements'; /** * Internal dependencies @@ -25,11 +26,11 @@ import { render, screen } from '@testing-library/react'; import WithLink from '..'; describe('WithLink', () => { - function withLink(linkProps) { + function withLink(linkProps: Partial = {}) { const props = { element: { id: '123', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', scale: 1, x: 50, @@ -60,33 +61,33 @@ describe('WithLink', () => { } describe('a[target]', () => { - it('should use target=_blank', async () => { + it('should use target=_blank', () => { render(withLink()); - const a = screen.getByRole('link'); - await expect(a.target).toBe('_blank'); - await expect(a.rel).toBe('noreferrer'); + const a = screen.getByRole('link'); + expect(a.target).toBe('_blank'); + expect(a.rel).toBe('noreferrer'); }); }); describe('a[rel]', () => { - it('should use rel=noreferrer', async () => { + it('should use rel=noreferrer', () => { render(withLink()); - const a = screen.getByRole('link'); - await expect(a.rel).toBe('noreferrer'); + const a = screen.getByRole('link'); + expect(a.rel).toBe('noreferrer'); }); - it('should use rel=noreferrer nofollow', async () => { + it('should use rel=noreferrer nofollow', () => { render(withLink({ rel: ['nofollow'] })); - const a = screen.getByRole('link'); - await expect(a.rel).toBe('nofollow noreferrer'); + const a = screen.getByRole('link'); + expect(a.rel).toBe('nofollow noreferrer'); }); }); describe('a[data-tooltip-icon]', () => { - it('should not add data-tooltip-icon attribute if there is no icon', async () => { + it('should not add data-tooltip-icon attribute if there is no icon', () => { render(withLink({ icon: '' })); - const a = screen.getByRole('link'); - await expect(a).not.toHaveAttribute('data-tooltip-icon'); + const a = screen.getByRole('link'); + expect(a).not.toHaveAttribute('data-tooltip-icon'); }); }); diff --git a/packages/output/src/element.tsx b/packages/output/src/element.tsx index 9a35f6150e64..af2fb05a065a 100644 --- a/packages/output/src/element.tsx +++ b/packages/output/src/element.tsx @@ -17,14 +17,14 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; import { generatePatternStyles } from '@googleforcreators/patterns'; import { getBox } from '@googleforcreators/units'; import { AMPWrapper } from '@googleforcreators/animation'; import { BACKGROUND_TEXT_MODE, getDefinitionForType, - StoryPropTypes, + type Element, + elementIs, } from '@googleforcreators/elements'; import { OutputWithMask as WithMask, @@ -39,17 +39,13 @@ import { */ import WithLink from './components/withLink'; -function OutputElement({ element, flags }) { - const { - id, - opacity, - type, - border, - backgroundColor, - backgroundTextMode, - overlay, - isHidden, - } = element; +interface OutputElementProps { + element: Element; + flags: Record; +} + +function OutputElement({ element, flags }: OutputElementProps) { + const { id, opacity, type, isHidden } = element; if (isHidden) { return null; @@ -61,6 +57,16 @@ function OutputElement({ element, flags }) { const box = getBox(element, 100, 100); const { x, y, width, height, rotationAngle } = box; + const backgroundColor = elementIs.text(element) + ? element.backgroundColor + : null; + + const isFillBackground = + elementIs.text(element) && + element.backgroundTextMode === BACKGROUND_TEXT_MODE.FILL; + + const isOverlayable = elementIs.overlayable(element); + // We're adding background styles in case of Fill here so that // the background and the border would match together. const bgStyles = { @@ -79,22 +85,22 @@ function OutputElement({ element, flags }) { height: `${height}%`, ...(shouldDisplayBorder(element) ? getBorderPositionCSS({ - ...border, + ...element.border, width: `${width}%`, height: `${height}%`, posTop: `${y}%`, posLeft: `${x}%`, }) : null), - transform: rotationAngle ? `rotate(${rotationAngle}deg)` : null, - opacity: typeof opacity !== 'undefined' ? opacity / 100 : null, + transform: rotationAngle ? `rotate(${rotationAngle}deg)` : undefined, + opacity: typeof opacity !== 'undefined' ? opacity / 100 : undefined, }} > @@ -125,10 +129,10 @@ function OutputElement({ element, flags }) { > - {overlay && ( + {isOverlayable && element.overlay && (
)} @@ -137,9 +141,4 @@ function OutputElement({ element, flags }) { ); } -OutputElement.propTypes = { - element: StoryPropTypes.element.isRequired, - flags: PropTypes.object, -}; - export default OutputElement; diff --git a/packages/output/src/page.tsx b/packages/output/src/page.tsx index 1b8d25f22969..ed83f527e4f6 100644 --- a/packages/output/src/page.tsx +++ b/packages/output/src/page.tsx @@ -22,9 +22,10 @@ import { generatePatternStyles } from '@googleforcreators/patterns'; import { PAGE_HEIGHT, PAGE_WIDTH } from '@googleforcreators/units'; import { AnimationProvider, AMPAnimations } from '@googleforcreators/animation'; import { - ELEMENT_TYPES, StoryPropTypes, isElementBelowLimit, + type Page, + elementIs, } from '@googleforcreators/elements'; /** @@ -40,12 +41,19 @@ import ShoppingAttachment from './utils/shoppingAttachment'; const ASPECT_RATIO = `${PAGE_WIDTH}:${PAGE_HEIGHT}`; +interface OutputPageProps { + page: Page; + defaultAutoAdvance?: boolean; + defaultPageDuration?: number; + flags: Record; +} + function OutputPage({ page, defaultAutoAdvance = DEFAULT_AUTO_ADVANCE, defaultPageDuration = DEFAULT_PAGE_DURATION, flags, -}) { +}: OutputPageProps) { const { id, animations, @@ -53,7 +61,7 @@ function OutputPage({ elements, backgroundColor, backgroundAudio, - pageAttachment = {}, + pageAttachment = null, shoppingAttachment = {}, } = page; @@ -111,7 +119,8 @@ function OutputPage({ }); const products = elements - .filter(({ type, isHidden }) => type === ELEMENT_TYPES.PRODUCT && !isHidden) + .filter(elementIs.product) + .filter(({ isHidden }) => !isHidden) .map(({ product }) => product) .filter(Boolean); @@ -119,9 +128,8 @@ function OutputPage({ const hasPageAttachment = pageAttachment?.url && !hasProducts; const videoCaptions = elements - .filter( - ({ type, tracks }) => type === ELEMENT_TYPES.VIDEO && tracks?.length > 0 - ) + .filter(elementIs.video) + .filter(({ tracks }) => tracks?.length > 0) .map(({ id: videoId }) => `el-${videoId}-captions`); const backgroundAudioSrc = backgroundAudio?.resource?.src; diff --git a/packages/output/src/story.tsx b/packages/output/src/story.tsx index 93e49635ceb9..ce169144da19 100644 --- a/packages/output/src/story.tsx +++ b/packages/output/src/story.tsx @@ -17,9 +17,7 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; -import { BackgroundAudioPropType } from '@googleforcreators/media'; -import { StoryPropTypes } from '@googleforcreators/elements'; +import type { Page } from '@googleforcreators/elements'; /** * Internal dependencies @@ -31,6 +29,14 @@ import FontDeclarations from './utils/fontDeclarations'; import OutputPage from './page'; import getPreloadResources from './utils/getPreloadResources'; import { populateElementFontData } from './utils/populateElementFontData'; +import type { StoryMetadata, Story } from './types'; + +interface OutputStoryProps { + story: Story; + pages: Page[]; + flags: Record; + metadata: StoryMetadata; +} function OutputStory({ story: { @@ -46,7 +52,7 @@ function OutputStory({ pages, metadata: { publisher }, flags, -}) { +}: OutputStoryProps) { const ampExtensions = getUsedAmpExtensions(pages); const preloadResources = getPreloadResources(pages); @@ -105,28 +111,4 @@ function OutputStory({ ); } -OutputStory.propTypes = { - story: PropTypes.shape({ - fonts: PropTypes.object, - link: PropTypes.string, - title: PropTypes.string.isRequired, - autoAdvance: PropTypes.bool, - defaultPageDuration: PropTypes.number, - backgroundAudio: PropTypes.shape({ - resource: BackgroundAudioPropType, - }), - publisherLogo: PropTypes.shape({ - url: PropTypes.string.isRequired, - }), - featuredMedia: PropTypes.shape({ - url: PropTypes.string.isRequired, - }), - }).isRequired, - pages: PropTypes.arrayOf(StoryPropTypes.page).isRequired, - metadata: PropTypes.shape({ - publisher: PropTypes.string.isRequired, - }).isRequired, - flags: PropTypes.object, -}; - export default OutputStory; diff --git a/packages/output/src/test/_utils/constants.ts b/packages/output/src/test/_utils/constants.ts index 5823de044a80..eb2e7de6f035 100644 --- a/packages/output/src/test/_utils/constants.ts +++ b/packages/output/src/test/_utils/constants.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * External dependencies + */ +import { ElementType, FontService } from '@googleforcreators/elements'; + export const DEFAULT_TEXT = { opacity: 100, flip: { @@ -42,7 +47,7 @@ export const DEFAULT_TEXT = { [1, 900], ], fallbacks: ['Helvetica Neue', 'Helvetica', 'sans-serif'], - service: 'fonts.google.com', + service: FontService.GoogleFonts, metrics: { upm: 2048, asc: 1900, @@ -70,13 +75,13 @@ export const DEFAULT_TEXT = { }, }, lineHeight: 1.5, - textAlign: 'initial', + textAlign: 'center', padding: { vertical: 0, horizontal: 0, locked: true, }, - type: 'text', + type: ElementType.Text, content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', x: 40, y: 300, diff --git a/packages/output/src/test/page.tsx b/packages/output/src/test/page.tsx index 6fde6c269dd3..3db0fdbd0beb 100644 --- a/packages/output/src/test/page.tsx +++ b/packages/output/src/test/page.tsx @@ -19,25 +19,34 @@ */ import { renderToStaticMarkup } from '@googleforcreators/react'; import { render } from '@testing-library/react'; -import { PAGE_WIDTH, PAGE_HEIGHT } from '@googleforcreators/units'; +import { PAGE_HEIGHT, PAGE_WIDTH } from '@googleforcreators/units'; import { MaskTypes } from '@googleforcreators/masks'; -import { registerElementType } from '@googleforcreators/elements'; +import { + BackgroundableElement, + ElementType, + MediaElement, + PageAttachment, + ProductData, + registerElementType, +} from '@googleforcreators/elements'; import { elementTypes } from '@googleforcreators/element-library'; import { - queryByAutoAdvanceAfter, getByAutoAdvanceAfter, - queryById, getById, + queryByAutoAdvanceAfter, + queryById, } from '@googleforcreators/test-utils'; /** * Internal dependencies */ +import { AnimationType, StoryAnimation } from '@googleforcreators/animation'; +import { Resource, ResourceType } from '@googleforcreators/media'; import PageOutput from '../page'; jest.mock('flagged'); -const PRODUCT_LAMP = { +const PRODUCT_LAMP: ProductData = { productUrl: 'https://www.google.com', productId: 'lamp', productTitle: 'Brass Lamp', @@ -61,7 +70,7 @@ const PRODUCT_LAMP = { 'One newline after this. \n Two newlines after this. \n\n Five consecutive newlines after this, should become 2 newlines. \n\n\n\n\n Many consecutive newlines with different spacing and tabs after this, should become 2 newlines. \n \n\n \n \n \n \n \n I hope it works!', }; -const PRODUCT_ART = { +const PRODUCT_ART: ProductData = { productUrl: 'https://www.google.com', productId: 'art', productTitle: 'Abstract Art', @@ -76,15 +85,16 @@ const PRODUCT_ART = { reviewCount: 89, reviewUrl: 'https://www.google.com', }, + productDetails: 'Some short text', }; -const PRODUCT_CHAIR = { +const PRODUCT_CHAIR: ProductData = { productUrl: 'https://www.google.com', productId: 'chair', productTitle: 'Yellow chair', + productBrand: 'The Chair Company', productPrice: 1000.0, productPriceCurrency: 'BRL', - productText: 'The perfectly imperfect yellow chair', productImages: [ { url: '/examples/visual-tests/amp-story/img/cat1.jpg', alt: 'chair' }, ], @@ -97,7 +107,7 @@ const PRODUCT_CHAIR = { 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere error deserunt dignissimos in laborum ea molestias veritatis sint laudantium iusto expedita atque provident doloremque, ad voluptatem culpa adipisci.', }; -const PRODUCT_FLOWERS = { +const PRODUCT_FLOWERS: ProductData = { productUrl: 'https://www.google.com', productId: 'flowers', productTitle: 'Flowers', @@ -117,39 +127,36 @@ const PRODUCT_FLOWERS = { 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere error deserunt dignissimos in laborum ea molestias veritatis sint laudantium iusto expedita atque provident doloremque, ad voluptatem culpa adipisci.', }; -/* eslint-disable testing-library/no-node-access, testing-library/no-container */ - describe('Page output', () => { beforeAll(() => { elementTypes.forEach(registerElementType); }); describe('aspect-ratio markup', () => { - let backgroundElement; + let backgroundElement: BackgroundableElement & MediaElement; beforeEach(() => { backgroundElement = { isBackground: true, id: 'baz', - type: 'image', - mimeType: 'image/png', + type: ElementType.Image, scale: 1, - origRatio: 9 / 16, x: 50, y: 100, height: 1920, width: 1080, rotationAngle: 0, - loop: true, resource: { - type: 'image', + type: ResourceType.Image, mimeType: 'image/png', id: 123, src: 'https://example.com/image.png', poster: 'https://example.com/poster.png', height: 1920, width: 1080, - }, + alt: '', + isExternal: false, + } as Resource, }; }); @@ -159,13 +166,14 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const layers = container.querySelectorAll('amp-story-grid-layer'); expect(layers).toHaveLength(1); const layer = layers[0]; @@ -188,13 +196,14 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [backgroundElement], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const layers = container.querySelectorAll('amp-story-grid-layer'); expect(layers).toHaveLength(2); const bgLayer = layers[0]; @@ -217,6 +226,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { ...backgroundElement, @@ -228,7 +238,7 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const overlayLayer = container.querySelector( '.page-background-overlay-area' ); @@ -240,22 +250,28 @@ describe('Page output', () => { it('should render animation tags for animations', () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, animations: [ { id: '123', targets: ['123', '124'], - type: 'bounce', + type: AnimationType.Bounce, duration: 1000, }, - { id: '124', targets: ['123'], type: 'spin', duration: 1000 }, - ], + { + id: '124', + targets: ['123'], + type: AnimationType.Spin, + duration: 1000, + }, + ] as StoryAnimation[], elements: [ { id: '123', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -278,7 +294,7 @@ describe('Page output', () => { }, { id: '124', - type: 'shape', + type: ElementType.Shape, opacity: 100, flip: { vertical: false, @@ -310,7 +326,7 @@ describe('Page output', () => { defaultPageDuration: 11, }; - const { container } = render(); + const { container } = render(); const storyAnimations = container.querySelectorAll('amp-story-animation'); expect(storyAnimations).toHaveLength(3); @@ -319,46 +335,47 @@ describe('Page output', () => { }); describe('page advancement', () => { - it('should use default value for auto-advance-after', async () => { + it('should use default value for auto-advance-after', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); - await expect( - queryByAutoAdvanceAfter(container, '7s') - ).not.toBeInTheDocument(); + const { container } = render(); + expect(queryByAutoAdvanceAfter(container, '7s')).not.toBeInTheDocument(); }); - it('should use default duration for auto-advance-after', async () => { + it('should use default duration for auto-advance-after', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], }, defaultAutoAdvance: true, defaultPageDuration: 7, }; - const { container } = render(); - await expect(getByAutoAdvanceAfter(container, '7s')).toBeInTheDocument(); + const { container } = render(); + expect(getByAutoAdvanceAfter(container, '7s')).toBeInTheDocument(); }); - it('should use custom value for auto-advance-after', async () => { + it('should use custom value for auto-advance-after', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, advancement: { autoAdvance: false }, elements: [], }, @@ -366,18 +383,17 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); - await expect( - queryByAutoAdvanceAfter(container, '7s') - ).not.toBeInTheDocument(); + const { container } = render(); + expect(queryByAutoAdvanceAfter(container, '7s')).not.toBeInTheDocument(); }); - it('should use custom duration for auto-advance-after', async () => { + it('should use custom duration for auto-advance-after', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, advancement: { autoAdvance: true, pageDuration: 9 }, elements: [], }, @@ -385,20 +401,21 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); - await expect(getByAutoAdvanceAfter(container, '9s')).toBeInTheDocument(); + const { container } = render(); + expect(getByAutoAdvanceAfter(container, '9s')).toBeInTheDocument(); }); - it('should use default duration for images', async () => { + it('should use default duration for images', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'baz', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', scale: 1, origRatio: 9 / 16, @@ -424,20 +441,21 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); - await expect(getByAutoAdvanceAfter(container, '7s')).toBeInTheDocument(); + const { container } = render(); + expect(getByAutoAdvanceAfter(container, '7s')).toBeInTheDocument(); }); - it('should use video element ID for auto-advance-after', async () => { + it('should use video element ID for auto-advance-after', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'baz', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -464,9 +482,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const video = queryById(container, 'el-baz-media'); - await expect(video).toBeInTheDocument(); + expect(video).toBeInTheDocument(); expect(video).toMatchInlineSnapshot(` { /> `); - await expect( + expect( getByAutoAdvanceAfter(container, 'el-baz-media') ).toBeInTheDocument(); }); - it('should use video with volume', async () => { + it('should use video with volume', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'baz', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -523,9 +542,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const video = queryById(container, 'el-baz-media'); - await expect(video).toBeInTheDocument(); + expect(video).toBeInTheDocument(); expect(video).toMatchInlineSnapshot(` { /> `); - await expect( + expect( getByAutoAdvanceAfter(container, 'el-baz-media') ).toBeInTheDocument(); }); - it('should use video element ID for auto-advance-after if video is below defaultPageDuration', async () => { + it('should use video element ID for auto-advance-after if video is below defaultPageDuration', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'baz', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -582,9 +602,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const video = getById(container, 'el-baz-media'); - await expect(video).toBeInTheDocument(); + expect(video).toBeInTheDocument(); expect(video).toMatchInlineSnapshot(` { /> `); - await expect( + expect( getByAutoAdvanceAfter(container, 'el-baz-media') ).toBeInTheDocument(); }); - it('should ignore looping video for auto-advance-after and set default instead', async () => { + it('should ignore looping video for auto-advance-after and set default instead', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'baz', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -640,8 +661,8 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); - await expect(getByAutoAdvanceAfter(container, '7s')).toBeInTheDocument(); + const { container } = render(); + expect(getByAutoAdvanceAfter(container, '7s')).toBeInTheDocument(); }); }); @@ -649,7 +670,7 @@ describe('Page output', () => { const BACKGROUND_ELEMENT = { isBackground: true, id: 'baz', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', origRatio: 1, x: 50, @@ -671,7 +692,7 @@ describe('Page output', () => { const TEXT_ELEMENT = { id: 'baz', - type: 'text', + type: ElementType.Text, content: 'Hello, link!', x: 50, y: PAGE_HEIGHT, @@ -697,45 +718,47 @@ describe('Page output', () => { }, }; - it('should output page attachment if the URL is set', async () => { + it('should output page attachment if the URL is set', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], pageAttachment: { url: 'https://example.test', ctaText: 'Click me!', theme: 'dark', icon: 'https://example.test/example.jpg', - }, + } as PageAttachment, }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const pageOutlink = container.querySelector('amp-story-page-outlink'); - await expect(pageOutlink).toHaveTextContent('Click me!'); - await expect(pageOutlink).toHaveAttribute( + expect(pageOutlink).toHaveTextContent('Click me!'); + expect(pageOutlink).toHaveAttribute( 'cta-image', 'https://example.test/example.jpg' ); - await expect(pageOutlink).toHaveAttribute('theme', 'dark'); - await expect(pageOutlink.firstChild).toHaveAttribute( + expect(pageOutlink).toHaveAttribute('theme', 'dark'); + expect(pageOutlink.firstChild).toHaveAttribute( 'href', 'https://example.test' ); - await expect(pageOutlink).toBeInTheDocument(); + expect(pageOutlink).toBeInTheDocument(); }); - it('should not output page attachment if the URL is empty', async () => { + it('should not output page attachment if the URL is empty', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], pageAttachment: { url: '', @@ -746,42 +769,44 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const pageOutlink = container.querySelector('amp-story-page-outlink'); - await expect(pageOutlink).not.toBeInTheDocument(); + expect(pageOutlink).not.toBeInTheDocument(); }); - it('should not output cta-image if empty', async () => { + it('should not output cta-image if empty', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], pageAttachment: { url: 'https://example.test', ctaText: 'Click me!', theme: 'dark', icon: '', - }, + } as PageAttachment, }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const pageOutlink = container.querySelector('amp-story-page-outlink'); - await expect(pageOutlink).toHaveTextContent('Click me!'); - await expect(pageOutlink).not.toHaveAttribute('cta-image'); - await expect(pageOutlink).toBeInTheDocument(); + expect(pageOutlink).toHaveTextContent('Click me!'); + expect(pageOutlink).not.toHaveAttribute('cta-image'); + expect(pageOutlink).toBeInTheDocument(); }); - it('should output rel', async () => { + it('should output rel', () => { const props = { id: '123', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], pageAttachment: { url: 'https://example.test', @@ -789,21 +814,21 @@ describe('Page output', () => { theme: 'dark', icon: '', rel: ['nofollow'], - }, + } as PageAttachment, }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const pageOutlink = container.querySelector('amp-story-page-outlink'); - await expect(pageOutlink).toBeInTheDocument(); - await expect(pageOutlink).toHaveTextContent('Click me!'); - await expect(pageOutlink).not.toHaveAttribute('cta-image'); + expect(pageOutlink).toBeInTheDocument(); + expect(pageOutlink).toHaveTextContent('Click me!'); + expect(pageOutlink).not.toHaveAttribute('cta-image'); const pageOutATag = pageOutlink.querySelector('a'); - await expect(pageOutATag).toBeInTheDocument(); - await expect(pageOutATag).toHaveAttribute('rel', 'nofollow'); + expect(pageOutATag).toBeInTheDocument(); + expect(pageOutATag).toHaveAttribute('rel', 'nofollow'); }); it('should not output a link in page attachment area', () => { @@ -812,17 +837,18 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { ...TEXT_ELEMENT, link: { - url: 'http://shouldremove.com', + url: 'https://shouldremove.com', }, }, ], pageAttachment: { - url: 'http://example.com', + url: 'https://example.com', ctaText: 'Click me!', }, }, @@ -830,9 +856,11 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('Hello, link'); - expect(content).not.toContain('http://shouldremove.com'); + expect(content).not.toContain('https://shouldremove.com'); }); it('should output a link outside of page attachment area', () => { @@ -841,19 +869,20 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { ...TEXT_ELEMENT, link: { - url: 'http://shouldoutput.com', + url: 'https://shouldoutput.com', }, y: 0, height: 100, }, ], pageAttachment: { - url: 'http://example.com', + url: 'https://example.com', ctaText: 'Click me!', }, }, @@ -861,9 +890,11 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('Hello, link'); - expect(content).toContain('http://shouldoutput.com'); + expect(content).toContain('https://shouldoutput.com'); }); it('should output a link in page attachment area if page attachment is not set', () => { @@ -872,41 +903,44 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { ...TEXT_ELEMENT, link: { - url: 'http://shouldoutput.com', + url: 'https://shouldoutput.com', }, }, ], - pageAttachment: null, }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('Hello, link'); - expect(content).toContain('http://shouldoutput.com'); + expect(content).toContain('https://shouldoutput.com'); }); it('should print page attachment as the last child element', () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, pageAttachment: { - url: 'http://example.com', + url: 'https://example.com', ctaText: 'Click me!', }, animations: [], elements: [ { id: 'baz', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -944,7 +978,7 @@ describe('Page output', () => { defaultPageDuration: 11, }; - const { container } = render(); + const { container } = render(); const page = container.querySelector('amp-story-page'); const pageOutlink = container.querySelector('amp-story-page-outlink'); expect(pageOutlink).toBeInTheDocument(); @@ -955,7 +989,7 @@ describe('Page output', () => { describe('background color', () => { const BACKGROUND_ELEMENT = { id: 'baz', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', origRatio: 1, x: 50, @@ -980,7 +1014,7 @@ describe('Page output', () => { const props = { id: '123', page: { - backgroundColor: { color: '#00379b' }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, id: '123', elements: [BACKGROUND_ELEMENT], }, @@ -988,7 +1022,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('background-color:#00379b'); }); @@ -1000,7 +1036,7 @@ describe('Page output', () => { elements: [ { id: '123', - type: 'shape', + type: ElementType.Shape, isBackground: true, isDefaultBackground: true, x: 1, @@ -1015,7 +1051,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('background-color:rgba(255,255,255,0.5)'); }); }); @@ -1024,7 +1062,7 @@ describe('Page output', () => { const BACKGROUND_ELEMENT = { isBackground: true, id: 'baz', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', origRatio: 1, x: 50, @@ -1046,7 +1084,7 @@ describe('Page output', () => { const TEXT_ELEMENT = { id: 'baz', - type: 'text', + type: ElementType.Text, content: 'Hello, link!', x: 50, y: PAGE_HEIGHT, @@ -1078,6 +1116,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { @@ -1094,7 +1133,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('Hello, example!'); expect(content).toContain('https://hello.example'); }); @@ -1105,6 +1146,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { @@ -1121,7 +1163,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).not.toContain('Hello, example!'); expect(content).not.toContain('https://hello.example'); }); @@ -1131,7 +1175,7 @@ describe('Page output', () => { const BACKGROUND_ELEMENT = { isBackground: true, id: 'baz', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', origRatio: 1, x: 50, @@ -1154,8 +1198,6 @@ describe('Page output', () => { const MEDIA_ELEMENT = { ...BACKGROUND_ELEMENT, isBackground: false, - id: 'baz', - type: 'image', }; it('should output element with border if border is set', () => { @@ -1164,6 +1206,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { @@ -1173,7 +1216,7 @@ describe('Page output', () => { left: 10, right: 10, bottom: 10, - color: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + color: { color: { r: 255, g: 255, b: 255 } }, }, }, ], @@ -1182,7 +1225,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('border-width:10px 10px 10px 10px;'); expect(content).toContain('border-color:rgba(255,255,255,1);'); }); @@ -1193,6 +1238,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { @@ -1202,7 +1248,7 @@ describe('Page output', () => { left: 10, right: 10, bottom: 10, - color: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + color: { color: { r: 255, g: 255, b: 255 } }, position: 'center', }, mask: { @@ -1215,7 +1261,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).not.toContain('border-width:10px 10px 10px 10px;'); expect(content).not.toContain('border-color:rgba(255,255,255,1);'); }); @@ -1225,7 +1273,7 @@ describe('Page output', () => { const BACKGROUND_ELEMENT = { isBackground: true, id: 'baz', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', origRatio: 1, x: 50, @@ -1248,8 +1296,6 @@ describe('Page output', () => { const MEDIA_ELEMENT = { ...BACKGROUND_ELEMENT, isBackground: false, - id: 'baz', - type: 'image', }; it('should output image with linear overlay if set', () => { @@ -1258,6 +1304,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { @@ -1278,7 +1325,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain( 'background-image:linear-gradient(0.5turn, rgba(0,0,0,0) 0%, rgba(0,0,0,0.7) 100%)' ); @@ -1290,11 +1339,12 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { ...MEDIA_ELEMENT, - type: 'video', + type: ElementType.Video, overlay: { color: { r: 0, g: 0, b: 0, a: 0.5 }, }, @@ -1305,7 +1355,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain('background-color:rgba(0,0,0,0.5)'); }); }); @@ -1314,7 +1366,7 @@ describe('Page output', () => { const BACKGROUND_ELEMENT = { isBackground: true, id: 'baz', - type: 'image', + type: ElementType.Image, mimeType: 'image/png', origRatio: 1, x: 50, @@ -1338,8 +1390,9 @@ describe('Page output', () => { ...BACKGROUND_ELEMENT, isBackground: false, id: 'baz', - type: 'image', + type: ElementType.Image, borderRadius: { + locked: false, topLeft: 10, topRight: 20, bottomRight: 10, @@ -1353,13 +1406,16 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [BACKGROUND_ELEMENT, MEDIA_ELEMENT], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain( 'border-radius:100% 200% 100% 100% / 100% 200% 100% 100%' ); @@ -1371,6 +1427,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ BACKGROUND_ELEMENT, { @@ -1385,7 +1442,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).not.toContain( 'border-radius:100% 200% 100% 100% / 100% 200% 100% 100%' ); @@ -1397,6 +1456,8 @@ describe('Page output', () => { const props = { id: '123', page: { + id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, backgroundAudio: { resource: { src: 'https://example.com/audio.mp3', @@ -1405,25 +1466,30 @@ describe('Page output', () => { length: 100, lengthFormatted: '1:40', }, + loop: true, tracks: [], }, - id: '123', elements: [], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup( + + ); expect(content).toContain( 'background-audio="https://example.com/audio.mp3"' ); expect(content).not.toContain('amp-video'); }); - it('should add background audio as amp-video', async () => { + + it('should add background audio as amp-video', () => { const props = { id: '123', page: { + id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, backgroundAudio: { resource: { src: 'https://example.com/audio.mp3', @@ -1445,54 +1511,55 @@ describe('Page output', () => { ], loop: true, }, - id: '123', elements: [], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const captions = container.querySelector('amp-story-captions'); - await expect(captions).toBeInTheDocument(); + expect(captions).toBeInTheDocument(); expect(captions).toMatchSnapshot(); expect(captions).toHaveAttribute('id', 'el-123-captions'); const video = container.querySelector('amp-video'); - await expect(video).toBeInTheDocument(); + expect(video).toBeInTheDocument(); expect(video).toMatchSnapshot(); expect(video).toHaveAttribute('captions-id', 'el-123-captions'); const page = container.querySelector('amp-story-page'); - await expect(page).toBeInTheDocument(); + expect(page).toBeInTheDocument(); expect(page).not.toContain( 'background-audio="https://example.com/audio.mp3"' ); }); - it('should not contain background audio if null', async () => { + it('should not contain background audio if missing', () => { const props = { id: '123', page: { - backgroundAudio: null, id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const page = container.querySelector('amp-story-page'); - await expect(page).toBeInTheDocument(); + expect(page).toBeInTheDocument(); expect(page).not.toContain('background-audio='); }); - it('should use amp-video for non-looping background audio', async () => { + it('should use amp-video for non-looping background audio', () => { const props = { id: '123', page: { + id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, backgroundAudio: { resource: { src: 'https://example.com/audio.mp3', @@ -1504,29 +1571,29 @@ describe('Page output', () => { tracks: [], loop: false, }, - id: '123', elements: [], }, defaultAutoAdvance: false, defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const video = container.querySelector('amp-video'); - await expect(video).toBeInTheDocument(); + expect(video).toBeInTheDocument(); expect(video).toMatchSnapshot(); const page = container.querySelector('amp-story-page'); - await expect(page).toBeInTheDocument(); + expect(page).toBeInTheDocument(); expect(page).not.toContain( 'background-audio="https://example.com/audio.mp3"' ); }); - it('should use amp-video with crossorigin="anonymous" for background audio with tracks', async () => { + it('should use amp-video with crossorigin="anonymous" for background audio with tracks', () => { const props = { id: '123', page: { + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, backgroundAudio: { resource: { src: 'https://example.com/audio.mp3', @@ -1555,13 +1622,13 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const video = container.querySelector('amp-video'); - await expect(video).toBeInTheDocument(); + expect(video).toBeInTheDocument(); expect(video).toMatchSnapshot(); const page = container.querySelector('amp-story-page'); - await expect(page).toBeInTheDocument(); + expect(page).toBeInTheDocument(); expect(page).not.toContain( 'background-audio="https://example.com/audio.mp3"' ); @@ -1569,16 +1636,17 @@ describe('Page output', () => { }); describe('video captions', () => { - it('should render layer for amp-story-captions', async () => { + it('should render layer for amp-story-captions', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'baz', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -1616,9 +1684,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - const { container } = render(); + const { container } = render(); const captions = container.querySelector('amp-story-captions'); - await expect(captions).toBeInTheDocument(); + expect(captions).toBeInTheDocument(); expect(captions).toMatchInlineSnapshot(` { }); describe('Shopping', () => { - it('should render shopping attachment if there are products', async () => { + it('should render shopping attachment if there are products', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'el1', - type: 'product', + type: ElementType.Product, x: 50, y: 50, width: 32, @@ -1649,7 +1718,7 @@ describe('Page output', () => { }, { id: 'el2', - type: 'product', + type: ElementType.Product, x: 100, y: 100, width: 32, @@ -1661,27 +1730,27 @@ describe('Page output', () => { }, }; - const { container } = render(); + const { container } = render(); const shoppingAttachment = container.querySelector( 'amp-story-shopping-attachment' ); - await expect(shoppingAttachment).toBeInTheDocument(); + expect(shoppingAttachment).toBeInTheDocument(); }); - it('should render shopping attachment with custom cta text if there are products', async () => { + it('should render shopping attachment with custom cta text if there are products', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, shoppingAttachment: { - theme: 'light', ctaText: 'Buy now', }, elements: [ { id: 'el1', - type: 'product', + type: ElementType.Product, x: 50, y: 50, width: 32, @@ -1691,7 +1760,7 @@ describe('Page output', () => { }, { id: 'el2', - type: 'product', + type: ElementType.Product, x: 100, y: 100, width: 32, @@ -1703,26 +1772,27 @@ describe('Page output', () => { }, }; - const { container } = render(); + const { container } = render(); const shoppingAttachment = container.querySelector( 'amp-story-shopping-attachment' ); - await expect(shoppingAttachment).toBeInTheDocument(); - await expect(shoppingAttachment).toHaveAttribute('cta-text', 'Buy now'); - await expect(shoppingAttachment).toHaveAttribute('theme', 'light'); + expect(shoppingAttachment).toBeInTheDocument(); + expect(shoppingAttachment).toHaveAttribute('cta-text', 'Buy now'); + expect(shoppingAttachment).toHaveAttribute('theme', 'light'); }); - it('should not render page attachment if there are products', async () => { + it('should not render page attachment if there are products', () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'el1', - type: 'product', + type: ElementType.Product, x: 50, y: 50, width: 32, @@ -1732,7 +1802,7 @@ describe('Page output', () => { }, { id: 'el2', - type: 'product', + type: ElementType.Product, x: 100, y: 100, width: 32, @@ -1742,19 +1812,19 @@ describe('Page output', () => { }, ], pageAttachment: { - url: 'http://example.com', + url: 'https://example.com', ctaText: 'Click me!', }, }, }; - const { container } = render(); + const { container } = render(); const shoppingAttachment = container.querySelector( 'amp-story-shopping-attachment' ); - await expect(shoppingAttachment).toBeInTheDocument(); + expect(shoppingAttachment).toBeInTheDocument(); const pageOutlink = container.querySelector('amp-story-page-outlink'); - await expect(pageOutlink).not.toBeInTheDocument(); + expect(pageOutlink).not.toBeInTheDocument(); }); }); @@ -1767,13 +1837,16 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], }, defaultAutoAdvance: true, defaultPageDuration: 11, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid AMP output with manual page advancement', async () => { @@ -1782,12 +1855,15 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], }, defaultAutoAdvance: false, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid AMP output with custom page duration', async () => { @@ -1796,6 +1872,7 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'p1', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, advancement: { autoAdvance: true, pageDuration: 10 }, elements: [], }, @@ -1803,7 +1880,9 @@ describe('Page output', () => { defaultPageDuration: 7, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid AMP output with custom manual page advancement', async () => { @@ -1812,13 +1891,16 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'p1', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, advancement: { autoAdvance: false }, elements: [], }, defaultAutoAdvance: true, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid AMP output with Page Attachment', async () => { @@ -1827,15 +1909,18 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [], }, defaultAutoAdvance: true, pageAttachment: { - url: 'http://example.com', + url: 'https://example.com', ctaText: 'Click me!', }, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid output with media elements', async () => { @@ -1844,10 +1929,11 @@ describe('Page output', () => { backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: '123', - type: 'video', + type: ElementType.Video, mimeType: 'video/mp4', scale: 1, origRatio: 9 / 16, @@ -1874,21 +1960,29 @@ describe('Page output', () => { defaultPageDuration: 11, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid output with animations', async () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, animations: [ - { id: '123', targets: ['123'], type: 'bounce', duration: 1000 }, - ], + { + id: '123', + targets: ['123'], + type: AnimationType.Bounce, + duration: 1000, + }, + ] as StoryAnimation[], elements: [ { - type: 'text', + type: ElementType.Text, id: '123', x: 50, y: 100, @@ -1912,22 +2006,28 @@ describe('Page output', () => { defaultPageDuration: 11, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid output with background audio', async () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, backgroundAudio: { resource: { src: 'https://example.com/audio.mp3', id: 123, mimeType: 'audio/mpeg', + length: 100, + lengthFormatted: '1:40', }, tracks: [], + loop: true, }, animations: [], elements: [], @@ -1936,20 +2036,25 @@ describe('Page output', () => { defaultPageDuration: 11, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid output with background audio with captions', async () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, backgroundAudio: { resource: { src: 'https://example.com/audio.mp3', id: 123, mimeType: 'audio/mpeg', + length: 100, + lengthFormatted: '1:40', }, tracks: [ { @@ -1962,6 +2067,7 @@ describe('Page output', () => { kind: 'captions', }, ], + loop: false, }, animations: [], elements: [], @@ -1970,15 +2076,18 @@ describe('Page output', () => { defaultPageDuration: 11, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); it('should produce valid output with non-looping background audio', async () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, backgroundAudio: { resource: { src: 'https://example.com/audio.mp3', @@ -1997,20 +2106,22 @@ describe('Page output', () => { defaultPageDuration: 11, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); - // eslint-disable-next-line jest/no-disabled-tests -- TODO: Enable once stable. it.skip('should produce valid output with shopping products', async () => { const props = { id: 'foo', backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: 'bar', + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, elements: [ { id: 'el1', - type: 'product', + type: ElementType.Product, x: 50, y: 50, width: 32, @@ -2020,7 +2131,7 @@ describe('Page output', () => { }, { id: 'el2', - type: 'product', + type: ElementType.Product, x: 100, y: 100, width: 32, @@ -2030,7 +2141,7 @@ describe('Page output', () => { }, { id: 'el3', - type: 'product', + type: ElementType.Product, x: 150, y: 150, width: 32, @@ -2040,7 +2151,7 @@ describe('Page output', () => { }, { id: 'el3', - type: 'product', + type: ElementType.Product, x: 200, y: 200, width: 32, @@ -2052,7 +2163,9 @@ describe('Page output', () => { }, }; - await expect().toBeValidAMPStoryPage(); + await expect( + + ).toBeValidAMPStoryPage(); }); }); }); diff --git a/packages/output/src/test/story.tsx b/packages/output/src/test/story.tsx index cba900ddbb30..fa77ee32ac76 100644 --- a/packages/output/src/test/story.tsx +++ b/packages/output/src/test/story.tsx @@ -18,12 +18,13 @@ * External dependencies */ import { renderToStaticMarkup } from '@googleforcreators/react'; -import { registerElementType } from '@googleforcreators/elements'; +import { ElementType, registerElementType } from '@googleforcreators/elements'; import { elementTypes } from '@googleforcreators/element-library'; /** * Internal dependencies */ +import { AnimationType, StoryAnimation } from '@googleforcreators/animation'; import StoryOutput from '../story'; describe('Story output', () => { @@ -64,11 +65,20 @@ describe('Story output', () => { { id: '123', animations: [ - { id: 'anim1', targets: ['123'], type: 'bounce', duration: 1000 }, - { id: 'anim1', targets: ['124'], type: 'spin', duration: 500 }, - ], + { + id: 'anim1', + targets: ['123'], + type: AnimationType.Bounce, + duration: 1000, + }, + { + id: 'anim1', + targets: ['124'], + type: AnimationType.Spin, + duration: 500, + }, + ] as StoryAnimation[], backgroundColor: { - type: 'solid', color: { r: 255, g: 255, b: 255 }, }, page: { @@ -76,7 +86,7 @@ describe('Story output', () => { }, elements: [ { - type: 'text', + type: ElementType.Text, id: '123', x: 50, y: 100, @@ -99,7 +109,7 @@ describe('Story output', () => { }, }, { - type: 'text', + type: ElementType.Text, id: '124', x: 50, y: 100, @@ -129,7 +139,7 @@ describe('Story output', () => { }, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup(); expect(content).toContain( '' @@ -168,18 +178,30 @@ describe('Story output', () => { src: 'https://example.com/audio.mp3', id: 123, mimeType: 'audio/mpeg', + length: 100, + lengthFormatted: '1:40', }, }, + fonts: {}, }, pages: [ { id: '123', animations: [ - { id: 'anim1', targets: ['123'], type: 'bounce', duration: 1000 }, - { id: 'anim1', targets: ['123'], type: 'spin', duration: 500 }, - ], + { + id: 'anim1', + targets: ['123'], + type: AnimationType.Bounce, + duration: 1000, + }, + { + id: 'anim1', + targets: ['123'], + type: AnimationType.Spin, + duration: 500, + }, + ] as StoryAnimation[], backgroundColor: { - type: 'solid', color: { r: 255, g: 255, b: 255 }, }, page: { @@ -187,7 +209,7 @@ describe('Story output', () => { }, elements: [ { - type: 'text', + type: ElementType.Text, id: '123', x: 50, y: 100, @@ -213,7 +235,7 @@ describe('Story output', () => { }, }; - const content = renderToStaticMarkup(); + const content = renderToStaticMarkup(); expect(content).toContain( 'background-audio="https://example.com/audio.mp3"' @@ -250,6 +272,7 @@ describe('Story output', () => { password: '123', link: 'https://example.com/story', autoAdvance: false, + fonts: {}, }, pages: [], metadata: { @@ -257,13 +280,13 @@ describe('Story output', () => { }, }; - await expect().not.toBeValidAMP(); + await expect().not.toBeValidAMP(); }); it('should produce valid AMP output', async () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, story: { title: 'Example', slug: 'example', @@ -287,12 +310,12 @@ describe('Story output', () => { password: '123', link: 'https://example.com/story', autoAdvance: false, + fonts: {}, }, pages: [ { id: '123', backgroundColor: { - type: 'solid', color: { r: 255, g: 255, b: 255 }, }, page: { @@ -306,7 +329,7 @@ describe('Story output', () => { }, }; - await expect().toBeValidAMP(); + await expect().toBeValidAMP(); }); it('should produce valid AMP output when using Google fonts', async () => { @@ -336,12 +359,12 @@ describe('Story output', () => { password: '123', link: 'https://example.com/story', autoAdvance: false, + fonts: {}, }, pages: [ { id: '123', backgroundColor: { - type: 'solid', color: { r: 255, g: 255, b: 255 }, }, page: { @@ -349,7 +372,7 @@ describe('Story output', () => { }, elements: [ { - type: 'text', + type: ElementType.Text, id: '123', x: 50, y: 100, @@ -375,7 +398,7 @@ describe('Story output', () => { }, }; - await expect().toBeValidAMP(); + await expect().toBeValidAMP(); }); it('should produce valid AMP output when using animations', async () => { @@ -405,15 +428,20 @@ describe('Story output', () => { password: '123', link: 'https://example.com/story', autoAdvance: false, + fonts: {}, }, pages: [ { id: '123', animations: [ - { id: 'anim1', targets: ['123'], type: 'bounce', duration: 1000 }, - ], + { + id: 'anim1', + targets: ['123'], + type: AnimationType.Bounce, + duration: 1000, + }, + ] as StoryAnimation[], backgroundColor: { - type: 'solid', color: { r: 255, g: 255, b: 255 }, }, page: { @@ -421,7 +449,7 @@ describe('Story output', () => { }, elements: [ { - type: 'text', + type: ElementType.Text, id: '123', x: 50, y: 100, @@ -447,13 +475,13 @@ describe('Story output', () => { }, }; - await expect().toBeValidAMP(); + await expect().toBeValidAMP(); }); it('should produce valid AMP output when using background audio', async () => { const props = { id: '123', - backgroundColor: { type: 'solid', color: { r: 255, g: 255, b: 255 } }, + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, story: { title: 'Example', slug: 'example', @@ -482,25 +510,30 @@ describe('Story output', () => { src: 'https://example.com/audio.mp3', id: 123, mimeType: 'audio/mpeg', + length: 100, + lengthFormatted: '1:40', }, }, + fonts: {}, }, pages: [ { id: '123', animations: [ - { id: 'anim1', targets: ['123'], type: 'bounce', duration: 1000 }, - ], - backgroundColor: { - type: 'solid', - color: { r: 255, g: 255, b: 255 }, - }, + { + id: 'anim1', + targets: ['123'], + type: AnimationType.Bounce, + duration: 1000, + }, + ] as StoryAnimation[], + backgroundColor: { color: { r: 255, g: 255, b: 255 } }, page: { id: '123', }, elements: [ { - type: 'text', + type: ElementType.Text, id: '123', x: 50, y: 100, @@ -526,7 +559,7 @@ describe('Story output', () => { }, }; - await expect().toBeValidAMP(); + await expect().toBeValidAMP(); }); }); }); diff --git a/packages/output/src/test/textElement.tsx b/packages/output/src/test/textElement.tsx index f929ccf9a06a..e11a4f0ada40 100644 --- a/packages/output/src/test/textElement.tsx +++ b/packages/output/src/test/textElement.tsx @@ -25,6 +25,7 @@ import { BACKGROUND_TEXT_MODE, } from '@googleforcreators/elements'; import { elementTypes } from '@googleforcreators/element-library'; +import type { PropsWithChildren } from 'react'; /** * Internal dependencies @@ -32,7 +33,7 @@ import { elementTypes } from '@googleforcreators/element-library'; import OutputElement from '../element'; import { DEFAULT_TEXT } from './_utils/constants'; -function WrapAnimation({ children }) { +function WrapAnimation({ children }: PropsWithChildren) { return {children}; } @@ -104,7 +105,7 @@ describe('Text Element output', () => { it('should render text with color', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -113,7 +114,7 @@ describe('Text Element output', () => { it('should render text with fill', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -122,7 +123,7 @@ describe('Text Element output', () => { it('should render text with highlight', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -131,7 +132,7 @@ describe('Text Element output', () => { it('should render text with padding', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -145,7 +146,7 @@ describe('Text Element output', () => { const textLeft = renderToStaticMarkup( - + ); expect(textLeft).toMatchSnapshot(); @@ -157,7 +158,7 @@ describe('Text Element output', () => { const textRight = renderToStaticMarkup( - + ); expect(textRight).toMatchSnapshot(); @@ -169,7 +170,7 @@ describe('Text Element output', () => { const textCenter = renderToStaticMarkup( - + ); expect(textCenter).toMatchSnapshot(); @@ -181,7 +182,7 @@ describe('Text Element output', () => { const textJustify = renderToStaticMarkup( - + ); expect(textJustify).toMatchSnapshot(); @@ -190,7 +191,7 @@ describe('Text Element output', () => { it('should render text with bold, italic, and underline', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -199,7 +200,7 @@ describe('Text Element output', () => { it('should render text with adjusted font-size and family', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -208,7 +209,7 @@ describe('Text Element output', () => { it('should render text with line-height', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -217,7 +218,7 @@ describe('Text Element output', () => { it('should render text with letter-spacing', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -226,7 +227,7 @@ describe('Text Element output', () => { it('should render text with applied rotation', () => { const element = renderToStaticMarkup( - + ); expect(element).toMatchSnapshot(); @@ -235,7 +236,7 @@ describe('Text Element output', () => { it('should render text without the mask class', () => { const html = renderToStaticMarkup( - + ); const div = document.createElement('div'); @@ -251,7 +252,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text color', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -259,7 +260,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text fill', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -267,7 +268,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text highlight', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -275,7 +276,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text padding', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -288,7 +289,7 @@ describe('Text Element output', () => { await expect( - + ).toBeValidAMPStoryElement(); @@ -299,7 +300,7 @@ describe('Text Element output', () => { await expect( - + ).toBeValidAMPStoryElement(); @@ -310,7 +311,7 @@ describe('Text Element output', () => { await expect( - + ).toBeValidAMPStoryElement(); @@ -321,7 +322,7 @@ describe('Text Element output', () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -329,7 +330,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text to bold, italic, and underline', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -337,7 +338,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text font-size and family', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -345,7 +346,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text line-height', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -353,7 +354,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text letter-spacing', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); @@ -361,7 +362,7 @@ describe('Text Element output', () => { it('should produce valid AMP output when setting text rotation', async () => { await expect( - + ).toBeValidAMPStoryElement(); }); diff --git a/packages/output/src/types.ts b/packages/output/src/types.ts index 86586638897c..aafe44b37b7a 100644 --- a/packages/output/src/types.ts +++ b/packages/output/src/types.ts @@ -21,16 +21,24 @@ /** * External dependencies */ -import type { Page, Story } from '@googleforcreators/elements'; +import type { Page, Story as FullStory } from '@googleforcreators/elements'; -interface MetaData { +export type Story = Pick; + +export interface StoryMetadata { publisher?: string; } export declare function getStoryMarkup( story: Story, pages: Page[], - metadata: MetaData, + metadata: StoryMetadata, flags: Record ): string; diff --git a/packages/output/src/typings/global.d.ts b/packages/output/src/typings/global.d.ts new file mode 100644 index 000000000000..d3c4e9e21973 --- /dev/null +++ b/packages/output/src/typings/global.d.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + * + * https://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 type * as React from 'react'; + +type AmpLayout = + | 'fill' + | 'fixed' + | 'fixed-height' + | 'flex-item' + | 'intrinsic' + | 'nodisplay' + | 'responsive' + | 'container'; + +interface AmpHTMLHtmlElement + extends React.DetailedHTMLProps< + React.HtmlHTMLAttributes, + HTMLHtmlElement + > { + amp: string; +} + +interface AmpVideo + extends React.DetailedHTMLProps< + React.VideoHTMLAttributes, + HTMLVideoElement + > { + layout: AmpLayout; + 'captions-id'?: string; + autoplay?: 'autoplay'; +} + +interface AmpImg + extends React.DetailedHTMLProps< + React.ImgHTMLAttributes, + HTMLImageElement + > { + layout: AmpLayout; +} + +interface AmpStory { + standalone: ''; + title: string; + publisher?: string; + 'publisher-logo-src': string; + 'poster-portrait-src': string; + 'background-audio'?: string; + children?: React.ReactNode; +} + +interface AmpStoryGridLayer { + template: 'fill' | 'vertical'; + 'aspect-ratio'?: string; + class?: string; + children?: React.ReactNode; +} + +interface AmpStoryPage { + id?: string; + 'auto-advance-after'?: string; + 'background-audio'?: string; + children?: React.ReactNode; +} + +interface AmpStoryShoppingAttachment { + theme?: 'light' | 'dark'; + 'cta-text'?: string; + children?: React.ReactNode; +} + +interface AmpStoryPageOutlink { + theme?: 'light' | 'dark'; + layout: AmpLayout; + 'cta-image'?: string; + children?: React.ReactNode; +} + +interface AmpStoryCaptions { + key: string; + id: string; + layout: AmpLayout; + height: string; +} + +declare module 'react' { + interface HtmlHTMLAttributes extends React.HTMLAttributes { + amp?: string; + } +} + +declare global { + namespace JSX { + interface IntrinsicElements { + 'amp-story': AmpStory; + 'amp-story-grid-layer': AmpStoryGridLayer; + 'amp-story-page': AmpStoryPage; + 'amp-story-page-outlink': AmpStoryPageOutlink; + 'amp-story-shopping-attachment': AmpStoryShoppingAttachment; + 'amp-story-captions': AmpStoryCaptions; + 'amp-video': AmpVideo; + 'amp-img': AmpImg; + } + } +} + +export {}; diff --git a/packages/output/src/typings/jest.d.ts b/packages/output/src/typings/jest.d.ts new file mode 100644 index 000000000000..e006387ae9dc --- /dev/null +++ b/packages/output/src/typings/jest.d.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + * + * https://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 'jest-extended'; +import '@testing-library/jest-dom'; + +declare global { + namespace jest { + interface Matchers { + toBeValidAMP(): Promise; + toBeValidAMPStory(): Promise; + toBeValidAMPStoryPage(): Promise; + toBeValidAMPStoryElement(): Promise; + } + } +} + +export {}; diff --git a/packages/output/src/typings/svg.d.ts b/packages/output/src/typings/svg.d.ts new file mode 100644 index 000000000000..2b2af4b3434d --- /dev/null +++ b/packages/output/src/typings/svg.d.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Google LLC + * + * 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 + * + * https://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. + */ + +declare module '*.svg' { + import type { FunctionComponent, SVGProps } from 'react'; + const ReactComponent: FunctionComponent< + SVGProps & { title?: string } + >; + export default ReactComponent; +} diff --git a/packages/output/src/utils/ampBoilerplate.tsx b/packages/output/src/utils/ampBoilerplate.tsx index 9c61070d97c2..cad09a18830d 100644 --- a/packages/output/src/utils/ampBoilerplate.tsx +++ b/packages/output/src/utils/ampBoilerplate.tsx @@ -19,7 +19,7 @@ * * @see https://amp.dev/documentation/guides-and-tutorials/learn/spec/amp-boilerplate/ * @see https://amp.dev/documentation/components/amp-story/#boilerplate - * @return {Element} AMP boilerplate. + * @return AMP boilerplate. */ function Boilerplate() { return ( diff --git a/packages/output/src/utils/backgroundAudio.tsx b/packages/output/src/utils/backgroundAudio.tsx index 845c1660f81a..54f44a7e646b 100644 --- a/packages/output/src/utils/backgroundAudio.tsx +++ b/packages/output/src/utils/backgroundAudio.tsx @@ -17,24 +17,27 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; -import { - ResourcePropTypes, - BackgroundAudioPropType, -} from '@googleforcreators/media'; +import type { BackgroundAudio, ElementId } from '@googleforcreators/elements'; -function BackgroundAudio({ backgroundAudio, id }) { - const { resource, tracks, loop } = backgroundAudio || {}; +interface BackgroundAudioProps { + backgroundAudio: BackgroundAudio; + id: ElementId; +} + +function BackgroundAudio({ backgroundAudio, id }: BackgroundAudioProps) { + const { resource, tracks, loop } = backgroundAudio; const { mimeType, src } = resource; + const hasTracks = tracks?.length && tracks?.length > 0; + const videoProps = { loop: loop ? 'loop' : undefined, id: `page-${id}-background-audio`, // Actual output happens in OutputPage. - 'captions-id': tracks?.length > 0 ? `el-${id}-captions` : undefined, + 'captions-id':hasTracks ? `el-${id}-captions` : undefined, // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track#attr-src // and https://github.com/GoogleForCreators/web-stories-wp/issues/11479 - crossorigin: tracks?.length > 0 ? 'anonymous' : undefined, + crossorigin: hasTracks ? 'anonymous' : undefined, }; const sourceProps = { @@ -73,13 +76,4 @@ function BackgroundAudio({ backgroundAudio, id }) { ); } -BackgroundAudio.propTypes = { - backgroundAudio: PropTypes.shape({ - resource: BackgroundAudioPropType, - loop: PropTypes.bool, - tracks: PropTypes.arrayOf(ResourcePropTypes.trackResource), - }), - id: PropTypes.string.isRequired, -}; - export default BackgroundAudio; diff --git a/packages/output/src/utils/fontDeclarations.tsx b/packages/output/src/utils/fontDeclarations.tsx index da83b478283a..47573f6db02a 100644 --- a/packages/output/src/utils/fontDeclarations.tsx +++ b/packages/output/src/utils/fontDeclarations.tsx @@ -17,19 +17,25 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; import { getGoogleFontURL, getFontCSS } from '@googleforcreators/fonts'; import { getFontVariants } from '@googleforcreators/rich-text'; -import { StoryPropTypes } from '@googleforcreators/elements'; - -const hasTuple = (tuples, tuple) => +import { elementIs, FontService } from '@googleforcreators/elements'; +import type { + CustomFontData, + GoogleFontData, + FontFamily, + FontVariant, + Page, +} from '@googleforcreators/elements'; + +const hasTuple = (tuples: FontVariant[], tuple: FontVariant) => tuples.some((val) => val[0] === tuple[0] && val[1] === tuple[1]); -const tupleDiff = (a, b) => { +const tupleDiff = (a: FontVariant, b: FontVariant) => { return Math.abs(a[0] + a[1] - b[0] - b[1]); }; -const getNearestTuple = (tuples, tuple) => { +const getNearestTuple = (tuples: FontVariant[], tuple: FontVariant) => { return tuples.reduce((acc, curr) => { const currDiff = tupleDiff(curr, tuple); const accDiff = tupleDiff(acc, tuple); @@ -38,22 +44,29 @@ const getNearestTuple = (tuples, tuple) => { }); }; -function FontDeclarations({ pages }) { - const map = new Map(); +function FontDeclarations({ pages }: { pages: Page[] }) { + const map = new Map< + FontService, + Map + >(); for (const { elements } of pages) { - const textElements = elements.filter(({ type }) => type === 'text'); + const textElements = elements.filter(elementIs.text); // Prepare font objects for later use. for (const { font, content } of textElements) { - const { service, family, variants = [], url } = font; - if (!service || service === 'system') { + const { service, family, variants = [] } = font; + if (!service || service === FontService.System) { continue; } const serviceMap = map.get(service) || new Map(); map.set(service, serviceMap); - const fontObj = serviceMap.get(family) || { family, variants: [], url }; + const fontObj = serviceMap.get(family) || { + family, + variants: [], + url: 'url' in font ? font.url : null, + }; const contentVariants = getFontVariants(content); @@ -92,18 +105,32 @@ function FontDeclarations({ pages }) { <> {Array.from(map.keys()).map((service) => { const serviceMap = map.get(service); + if (!serviceMap) { + return null; + } switch (service) { - case 'fonts.google.com': + case FontService.GoogleFonts: return ( ).values() + ) + )} rel="stylesheet" /> ); - case 'custom': - return Array.from(serviceMap.values()).map(({ family, url }) => { + case FontService.Custom: + return Array.from( + (serviceMap as Map).values() + ).map(({ family, url }) => { const inlineStyle = getFontCSS(family, url); + + if (!inlineStyle) { + return null; + } + return (