diff --git a/packages/core/src/hydration/error_handling.ts b/packages/core/src/hydration/error_handling.ts index ccfe1fdffbb8..effd19bdee95 100644 --- a/packages/core/src/hydration/error_handling.ts +++ b/packages/core/src/hydration/error_handling.ts @@ -64,13 +64,17 @@ export function validateMatchingNode( tNode: TNode, isViewContainerAnchor = false, ): void { + const hasStaticTextContentMismatch = hasTextContentMismatch(node, tNode); + if ( !node || (node as Node).nodeType !== nodeType || + hasStaticTextContentMismatch || ((node as Node).nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName.toLowerCase() !== tagName?.toLowerCase()) ) { - const expectedNode = shortRNodeDescription(nodeType, tagName, null); + const expectedTextContent = hasStaticTextContentMismatch ? tNode.value : null; + const expectedNode = shortRNodeDescription(nodeType, tagName, expectedTextContent); let header = `During hydration Angular expected ${expectedNode} but `; const hostComponentDef = getDeclarationComponentDef(lView); @@ -120,6 +124,16 @@ export function validateMatchingNode( } } +function hasTextContentMismatch(node: RNode | null, tNode: TNode): boolean { + return ( + node !== null && + (node as Node).nodeType === Node.TEXT_NODE && + tNode.type === TNodeType.Text && + tNode.value.length > 0 && + (node as Node).textContent !== tNode.value + ); +} + /** * Validates that a given node has sibling nodes */ diff --git a/packages/platform-server/test/full_app_hydration_spec.ts b/packages/platform-server/test/full_app_hydration_spec.ts index ca0c1c58924a..b62341fc4e30 100644 --- a/packages/platform-server/test/full_app_hydration_spec.ts +++ b/packages/platform-server/test/full_app_hydration_spec.ts @@ -6174,6 +6174,41 @@ describe('platform-server full application hydration integration', () => { }); }); + it('should detect static text content mismatch', async () => { + @Component({ + selector: 'app', + template: ` +