From ecef0101104f2d575279df7415e43563a89a928e Mon Sep 17 00:00:00 2001 From: Thomas Kellermeier Date: Mon, 27 Jan 2025 15:25:35 +0100 Subject: [PATCH] test: add test and monkey patch error logs --- .../blocks/preview/CompositionBlock.spec.tsx | 501 ++++++++++-------- .../test/__fixtures__/assembly.ts | 8 +- .../test/__fixtures__/composition.ts | 1 + .../experience-builder-sdk/testing-library.js | 14 + 4 files changed, 292 insertions(+), 232 deletions(-) diff --git a/packages/experience-builder-sdk/src/blocks/preview/CompositionBlock.spec.tsx b/packages/experience-builder-sdk/src/blocks/preview/CompositionBlock.spec.tsx index f0ff4abe7..6839a9cfd 100644 --- a/packages/experience-builder-sdk/src/blocks/preview/CompositionBlock.spec.tsx +++ b/packages/experience-builder-sdk/src/blocks/preview/CompositionBlock.spec.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { CONTENTFUL_COMPONENTS } from '@contentful/experiences-core/constants'; import { defineComponents, resetComponentRegistry } from '../../core/componentRegistry'; import type { ComponentTreeNode, ExperienceEntry } from '@contentful/experiences-core/types'; import { CompositionBlock } from './CompositionBlock'; -import type { Entry } from 'contentful'; import { experienceEntry } from '../../../test/__fixtures__/composition'; import { createAssemblyEntry, @@ -61,7 +60,7 @@ describe('CompositionBlock', () => { }; // Render the component with the initial text - const { getByText } = render( + render( { />, ); - expect(getByText('unboundValue1')).toBeInTheDocument(); + expect(screen.getByText('unboundValue1')).toBeInTheDocument(); }); it('renders section node', () => { @@ -88,7 +87,7 @@ describe('CompositionBlock', () => { children: [], }; - const { getByTestId } = render( + render( { />, ); - expect(getByTestId('contentful-container')).toBeInTheDocument(); + expect(screen.getByTestId('contentful-container')).toBeInTheDocument(); }); it('renders container node', () => { @@ -107,7 +106,7 @@ describe('CompositionBlock', () => { children: [], }; - const { getByTestId } = render( + render( { />, ); - expect(getByTestId('contentful-container')).toBeInTheDocument(); + expect(screen.getByTestId('contentful-container')).toBeInTheDocument(); }); - it('renders assembly node', () => { + it('renders pattern node', () => { const unboundValueKey = 'some-unbound-value-key'; - const assemblyEntry = createAssemblyEntry({ - id: defaultAssemblyId, - schemaVersion: '2023-09-28', - }); + const patternEntry = createAssemblyEntry(); const updatedExperienceEntry = { ...experienceEntry, fields: { ...experienceEntry.fields, - usedComponents: [assemblyEntry], + usedComponents: [patternEntry], unboundValues: { [unboundValueKey]: { value: 'New year eve', @@ -139,12 +135,12 @@ describe('CompositionBlock', () => { } as ExperienceEntry; const entityStore = new EntityStore({ - experienceEntry: updatedExperienceEntry as unknown as Entry, + experienceEntry: updatedExperienceEntry, entities: [...entries, ...assets], locale: 'en-US', }); - const assemblyNode: ComponentTreeNode = { + const patternNode: ComponentTreeNode = { definitionId: defaultAssemblyId, variables: { [assemblyGeneratedVariableName]: { type: 'UnboundValue', key: unboundValueKey }, @@ -152,260 +148,307 @@ describe('CompositionBlock', () => { children: [], }; - const { getByTestId, getByText } = render( + render( , ); - expect(getByTestId('assembly')).toBeInTheDocument(); - expect(getByTestId('contentful-container')).toBeInTheDocument(); - expect(getByText('New year eve')).toBeInTheDocument(); + expect(screen.getByTestId('assembly')).toBeInTheDocument(); + expect(screen.getByTestId('contentful-container')).toBeInTheDocument(); + expect(screen.getByText('New year eve')).toBeInTheDocument(); }); - it('renders the custom component node with SSR class', () => { - const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; - const mockExperienceTreeNode: ComponentTreeNode = { - definitionId: 'custom-component', - variables: { - text: { type: 'UnboundValue', key: 'value1' }, - cfSsrClassName: { - type: 'DesignValue', - valuesByBreakpoint: { desktop: ssrClassName }, - }, - }, - children: [], - }; + it('returns null when detecting a circular reference in the tree', () => { + const updatedExperienceEntry = structuredClone(experienceEntry); + const patternEntry = createAssemblyEntry(); + const nestedPatternEntry = createAssemblyEntry({ id: 'nested-pattern-id' }); - const { container } = render( - , - ); + updatedExperienceEntry.fields.usedComponents = [patternEntry]; + patternEntry.fields.usedComponents = [nestedPatternEntry]; + nestedPatternEntry.fields.usedComponents = [patternEntry]; - expect(container.firstChild).toHaveClass(ssrClassName); - }); + const entityStore = new EntityStore({ + experienceEntry: updatedExperienceEntry, + entities: [...entries, ...assets], + locale: 'en-US', + }); - it('renders section node with SSR class', () => { - const ssrClassName = 'cfstyles-4a59232681bf8491154b4c4ec81ea113'; - const sectionNode: ComponentTreeNode = { - definitionId: CONTENTFUL_COMPONENTS.section.id, - variables: { - cfSsrClassName: { type: 'DesignValue', valuesByBreakpoint: { desktop: ssrClassName } }, - }, + const patternNode: ComponentTreeNode = { + definitionId: defaultAssemblyId, + variables: {}, children: [], }; - const { getByTestId } = render( - , - ); - - expect(getByTestId('contentful-container')).toHaveClass(`${ssrClassName} contentful-container`); - }); - - it('renders container node with SSR class', () => { - const ssrClassName = 'cfstyles-4a59232681bf8491154b4c4ec81ea113'; - const containerNode: ComponentTreeNode = { - definitionId: CONTENTFUL_COMPONENTS.container.id, - variables: { - cfSsrClassName: { type: 'DesignValue', valuesByBreakpoint: { desktop: ssrClassName } }, - }, + const nestedPatternNode: ComponentTreeNode = { + definitionId: 'nested-pattern-id', + variables: {}, children: [], }; - const { getByTestId } = render( + patternEntry.fields.componentTree.children = [nestedPatternNode]; + nestedPatternEntry.fields.componentTree.children = [patternNode]; + + render( , ); - expect(getByTestId('contentful-container')).toHaveClass(`${ssrClassName} contentful-container`); + expect(screen.getAllByTestId('assembly')).toHaveLength(2); }); - it('renders nested components with SSR class', () => { - const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; - const sectionNode: ComponentTreeNode = { - definitionId: CONTENTFUL_COMPONENTS.section.id, - variables: { - cfSsrClassName: { - type: 'DesignValue', - valuesByBreakpoint: { desktop: ssrClassName }, - }, - }, - children: [ - { - definitionId: 'custom-component', - variables: { - text: { type: 'UnboundValue', key: 'value1' }, - cfSsrClassName: { - type: 'DesignValue', - valuesByBreakpoint: { desktop: ssrClassName }, - }, + describe('when SSR class is defined', () => { + it('renders the custom component node', () => { + const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; + const mockExperienceTreeNode: ComponentTreeNode = { + definitionId: 'custom-component', + variables: { + text: { type: 'UnboundValue', key: 'value1' }, + cfSsrClassName: { + type: 'DesignValue', + valuesByBreakpoint: { desktop: ssrClassName }, }, - children: [], }, - ], - }; - - const { getByTestId } = render( - , - ); + children: [], + }; + + const { container } = render( + , + ); + + expect(container.firstChild).toHaveClass(ssrClassName); + }); - expect(getByTestId('contentful-container')).toHaveClass(`${ssrClassName} contentful-container`); - expect(getByTestId('contentful-container').firstChild).toHaveClass(ssrClassName); - }); + it('renders section node', () => { + const ssrClassName = 'cfstyles-4a59232681bf8491154b4c4ec81ea113'; + const sectionNode: ComponentTreeNode = { + definitionId: CONTENTFUL_COMPONENTS.section.id, + variables: { + cfSsrClassName: { type: 'DesignValue', valuesByBreakpoint: { desktop: ssrClassName } }, + }, + children: [], + }; + + render( + , + ); + + expect(screen.getByTestId('contentful-container')).toHaveClass( + `${ssrClassName} contentful-container`, + ); + }); - it('renders assembly node with SSR class', () => { - const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; - const unboundValueKey = 'some-unbound-value-key'; - const assemblyEntry = createAssemblyEntry({ - id: defaultAssemblyId, - schemaVersion: '2023-09-28', + it('renders container node', () => { + const ssrClassName = 'cfstyles-4a59232681bf8491154b4c4ec81ea113'; + const containerNode: ComponentTreeNode = { + definitionId: CONTENTFUL_COMPONENTS.container.id, + variables: { + cfSsrClassName: { type: 'DesignValue', valuesByBreakpoint: { desktop: ssrClassName } }, + }, + children: [], + }; + + render( + , + ); + + expect(screen.getByTestId('contentful-container')).toHaveClass( + `${ssrClassName} contentful-container`, + ); }); - const updatedExperienceEntry = { - ...experienceEntry, - fields: { - ...experienceEntry.fields, - usedComponents: [assemblyEntry], - unboundValues: { - [unboundValueKey]: { - value: 'New year eve', + + it('renders nested components', () => { + const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; + const sectionNode: ComponentTreeNode = { + definitionId: CONTENTFUL_COMPONENTS.section.id, + variables: { + cfSsrClassName: { + type: 'DesignValue', + valuesByBreakpoint: { desktop: ssrClassName }, }, }, - }, - } as ExperienceEntry; - - const entityStore = new EntityStore({ - experienceEntry: updatedExperienceEntry as unknown as Entry, - entities: [...entries, ...assets], - locale: 'en-US', + children: [ + { + definitionId: 'custom-component', + variables: { + text: { type: 'UnboundValue', key: 'value1' }, + cfSsrClassName: { + type: 'DesignValue', + valuesByBreakpoint: { desktop: ssrClassName }, + }, + }, + children: [], + }, + ], + }; + + render( + , + ); + + expect(screen.getByTestId('contentful-container')).toHaveClass( + `${ssrClassName} contentful-container`, + ); + expect(screen.getByTestId('contentful-container').firstChild).toHaveClass(ssrClassName); }); - const assemblyNode: ComponentTreeNode = { - definitionId: defaultAssemblyId, - variables: { - [assemblyGeneratedVariableName]: { type: 'UnboundValue', key: unboundValueKey }, - cfSsrClassName: { - type: 'DesignValue', - valuesByBreakpoint: { desktop: ssrClassName }, + it('renders pattern node', () => { + const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; + const unboundValueKey = 'some-unbound-value-key'; + const patternEntry = createAssemblyEntry(); + const updatedExperienceEntry = { + ...experienceEntry, + fields: { + ...experienceEntry.fields, + usedComponents: [patternEntry], + unboundValues: { + [unboundValueKey]: { + value: 'New year eve', + }, + }, }, - }, - children: [], - }; - - const { getByTestId, getByText } = render( - , - ); - - expect(getByTestId('assembly')).toHaveClass(ssrClassName); - expect(getByTestId('contentful-container')).toBeInTheDocument(); - expect(getByText('New year eve')).toBeInTheDocument(); - }); - - it('renders nested patterns with SSR class', () => { - const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; - const unboundValueKey = 'some-unbound-value-key'; - const assemblyEntry = createAssemblyEntry({ - id: defaultAssemblyId, - schemaVersion: '2023-09-28', - }); - const nestedAssemblyEntry = createAssemblyEntry({ - id: 'nested-assembly-id', - schemaVersion: '2023-09-28', - }); - const updatedExperienceEntry = { - ...experienceEntry, - fields: { - ...experienceEntry.fields, - usedComponents: [assemblyEntry, nestedAssemblyEntry], - unboundValues: { - [unboundValueKey]: { - value: 'Nested pattern value', + } as ExperienceEntry; + + const entityStore = new EntityStore({ + experienceEntry: updatedExperienceEntry, + entities: [...entries, ...assets], + locale: 'en-US', + }); + + const patternNode: ComponentTreeNode = { + definitionId: defaultAssemblyId, + variables: { + [assemblyGeneratedVariableName]: { type: 'UnboundValue', key: unboundValueKey }, + cfSsrClassName: { + type: 'DesignValue', + valuesByBreakpoint: { desktop: ssrClassName }, }, }, - }, - } as ExperienceEntry; - - const entityStore = new EntityStore({ - experienceEntry: updatedExperienceEntry as unknown as Entry, - entities: [...entries, ...assets], - locale: 'en-US', + children: [], + }; + + render( + , + ); + + expect(screen.getByTestId('assembly')).toHaveClass(ssrClassName); + expect(screen.getByTestId('contentful-container')).toBeInTheDocument(); + expect(screen.getByText('New year eve')).toBeInTheDocument(); }); - const nestedAssemblyNode: ComponentTreeNode = { - definitionId: 'nested-assembly-id', - variables: { - [assemblyGeneratedVariableName]: { type: 'UnboundValue', key: unboundValueKey }, - cfSsrClassName: { - type: 'DesignValue', - valuesByBreakpoint: { desktop: ssrClassName }, + it('renders nested patterns', () => { + const ssrClassName = 'cfstyles-3da2d7a8871905d8079c313b36bcf404'; + const unboundValueKey = 'some-unbound-value-key'; + const patternEntry = createAssemblyEntry(); + const nestedPatternEntry = createAssemblyEntry({ + id: 'nested-pattern-id', + }); + const updatedExperienceEntry = { + ...experienceEntry, + fields: { + ...experienceEntry.fields, + usedComponents: [patternEntry, nestedPatternEntry], + unboundValues: { + [unboundValueKey]: { + value: 'Nested pattern value', + }, + }, }, - }, - children: [], - }; - - const assemblyNode: ComponentTreeNode = { - definitionId: defaultAssemblyId, - variables: { - [assemblyGeneratedVariableName]: { type: 'UnboundValue', key: unboundValueKey }, - cfSsrClassName: { - type: 'DesignValue', - valuesByBreakpoint: { desktop: ssrClassName }, + } as ExperienceEntry; + + const entityStore = new EntityStore({ + experienceEntry: updatedExperienceEntry, + entities: [...entries, ...assets], + locale: 'en-US', + }); + + const nestedPatternNode: ComponentTreeNode = { + definitionId: 'nested-pattern-id', + variables: { + [assemblyGeneratedVariableName]: { type: 'UnboundValue', key: unboundValueKey }, + cfSsrClassName: { + type: 'DesignValue', + valuesByBreakpoint: { desktop: ssrClassName }, + }, }, - }, - children: [nestedAssemblyNode], - }; - - const { getByTestId, getByText } = render( - , - ); - - expect(getByTestId('assembly')).toHaveClass(ssrClassName); - expect(getByTestId('assembly').firstChild).toHaveClass(ssrClassName); - expect(getByText('Nested pattern value')).toBeInTheDocument(); + children: [], + }; + + const patternNode: ComponentTreeNode = { + definitionId: defaultAssemblyId, + variables: { + [assemblyGeneratedVariableName]: { type: 'UnboundValue', key: unboundValueKey }, + cfSsrClassName: { + type: 'DesignValue', + valuesByBreakpoint: { desktop: ssrClassName }, + }, + }, + children: [], + }; + + patternEntry.fields.componentTree.children = [nestedPatternNode]; + + render( + , + ); + + expect(screen.getAllByTestId('assembly')).toHaveLength(2); + expect(screen.getAllByTestId('assembly')[0]).toHaveClass(ssrClassName); + expect(screen.getAllByTestId('assembly')[1]).toHaveClass(ssrClassName); + expect(screen.getAllByTestId('assembly')[1].firstChild).toHaveClass(ssrClassName); + expect(screen.getByText('Nested pattern value')).toBeInTheDocument(); + }); }); }); diff --git a/packages/experience-builder-sdk/test/__fixtures__/assembly.ts b/packages/experience-builder-sdk/test/__fixtures__/assembly.ts index acab4646a..c401b0b82 100644 --- a/packages/experience-builder-sdk/test/__fixtures__/assembly.ts +++ b/packages/experience-builder-sdk/test/__fixtures__/assembly.ts @@ -5,15 +5,17 @@ import { } from '@contentful/experiences-core/constants'; import type { ExperienceComponentSettings, + ExperienceEntry, ExperienceTreeNode, SchemaVersions, } from '@contentful/experiences-core/types'; type createAssemblyEntryArgs = { - schemaVersion: SchemaVersions; - id: string; + schemaVersion?: SchemaVersions; + id?: string; }; +// TODO: Rename to TEST_PATTERN_ID export const defaultAssemblyId = 'assembly-id'; export const assemblyGeneratedVariableName = 'text_uuid1Assembly'; @@ -21,7 +23,7 @@ export const assemblyGeneratedDesignVariableName = 'cfWidth_uuid2Assembly'; export const createAssemblyEntry = ({ schemaVersion = LATEST_SCHEMA_VERSION, id = defaultAssemblyId, -}: createAssemblyEntryArgs) => { +}: createAssemblyEntryArgs = {}): ExperienceEntry => { return { sys: { id, diff --git a/packages/experience-builder-sdk/test/__fixtures__/composition.ts b/packages/experience-builder-sdk/test/__fixtures__/composition.ts index 809d8ce28..2c51ea2f5 100644 --- a/packages/experience-builder-sdk/test/__fixtures__/composition.ts +++ b/packages/experience-builder-sdk/test/__fixtures__/composition.ts @@ -51,6 +51,7 @@ const compositionFields: ExperienceEntry['fields'] = { }, }; +// TODO: Turn global fixture into a factory method to avoid cloning data on the fly export const experienceEntry: ExperienceEntry = { sys: { id: 'composition-id', diff --git a/packages/experience-builder-sdk/testing-library.js b/packages/experience-builder-sdk/testing-library.js index ab93aab84..6ff0edf29 100644 --- a/packages/experience-builder-sdk/testing-library.js +++ b/packages/experience-builder-sdk/testing-library.js @@ -16,3 +16,17 @@ console.debug = (message, ...args) => { global.CSS = { supports: (k, v) => true, }; + +global.structuredClone = (val) => JSON.parse(JSON.stringify(val)); + +// Monkey patch errors thrown by Jest trying to parse CSS. +// Src: https://stackoverflow.com/questions/69906136/console-error-error-could-not-parse-css-stylesheet +const originalConsoleError = console.error; +console.error = function (...data) { + if ( + typeof data[0]?.toString === 'function' && + data[0].toString().includes('Error: Could not parse CSS stylesheet') + ) + return; + originalConsoleError(...data); +};