From ddde92a12e04c87efe5a3840d03c2eff58c1dfbb Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 15 Sep 2026 09:22:04 +0200 Subject: [PATCH 1/2] Adding docx unit tests and moving existing tests to proper package --- .../quadient/migration/example}/Utils.groovy | 2 + .../AttachmentsMappingExportTest.groovy | 4 +- .../AttachmentsMappingImportTest.groovy | 4 +- .../BaseTemplatesMappingExportTest.groovy | 4 +- .../BaseTemplatesMappingImportTest.groovy | 4 +- .../DisplayRulesMappingExportTest.groovy | 4 +- .../DisplayRulesMappingImportTest.groovy | 4 +- .../DocumentObjectsMappingExportTest.groovy | 4 +- .../DocumentObjectsMappingImportTest.groovy | 4 +- .../mapping}/ImagesMappingExportTest.groovy | 4 +- .../mapping}/ImagesMappingImportTest.groovy | 4 +- .../common/mapping}/LayoutExportTest.groovy | 4 +- .../common/mapping}/LayoutImportTest.groovy | 4 +- .../ParagraphStylesMappingExportTest.groovy | 4 +- .../ParagraphStylesMappingImportTest.groovy | 4 +- .../mapping}/TablesMappingExportTest.groovy | 4 +- .../mapping}/TablesMappingImportTest.groovy | 4 +- .../TextStylesMappingExportTest.groovy | 4 +- .../TextStylesMappingImportTest.groovy | 4 +- .../VariablesMappingExportTest.groovy | 4 +- .../VariablesMappingImportTest.groovy | 4 +- .../example/common/util}/CsvTest.groovy | 2 +- .../example/docx/DocxFieldFixtures.groovy | 35 +++ .../example/docx/DocxTestSupport.groovy | 35 +++ .../docx/parser/DocxAnchoredAreasTest.groovy | 120 +++++++++ .../docx/parser/DocxBodyContentTest.groovy | 159 ++++++++++++ .../docx/parser/DocxFloatingTablesTest.groovy | 63 +++++ .../docx/parser/DocxHeaderFootersTest.groovy | 47 ++++ .../example/docx/parser/DocxImagesTest.groovy | 79 ++++++ .../docx/parser/DocxMergeFieldsTest.groovy | 239 ++++++++++++++++++ .../docx/parser/DocxPageLayoutTest.groovy | 97 +++++++ .../docx/parser/DocxPageSectionsTest.groovy | 105 ++++++++ .../parser/DocxParagraphParserTest.groovy | 179 +++++++++++++ .../docx/parser/DocxSectionsTest.groovy | 172 +++++++++++++ .../ParagraphContentCollectorTest.groovy | 190 ++++++++++++++ .../docx/style/DocxParagraphStylesTest.groovy | 31 +++ .../docx/style/StyleChainResolverTest.groovy | 114 +++++++++ .../example/docx/util/DocxUtilsTest.groovy | 37 +++ 38 files changed, 1765 insertions(+), 21 deletions(-) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example}/Utils.groovy (98%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/AttachmentsMappingExportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/AttachmentsMappingImportTest.groovy (98%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/BaseTemplatesMappingExportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/BaseTemplatesMappingImportTest.groovy (98%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/DisplayRulesMappingExportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/DisplayRulesMappingImportTest.groovy (98%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/DocumentObjectsMappingExportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/DocumentObjectsMappingImportTest.groovy (98%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/ImagesMappingExportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/ImagesMappingImportTest.groovy (99%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/LayoutExportTest.groovy (99%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/LayoutImportTest.groovy (99%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/ParagraphStylesMappingExportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/ParagraphStylesMappingImportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/TablesMappingExportTest.groovy (98%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/TablesMappingImportTest.groovy (95%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/TextStylesMappingExportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/TextStylesMappingImportTest.groovy (97%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/VariablesMappingExportTest.groovy (98%) rename migration-examples/src/test/groovy/{ => com/quadient/migration/example/common/mapping}/VariablesMappingImportTest.groovy (99%) rename migration-examples/src/test/groovy/{utils => com/quadient/migration/example/common/util}/CsvTest.groovy (96%) create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxFieldFixtures.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxFloatingTablesTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxMergeFieldsTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageLayoutTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageSectionsTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/DocxParagraphStylesTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/StyleChainResolverTest.groovy create mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/util/DocxUtilsTest.groovy diff --git a/migration-examples/src/test/groovy/Utils.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/Utils.groovy similarity index 98% rename from migration-examples/src/test/groovy/Utils.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/Utils.groovy index c603eba8..d35a058e 100644 --- a/migration-examples/src/test/groovy/Utils.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/Utils.groovy @@ -1,3 +1,5 @@ +package com.quadient.migration.example + import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.Migration import com.quadient.migration.api.ProjectConfig diff --git a/migration-examples/src/test/groovy/AttachmentsMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/AttachmentsMappingExportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/AttachmentsMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/AttachmentsMappingExportTest.groovy index 4fd0fc70..c2385944 100644 --- a/migration-examples/src/test/groovy/AttachmentsMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/AttachmentsMappingExportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.data.Active -import com.quadient.migration.example.common.mapping.AttachmentsExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.AttachmentType import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.SkipOptions diff --git a/migration-examples/src/test/groovy/AttachmentsMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/AttachmentsMappingImportTest.groovy similarity index 98% rename from migration-examples/src/test/groovy/AttachmentsMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/AttachmentsMappingImportTest.groovy index d36ab3f2..7a14f296 100644 --- a/migration-examples/src/test/groovy/AttachmentsMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/AttachmentsMappingImportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.* import com.quadient.migration.api.dto.migrationmodel.builder.AttachmentBuilder -import com.quadient.migration.example.common.mapping.AttachmentsImport +import com.quadient.migration.example.Utils import com.quadient.migration.service.deploy.utility.ResourceType import com.quadient.migration.shared.AttachmentType import com.quadient.migration.shared.SkipOptions diff --git a/migration-examples/src/test/groovy/BaseTemplatesMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/BaseTemplatesMappingExportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/BaseTemplatesMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/BaseTemplatesMappingExportTest.groovy index eb6dce7d..dc360c66 100644 --- a/migration-examples/src/test/groovy/BaseTemplatesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/BaseTemplatesMappingExportTest.groovy @@ -1,9 +1,11 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.BaseTemplate import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.data.Active -import com.quadient.migration.example.common.mapping.BaseTemplatesExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test diff --git a/migration-examples/src/test/groovy/BaseTemplatesMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/BaseTemplatesMappingImportTest.groovy similarity index 98% rename from migration-examples/src/test/groovy/BaseTemplatesMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/BaseTemplatesMappingImportTest.groovy index 564e47e9..7ef3ee5f 100644 --- a/migration-examples/src/test/groovy/BaseTemplatesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/BaseTemplatesMappingImportTest.groovy @@ -1,9 +1,11 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder import com.quadient.migration.data.Active -import com.quadient.migration.example.common.mapping.BaseTemplatesImport +import com.quadient.migration.example.Utils import com.quadient.migration.service.deploy.utility.ResourceType import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir diff --git a/migration-examples/src/test/groovy/DisplayRulesMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DisplayRulesMappingExportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/DisplayRulesMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DisplayRulesMappingExportTest.groovy index 31fa8deb..bf7d7fa0 100644 --- a/migration-examples/src/test/groovy/DisplayRulesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DisplayRulesMappingExportTest.groovy @@ -1,3 +1,5 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRule @@ -6,7 +8,7 @@ import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.data.Active -import com.quadient.migration.example.common.mapping.DisplayRulesExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test diff --git a/migration-examples/src/test/groovy/DisplayRulesMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DisplayRulesMappingImportTest.groovy similarity index 98% rename from migration-examples/src/test/groovy/DisplayRulesMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DisplayRulesMappingImportTest.groovy index 7f6bc8f6..bad69a30 100644 --- a/migration-examples/src/test/groovy/DisplayRulesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DisplayRulesMappingImportTest.groovy @@ -1,3 +1,5 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef @@ -5,7 +7,7 @@ import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.DisplayRuleBuilder import com.quadient.migration.data.Active -import com.quadient.migration.example.common.mapping.DisplayRulesImport +import com.quadient.migration.example.Utils import com.quadient.migration.service.deploy.utility.ResourceType import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir diff --git a/migration-examples/src/test/groovy/DocumentObjectsMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsMappingExportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/DocumentObjectsMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsMappingExportTest.groovy index d0d33da2..713a1e42 100644 --- a/migration-examples/src/test/groovy/DocumentObjectsMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsMappingExportTest.groovy @@ -1,6 +1,8 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.data.Active -import com.quadient.migration.example.common.mapping.DocumentObjectsExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.io.TempDir diff --git a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsMappingImportTest.groovy similarity index 98% rename from migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsMappingImportTest.groovy index b3b192fc..6b20beeb 100644 --- a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsMappingImportTest.groovy @@ -1,9 +1,11 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder -import com.quadient.migration.example.common.mapping.DocumentObjectsImport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.SkipOptions import org.junit.jupiter.api.io.TempDir diff --git a/migration-examples/src/test/groovy/ImagesMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ImagesMappingExportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/ImagesMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ImagesMappingExportTest.groovy index 8b020e73..a586912e 100644 --- a/migration-examples/src/test/groovy/ImagesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ImagesMappingExportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.data.Active -import com.quadient.migration.example.common.mapping.ImagesExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.ImageType import com.quadient.migration.shared.SkipOptions diff --git a/migration-examples/src/test/groovy/ImagesMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ImagesMappingImportTest.groovy similarity index 99% rename from migration-examples/src/test/groovy/ImagesMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ImagesMappingImportTest.groovy index 360bd79e..562cdaf8 100644 --- a/migration-examples/src/test/groovy/ImagesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ImagesMappingImportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.* import com.quadient.migration.api.dto.migrationmodel.builder.ImageBuilder -import com.quadient.migration.example.common.mapping.ImagesImport +import com.quadient.migration.example.Utils import com.quadient.migration.service.deploy.utility.ResourceType import com.quadient.migration.shared.ImageType import com.quadient.migration.shared.SkipOptions diff --git a/migration-examples/src/test/groovy/LayoutExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/LayoutExportTest.groovy similarity index 99% rename from migration-examples/src/test/groovy/LayoutExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/LayoutExportTest.groovy index 0ee8d424..4c5048d3 100644 --- a/migration-examples/src/test/groovy/LayoutExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/LayoutExportTest.groovy @@ -1,10 +1,12 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.* import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ImageBuilder import com.quadient.migration.api.dto.migrationmodel.builder.documentcontent.AreaBuilder import com.quadient.migration.api.repository.DocumentObjectRepository -import com.quadient.migration.example.common.mapping.LayoutExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.BaseTemplateArea import com.quadient.migration.shared.BaseTemplatePage import com.quadient.migration.shared.DocumentObjectType diff --git a/migration-examples/src/test/groovy/LayoutImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/LayoutImportTest.groovy similarity index 99% rename from migration-examples/src/test/groovy/LayoutImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/LayoutImportTest.groovy index 117fb7d0..91e05321 100644 --- a/migration-examples/src/test/groovy/LayoutImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/LayoutImportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.* import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.documentcontent.AreaBuilder -import com.quadient.migration.example.common.mapping.LayoutImport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.BaseTemplateArea import com.quadient.migration.shared.BaseTemplatePage import com.quadient.migration.shared.DocumentObjectType diff --git a/migration-examples/src/test/groovy/ParagraphStylesMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesMappingExportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/ParagraphStylesMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesMappingExportTest.groovy index 04113a3c..d1f90182 100644 --- a/migration-examples/src/test/groovy/ParagraphStylesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesMappingExportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.* import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleDefinitionBuilder -import com.quadient.migration.example.common.mapping.ParagraphStylesExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.Alignment import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.Size diff --git a/migration-examples/src/test/groovy/ParagraphStylesMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesMappingImportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/ParagraphStylesMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesMappingImportTest.groovy index c8036a03..e489fd38 100644 --- a/migration-examples/src/test/groovy/ParagraphStylesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/ParagraphStylesMappingImportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleDefinitionBuilder -import com.quadient.migration.example.common.mapping.ParagraphStylesImport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.Alignment import com.quadient.migration.shared.LineSpacing import com.quadient.migration.shared.Size diff --git a/migration-examples/src/test/groovy/TablesMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TablesMappingExportTest.groovy similarity index 98% rename from migration-examples/src/test/groovy/TablesMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TablesMappingExportTest.groovy index 7017417b..ce509024 100644 --- a/migration-examples/src/test/groovy/TablesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TablesMappingExportTest.groovy @@ -1,6 +1,8 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TableBuilder -import com.quadient.migration.example.common.mapping.TablesExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.TableAction import com.quadient.migration.shared.TablePdfTaggingRule diff --git a/migration-examples/src/test/groovy/TablesMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TablesMappingImportTest.groovy similarity index 95% rename from migration-examples/src/test/groovy/TablesMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TablesMappingImportTest.groovy index 6c8d245f..fe7e895f 100644 --- a/migration-examples/src/test/groovy/TablesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TablesMappingImportTest.groovy @@ -1,7 +1,9 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TableBuilder -import com.quadient.migration.example.common.mapping.TablesImport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.TableAction import com.quadient.migration.shared.TablePdfTaggingRule diff --git a/migration-examples/src/test/groovy/TextStylesMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TextStylesMappingExportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/TextStylesMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TextStylesMappingExportTest.groovy index f7820728..d67e9edd 100644 --- a/migration-examples/src/test/groovy/TextStylesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TextStylesMappingExportTest.groovy @@ -1,7 +1,9 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleDefinitionBuilder -import com.quadient.migration.example.common.mapping.TextStylesExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.Size import com.quadient.migration.shared.SuperOrSubscript diff --git a/migration-examples/src/test/groovy/TextStylesMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TextStylesMappingImportTest.groovy similarity index 97% rename from migration-examples/src/test/groovy/TextStylesMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TextStylesMappingImportTest.groovy index 832b36e4..d1a552c1 100644 --- a/migration-examples/src/test/groovy/TextStylesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/TextStylesMappingImportTest.groovy @@ -1,8 +1,10 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.TextStyleDefinitionBuilder -import com.quadient.migration.example.common.mapping.TextStylesImport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.Color import com.quadient.migration.shared.Size import com.quadient.migration.shared.SuperOrSubscript diff --git a/migration-examples/src/test/groovy/VariablesMappingExportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/VariablesMappingExportTest.groovy similarity index 98% rename from migration-examples/src/test/groovy/VariablesMappingExportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/VariablesMappingExportTest.groovy index a65eb019..0fdc990f 100644 --- a/migration-examples/src/test/groovy/VariablesMappingExportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/VariablesMappingExportTest.groovy @@ -1,10 +1,12 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.Variable import com.quadient.migration.api.dto.migrationmodel.VariableRef import com.quadient.migration.api.dto.migrationmodel.VariableStructure import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder -import com.quadient.migration.example.common.mapping.VariablesExport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DataType import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.VariablePathData diff --git a/migration-examples/src/test/groovy/VariablesMappingImportTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/VariablesMappingImportTest.groovy similarity index 99% rename from migration-examples/src/test/groovy/VariablesMappingImportTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/VariablesMappingImportTest.groovy index bb964e6d..ad143b5d 100644 --- a/migration-examples/src/test/groovy/VariablesMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/mapping/VariablesMappingImportTest.groovy @@ -1,9 +1,11 @@ +package com.quadient.migration.example.common.mapping + import com.quadient.migration.api.Migration import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.MappingItem import com.quadient.migration.api.dto.migrationmodel.Variable import com.quadient.migration.api.dto.migrationmodel.VariableRef -import com.quadient.migration.example.common.mapping.VariablesImport +import com.quadient.migration.example.Utils import com.quadient.migration.shared.DataType import com.quadient.migration.shared.VariablePathData import com.quadient.migration.shared.VariableRefPath diff --git a/migration-examples/src/test/groovy/utils/CsvTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/common/util/CsvTest.groovy similarity index 96% rename from migration-examples/src/test/groovy/utils/CsvTest.groovy rename to migration-examples/src/test/groovy/com/quadient/migration/example/common/util/CsvTest.groovy index eda94702..9124d3b9 100644 --- a/migration-examples/src/test/groovy/utils/CsvTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/common/util/CsvTest.groovy @@ -1,4 +1,4 @@ -package utils +package com.quadient.migration.example.common.utils import com.quadient.migration.example.common.util.Csv import org.junit.jupiter.api.Assertions diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxFieldFixtures.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxFieldFixtures.groovy new file mode 100644 index 00000000..e12db769 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxFieldFixtures.groovy @@ -0,0 +1,35 @@ +package com.quadient.migration.example.docx + +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType + +/** Builds Word field markup shared by the DOCX parser tests. */ +class DocxFieldFixtures { + + static void appendMergeField(paragraph, String mergeInstruction, String cachedResult = null) { + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), mergeInstruction) + if (cachedResult != null) { + fieldCharacter(paragraph.createRun(), STFldCharType.SEPARATE) + paragraph.createRun().setText(cachedResult) + } + fieldCharacter(paragraph.createRun(), STFldCharType.END) + } + + static void appendEqualityIfField(paragraph, String variable, String operand, String result) { + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " IF ") + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " MERGEFIELD ${variable} ") + fieldCharacter(paragraph.createRun(), STFldCharType.END) + instruction(paragraph.createRun(), " = ${operand} \"${result}\" \"\" ") + fieldCharacter(paragraph.createRun(), STFldCharType.END) + } + + static void fieldCharacter(run, STFldCharType.Enum type) { + run.CTR.addNewFldChar().setFldCharType(type) + } + + static void instruction(run, String value) { + run.CTR.addNewInstrText().setStringValue(value) + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy new file mode 100644 index 00000000..a623f20a --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy @@ -0,0 +1,35 @@ +package com.quadient.migration.example.docx + +import com.quadient.migration.api.Migration +import com.quadient.migration.api.ProjectConfig +import com.quadient.migration.api.repository.* +import com.quadient.migration.service.Storage + +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when + +class DocxTestSupport { + /** Creates fresh, ordinary mocks for the services used by the DOCX parser. */ + static Migration mockMigration(Map context = [:]) { + def migration = mock(Migration) + def variables = mock(VariableRepository) + def displayRules = mock(DisplayRuleRepository) + def documentObjects = mock(DocumentObjectRepository) + def textStyles = mock(TextStyleRepository) + def paragraphStyles = mock(ParagraphStyleRepository) + def images = mock(ImageRepository) + def storage = mock(Storage) + def projectConfig = mock(ProjectConfig) + + when(migration.variableRepository).thenReturn(variables) + when(migration.displayRuleRepository).thenReturn(displayRules) + when(migration.documentObjectRepository).thenReturn(documentObjects) + when(migration.textStyleRepository).thenReturn(textStyles) + when(migration.paragraphStyleRepository).thenReturn(paragraphStyles) + when(migration.imageRepository).thenReturn(images) + when(migration.storage).thenReturn(storage) + when(migration.projectConfig).thenReturn(projectConfig) + when(projectConfig.context).thenReturn(context) + return migration + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy new file mode 100644 index 00000000..f8da7c85 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy @@ -0,0 +1,120 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.shared.Position +import com.quadient.migration.shared.Size +import org.apache.poi.common.usermodel.PictureType +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.apache.poi.xwpf.usermodel.XWPFPictureData +import org.apache.xmlbeans.XmlObject +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTAnchor +import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STAlignH +import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromH +import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromV + +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when +import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration + +class DocxAnchoredAreasTest { + @Test + void "page-sized image is a background and the same embed is suppressed from floating areas across pages"() { + // given: the same image appears as a small anchor and a duplicated background on another page + DocxImages.resetImageState() + try { + new XWPFDocument().withCloseable { document -> + def small = document.createParagraph() + addPictureAnchor(small, 1, 100, 50) + def background = document.createParagraph() + addPictureAnchor(background, 2, 360, 480) // Exactly 60 percent on both axes. + addPictureAnchor(background, 2, 360, 480) // Duplicate drawing in the same page. + def firstPage = page() + firstPage.sections = [new DocxSection(elements: [small])] + def secondPage = page() + secondPage.index = 1 + secondPage.sections = [new DocxSection(elements: [background])] + def source = mock(XWPFDocument) + def data = mock(XWPFPictureData) + when(source.getPictureDataByID('rId1')).thenReturn(data) + when(data.pictureTypeEnum).thenReturn(PictureType.PNG) + when(data.checksum).thenReturn(42L) + when(data.data).thenReturn([1, 2, 3] as byte[]) + def migration = mockMigration() + // when: anchors are extracted across both pages + def result = DocxAnchoredAreas.extract(migration, source, 'sample', [firstPage, secondPage]) + // then: only one full-page background is emitted and its embed is consumed + assert result.backgroundAreasByPage.keySet() == [1] as Set + assert result.backgroundAreasByPage[1].size() == 1 + assert result.floatingAreasByPage.isEmpty() + assert result.consumedEmbedIds == ['rId1'] as Set + def position = result.backgroundAreasByPage[1][0].position + assert [position.x, position.y, position.width, position.height]*.toPoints() == [0d, 0d, 600d, 800d] + } + } finally { + DocxImages.resetImageState() + } + } + + private static void addPictureAnchor(def paragraph, long id, long width, long height) { + def anchor = paragraph.createRun().CTR.addNewDrawing().addNewAnchor() + anchor.addNewDocPr().id = id + anchor.addNewExtent().cx = width * 12700L + anchor.extent.cy = height * 12700L + def graphicData = anchor.addNewGraphic().addNewGraphicData() + graphicData.set(XmlObject.Factory.parse(''' + + ''')) + graphicData.uri = 'http://schemas.openxmlformats.org/drawingml/2006/picture' + } + + @ParameterizedTest + @CsvSource(['page,10,20', 'margin,50,80']) + void "anchor offsets use the selected coordinate origin"(String origin, double x, double y) { + // given: an anchor with explicit offsets relative to the page or margins + def anchor = anchor() + anchor.addNewPositionH().relativeFrom = STRelFromH.Enum.forString(origin) + anchor.positionH.posOffset = 127000 + anchor.addNewPositionV().relativeFrom = STRelFromV.Enum.forString(origin) + anchor.positionV.posOffset = 254000 + // when + def position = DocxAnchoredAreas.resolveAnchorPosition(anchor, page()) + // then + assert [position.x, position.y, position.width, position.height]*.toPoints() == [x, y, 100d, 50d] + } + + @ParameterizedTest + @CsvSource(['page,left,0', 'page,center,250', 'page,right,500', 'margin,left,40', 'margin,center,240', 'margin,right,440', 'margin,outside,440']) + void "horizontal alignment accounts for object width and available bounds"(String origin, String alignment, double x) { + // given: an alignment and coordinate origin from the parameterized case + def anchor = anchor() + anchor.addNewPositionH().relativeFrom = STRelFromH.Enum.forString(origin) + anchor.positionH.align = STAlignH.Enum.forString(alignment) + // when / then: resolving the position places the anchor within the selected bounds + assert DocxAnchoredAreas.resolveAnchorPosition(anchor, page()).x.toPoints() == x + } + + @Test + void "missing anchor geometry falls back to content origin and zero extent"() { + // given / when: resolve an empty anchor against a page with margins + def position = DocxAnchoredAreas.resolveAnchorPosition(CTAnchor.Factory.newInstance(), page()) + // then + assert [position.x, position.y, position.width, position.height]*.toPoints() == [40d, 60d, 0d, 0d] + } + + private static CTAnchor anchor() { + def anchor = CTAnchor.Factory.newInstance() + anchor.addNewExtent().cx = 1270000L + anchor.extent.cy = 635000L + return anchor + } + + private static DocxPage page() { + return new DocxPage(width: Size.ofPoints(600), height: Size.ofPoints(800), + contentPosition: new Position(Size.ofPoints(40), Size.ofPoints(60), Size.ofPoints(500), Size.ofPoints(680))) + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy new file mode 100644 index 00000000..7b76f732 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy @@ -0,0 +1,159 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.api.dto.migrationmodel.DisplayRule +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef +import com.quadient.migration.api.dto.migrationmodel.Table +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.apache.poi.xwpf.usermodel.XWPFParagraph +import org.apache.poi.xwpf.usermodel.XWPFTable +import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType + +import static org.mockito.Mockito.verify +import static com.quadient.migration.example.docx.DocxFieldFixtures.* +import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration + +class DocxBodyContentTest { + + @Test + void "tables wrapped in body-level IF fields become conditional rows of the preceding table"() { + // given + def migration = mockMigration() + def displayRules = migration.displayRuleRepository + + def document = new XWPFDocument() + document.createStyles() + def headerTable = createTable(document, ["Benefit", "Amount"]) + setGrid(headerTable, [2190L, 1525L]) + // ¶ IF «Ben02Type» = "SCH" " IF «BenefitCount» > 1 " ¶ "" "" [sep]
¶ [end] [end] + def opening = document.createParagraph() + fieldCharacter(opening.createRun(), STFldCharType.BEGIN) + instruction(opening.createRun(), " if ") + appendMergeField(opening, " MERGEFIELD Ben02Type ") + instruction(opening.createRun(), ' = "SCH" ') + fieldCharacter(opening.createRun(), STFldCharType.BEGIN) + instruction(opening.createRun(), " if ") + appendMergeField(opening, " MERGEFIELD BenefitCount ", "2") + instruction(opening.createRun(), ' > 1 "') + def conditionalTable = createTable(document, ["", "596.75"]) + // Word stores the text of a table nested in a field instruction as instrText; grid widths drift by a twip. + instruction(conditionalTable.getRow(0).getCell(0).paragraphs[0].createRun(), "Ben02") + setGrid(conditionalTable, [2191L, 1525L]) + def middle = document.createParagraph() + instruction(middle.createRun(), '"" "" ') + fieldCharacter(middle.createRun(), STFldCharType.SEPARATE) + def cachedResultTable = createTable(document, ["Ben02 cached", "596.75"]) + def closing = document.createParagraph() + fieldCharacter(closing.createRun(), STFldCharType.END) + instruction(closing.createRun(), " ") + fieldCharacter(closing.createRun(), STFldCharType.END) + def trailing = document.createParagraph() + trailing.createRun().setText("After the table") + + // when + def body = new DocxBodyContent(migration, "sample", "sample_page1") + def state = new FieldParseState(conditionalTableHandler: body.&addConditionalTable) + def headerBuilder = DocxTableParser.createTableBuilder(migration, headerTable, true) + DocxTableParser.addRows(migration, headerBuilder, headerTable, headerTable.rows, "sample", 2, true) + body.addTable(headerTable, headerBuilder, 2) + document.bodyElements.each { elem -> + if (elem instanceof XWPFParagraph) { + def paragraph = DocxParagraphParser.parseFlowParagraph(migration, elem, "sample", state) + if (paragraph != null) { + body.add(paragraph) + } + } else if (elem instanceof XWPFTable && elem != headerTable) { + if (state.depth > 0) { + DocxMergeFields.handleTableInsideField(state, elem) + } else { + body.add(DocxTableParser.buildTable(migration, elem, elem.rows, "sample", 2, true)) + } + } + } + def content = body.flow() + + // then + assert content.size() == 2 + Table table = content[0] as Table + assert table.rows.size() == 2 + assert table.rows[0].displayRuleRef == null + assert table.rows[1].displayRuleRef.id.startsWith("docx_if_") + assert table.rows[1].cells[0].content[0].content[0].content[0].value == "Ben02" + assert content[1].content[0].content[0].value == "After the table" + def ruleCaptor = ArgumentCaptor.forClass(DisplayRule) + verify(displayRules).upsert(ruleCaptor.capture()) + assert ruleCaptor.value.name == 'Ben02Type = "SCH" AND BenefitCount > 1' + + document.close() + } + + @Test + void "conditional table not matching the preceding table is wrapped in an internal block with the rule"() { + // given + def migration = mockMigration() + def displayRules = migration.displayRuleRepository + def documentObjects = migration.documentObjectRepository + + def document = new XWPFDocument() + document.createStyles() + // ¶ IF «GmpPayable» = "Y" "
¶ "" [end] + def opening = document.createParagraph() + fieldCharacter(opening.createRun(), STFldCharType.BEGIN) + instruction(opening.createRun(), " if ") + appendMergeField(opening, " MERGEFIELD GmpPayable ") + instruction(opening.createRun(), ' = "Y" "') + def table = createTable(document, ["", "Meaning"]) + instruction(table.getRow(0).getCell(0).paragraphs[0].createRun(), "Technical word") + def closing = document.createParagraph() + instruction(closing.createRun(), '" "" ') + fieldCharacter(closing.createRun(), STFldCharType.END) + + // when + def body = new DocxBodyContent(migration, "sample", "sample_page3") + def state = new FieldParseState(conditionalTableHandler: body.&addConditionalTable) + document.bodyElements.each { elem -> + if (elem instanceof XWPFParagraph) { + def paragraph = DocxParagraphParser.parseFlowParagraph(migration, elem, "sample", state) + if (paragraph != null) { + body.add(paragraph) + } + } else if (elem instanceof XWPFTable) { + DocxMergeFields.handleTableInsideField(state, elem) + } + } + def content = body.flow() + + // then + def ruleCaptor = ArgumentCaptor.forClass(DisplayRule) + verify(displayRules).upsert(ruleCaptor.capture()) + assert ruleCaptor.value.name == 'GmpPayable = "Y"' + assert content.size() == 1 + DocumentObjectRef ref = content[0] as DocumentObjectRef + assert ref.id == "sample_page3_table1" + assert ref.displayRuleRef.id == ruleCaptor.value.id + def blockCaptor = ArgumentCaptor.forClass(DocumentObject) + verify(documentObjects).upsert(blockCaptor.capture()) + assert blockCaptor.value.id == "sample_page3_table1" + assert blockCaptor.value.name == "Technical word table" + assert blockCaptor.value.internal + Table blockTable = blockCaptor.value.content[0] as Table + assert blockTable.rows.size() == 1 + assert blockTable.rows[0].displayRuleRef == null + assert blockTable.rows[0].cells[0].content[0].content[0].content[0].value == "Technical word" + + document.close() + } + + private static XWPFTable createTable(XWPFDocument document, List cellTexts) { + XWPFTable table = document.createTable(1, cellTexts.size()) + cellTexts.eachWithIndex { String cellText, int i -> table.getRow(0).getCell(i).setText(cellText) } + return table + } + + private static void setGrid(XWPFTable table, List widths) { + def grid = table.CTTbl.tblGrid ?: table.CTTbl.addNewTblGrid() + widths.each { grid.addNewGridCol().w = BigInteger.valueOf(it) } + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxFloatingTablesTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxFloatingTablesTest.groovy new file mode 100644 index 00000000..ab9ae398 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxFloatingTablesTest.groovy @@ -0,0 +1,63 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphBuilder +import com.quadient.migration.shared.Position +import com.quadient.migration.shared.Size +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STHAnchor +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVAnchor + +class DocxFloatingTablesTest { + @ParameterizedTest + @CsvSource(['page,10,20,720', 'margin,50,80,660']) + void "floating table uses anchor offsets and grid width"(String origin, double x, double y, double height) { + // given: a table that initially has no floating properties + new XWPFDocument().withCloseable { doc -> + def table = doc.createTable() + // when / then: the initial table is inline + assert !DocxFloatingTables.isFloating(table) + // given: floating offsets and two explicit grid column widths + def properties = table.CTTbl.tblPr.addNewTblpPr() + properties.horzAnchor = STHAnchor.Enum.forString(origin) + properties.vertAnchor = STVAnchor.Enum.forString(origin) + properties.tblpX = BigInteger.valueOf(200) + properties.tblpY = BigInteger.valueOf(400) + def grid = table.CTTbl.addNewTblGrid() + grid.addNewGridCol().w = BigInteger.valueOf(1000) + grid.addNewGridCol().w = BigInteger.valueOf(2000) + // when / then: adding positioning properties makes the table floating + assert DocxFloatingTables.isFloating(table) + // given: content for the floating area + def content = [new ParagraphBuilder().build()] + // when + def area = DocxFloatingTables.buildFloatingTableArea(table, content, pagePosition()) + // then: content is preserved and geometry uses offsets and the total grid width + assert area.content == content + assert [area.position.x, area.position.y, area.position.width, area.position.height]*.toPoints() == [x, y, 150d, height] + } + } + + @Test + void "table without grid uses content width and off-page table retains minimum height"() { + // given: a floating table below the page content, with no column grid + new XWPFDocument().withCloseable { doc -> + def table = doc.createTable() + def properties = table.CTTbl.tblPr.addNewTblpPr() + properties.tblpY = BigInteger.valueOf(20000) + // when / then: empty content does not create an area + assert DocxFloatingTables.buildFloatingTableArea(table, [], pagePosition()) == null + // when: nonempty content is supplied + def area = DocxFloatingTables.buildFloatingTableArea(table, [new ParagraphBuilder().build()], pagePosition()) + // then: width falls back to the page content and height remains positive + assert area.position.width.toPoints() == 500d + assert area.position.height.toPoints() == 10d + } + } + + private static Position pagePosition() { + return new Position(Size.ofPoints(40), Size.ofPoints(60), Size.ofPoints(500), Size.ofPoints(680)) + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy new file mode 100644 index 00000000..86fbb8da --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy @@ -0,0 +1,47 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.shared.Position +import com.quadient.migration.shared.Size +import org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.junit.jupiter.api.Test +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STHdrFtr + +import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration + +class DocxHeaderFootersTest { + @Test + void 'default header and footer become fixed areas at the page edges'() { + new XWPFDocument().withCloseable { document -> + document.createStyles() + def policy = new XWPFHeaderFooterPolicy(document) + policy.createHeader(STHdrFtr.DEFAULT).createParagraph().createRun().setText('Header text') + policy.createFooter(STHdrFtr.DEFAULT).createParagraph().createRun().setText('Footer text') + def bytes = new ByteArrayOutputStream() + document.write(bytes) + new XWPFDocument(new ByteArrayInputStream(bytes.toByteArray())).withCloseable { reopened -> + assert reopened.headerList.size() == 1 + assert reopened.footerList.size() == 1 + reopened.document.body.sectPr.addNewPgMar().header = 720 + def page = new DocxPage(index: 0, + sections: [new DocxSection(sectPr: reopened.document.body.sectPr)], + width: Size.ofPoints(600), height: Size.ofPoints(800), + contentPosition: new Position(Size.ofPoints(40), Size.ofPoints(60), Size.ofPoints(500), Size.ofPoints(680))) + + def migration = mockMigration() + def areas = DocxHeaderFooters.areas(migration, reopened, page, 'sample') + + assert areas.size() == 2 + assert areas[0].position.x.toPoints() == 40d + assert areas[0].position.y.toPoints() == 36d + assert areas[0].position.height.toPoints() == 24d + assert areas[1].position.x.toPoints() == 40d + assert areas[1].position.y.toPoints() == 740d + assert areas[1].position.height.toPoints() == 60d + assert areas*.content*.size() == [1, 1] + assert areas[0].content[0].content[0].content[0].value == 'Header text' + assert areas[1].content[0].content[0].content[0].value == 'Footer text' + } + } + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy new file mode 100644 index 00000000..9c0082d3 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy @@ -0,0 +1,79 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.api.dto.migrationmodel.Image +import com.quadient.migration.shared.ImageOptions +import com.quadient.migration.shared.Size +import org.apache.poi.common.usermodel.PictureType +import org.apache.poi.xwpf.usermodel.XWPFPictureData +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.mockito.ArgumentCaptor + +import static org.mockito.ArgumentMatchers.any +import static org.mockito.Mockito.* +import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration + +class DocxImagesTest { + @BeforeEach + @AfterEach + void resetImages() { DocxImages.resetImageState() } + + @ParameterizedTest + @CsvSource(['PNG,.png', 'JPEG,.jpg', 'CMYKJPEG,.jpg', 'GIF,.gif', 'BMP,.bmp', 'DIB,.bmp', 'TIFF,.tiff', 'SVG,.svg']) + void "supported images persist bytes and dimensions"(String type, String extension) { + // given: a supported image with explicit dimensions + def migration = mockMigration() + def data = picture(PictureType.valueOf(type), 42L) + def options = new ImageOptions(Size.ofPoints(120), Size.ofPoints(80)) + // when / then: registration assigns the first image identifier + assert DocxImages.registerImageData(migration, data, 'sample', options) == 'sample_img_1' + // then: bytes, source path and dimensions are persisted + verify(migration.storage).write('sample_img_1' + extension, data.data) + def captor = ArgumentCaptor.forClass(Image) + verify(migration.imageRepository).upsert(captor.capture()) + assert captor.value.sourcePath == 'sample_img_1' + extension + assert captor.value.options == options + } + + @Test + void "checksum deduplication avoids repeated writes and reset starts a new document"() { + // given: two image objects with the same checksum + def migration = mockMigration() + def first = picture(PictureType.PNG, 42L) + def duplicate = picture(PictureType.PNG, 42L) + // when / then: registering both images returns the same identifier + assert DocxImages.registerImageData(migration, first, 'sample', null) == 'sample_img_1' + assert DocxImages.registerImageData(migration, duplicate, 'sample', null) == 'sample_img_1' + // then: the duplicate causes no additional storage or repository write + verify(migration.imageRepository, times(1)).upsert(any(Image)) + verify(migration.storage, times(1)).write('sample_img_1.png', first.data) + // when: image state is reset for the next document + DocxImages.resetImageState() + // then: the same bytes are registered again, starting from image number one + assert DocxImages.registerImageData(migration, duplicate, 'next', null) == 'next_img_1' + verify(migration.imageRepository, times(2)).upsert(any(Image)) + } + + @Test + void "unsupported images neither persist nor consume an image number"() { + // given: fresh repositories and storage + def migration = mockMigration() + // when / then: an unsupported image is rejected + assert DocxImages.registerImageData(migration, picture(PictureType.EMF, 1L), 'sample', null) == null + // then: nothing is persisted + verifyNoInteractions(migration.storage, migration.imageRepository) + // when / then: the next supported image still receives the first identifier + assert DocxImages.registerImageData(migration, picture(PictureType.PNG, 2L), 'sample', null) == 'sample_img_1' + } + + private static XWPFPictureData picture(PictureType type, Long checksum) { + def data = mock(XWPFPictureData) + when(data.pictureTypeEnum).thenReturn(type) + when(data.checksum).thenReturn(checksum) + when(data.data).thenReturn([1, 2, 3] as byte[]) + return data + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxMergeFieldsTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxMergeFieldsTest.groovy new file mode 100644 index 00000000..ec1eb9f3 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxMergeFieldsTest.groovy @@ -0,0 +1,239 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.shared.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource + +class DocxMergeFieldsTest { + + @Test + void "parseIfField extracts merge field comparison and styled branches"() { + // given + def mergeField = field(text(" MERGEFIELD PaymentFrequency ", "variable")) + def conditional = field( + text(" IF ", "condition"), + mergeField, + text(' = 4 "', "condition"), + text("in equal ", "normal"), + text("quarterly payments", "emphasis"), + text('" "" ', "condition") + ) + + // when + def result = DocxMergeFields.parseIfField(conditional) + + // then + assert result.variableId == "PaymentFrequency" + assert result.operator == "=" + assert result.rightOperand == "4" + assert result.trueContent*.text == ["in equal ", "quarterly payments"] + assert result.trueContent*.styleId == ["normal", "emphasis"] + assert result.falseContent.isEmpty() + } + + @Test + void "parseIfField extracts quoted operand and false branch"() { + // given + def conditional = field( + text(" if "), + field(text(" MERGEFIELD GmpPayable ")), + text(' = "Y" "GMP applies" "No GMP" ') + ) + + // when + def result = DocxMergeFields.parseIfField(conditional) + + // then + assert result.variableId == "GmpPayable" + assert result.rightOperand == '"Y"' + assert result.trueContent*.text == ["GMP applies"] + assert result.falseContent*.text == ["No GMP"] + } + + @Test + void "parseIfField accepts omitted false branch and trailing field switch"() { + // given + def conditional = field( + text(" IF "), + field(text(" MERGEFIELD GmpPayable ")), + text(' = "Y" "GMP applies" \\* MERGEFORMAT ') + ) + + // when + def result = DocxMergeFields.parseIfField(conditional) + + // then + assert result.trueContent*.text == ["GMP applies"] + assert result.falseContent.isEmpty() + } + + @Test + void "parseIfField treats an unterminated quote as running to the end like Word"() { + // given + def conditional = field( + text(" IF "), + field(text(" MERGEFIELD PaymentFrequency ")), + text(' = 4 "starts here') + ) + + // when + def result = DocxMergeFields.parseIfField(conditional) + + // then + assert result.trueContent*.text == ["starts here"] + assert result.falseContent.isEmpty() + } + + @Test + void "parseIfField keeps nested fields as branch content and accepts a bare nested field branch"() { + // given + def nestedIf = field(text(" IF "), field(text(" MERGEFIELD Ben01Increase ")), text(' = "FIX0" "" "no fixed increase" ')) + def conditional = field( + text(" if "), + field(text(' MERGEFIELD PensionCommencementDate \\@"YYYYMMDD"')), + text(' > "20250401" '), + nestedIf, + text(" ") + ) + + // when + def result = DocxMergeFields.parseIfField(conditional) + + // then + assert result.variableId == "PensionCommencementDate" + assert result.operator == ">" + assert result.rightOperand == '"20250401"' + assert result.trueContent == [nestedIf] + assert result.falseContent.isEmpty() + } + + @Test + void "parseIfField accepts a nested field directly after the IF keyword"() { + // given + def conditional = field( + text(" "), + text("if"), + field(text(" MERGEFIELD TempPensionPayable ")), + text(' = "Y" "yes" "no" ') + ) + + // when + def result = DocxMergeFields.parseIfField(conditional) + + // then + assert result.variableId == "TempPensionPayable" + assert result.trueContent*.text == ["yes"] + assert result.falseContent*.text == ["no"] + } + + @ParameterizedTest + @CsvSource(value = [ + "=|Equals", + "<>|NotEquals", + ">|GreaterThan", + "<|LessThan", + ">=|GreaterOrEqualThan", + "<=|LessOrEqualThen", + ], delimiterString = '|') + void "parseConditions directly maps every supported Word operator"(String wordOperator, String expectedOperator) { + // when + def definition = DocxMergeFields.parseConditions([new IfCondition( + variableId: "PaymentFrequency", + operator: wordOperator, + rightOperand: "4" + )]) + + // then + Binary comparison = definition.group.items[0] as Binary + def left = comparison.left as Literal + assert left.value == "PaymentFrequency" + assert left.dataType == LiteralDataType.Variable + assert comparison.operator == BinOp.valueOf(expectedOperator) + def right = comparison.right as Literal + assert right.value == "4" + assert right.dataType == LiteralDataType.Number + assert !definition.group.negation + } + + @Test + void "parseConditions preserves quoted strings and inverts the operator of an else condition"() { + // when + def definition = DocxMergeFields.parseConditions([new IfCondition( + variableId: "GmpPayable", + operator: "=", + rightOperand: '"Y"', + negated: true + )]) + + // then + Binary comparison = definition.group.items[0] as Binary + assert comparison.operator == BinOp.NotEquals + def right = comparison.right as Literal + assert right.value == "Y" + assert right.dataType == LiteralDataType.String + assert !definition.group.negation + } + + @Test + void "parseConditions ANDs the conditions of a nested IF path into one flat group"() { + // when + def definition = DocxMergeFields.parseConditions([ + new IfCondition(variableId: "PensionCommencementDate", operator: ">", rightOperand: '"20250401"'), + new IfCondition(variableId: "Ben01Increase", operator: "=", rightOperand: '"FIX0"', negated: true), + ]) + + // then + assert definition.group.operator == GroupOp.And + assert definition.group.items.size() == 2 + assert (definition.group.items[1] as Binary).operator == BinOp.NotEquals + } + + private static WordField field(Object... parts) { + return new WordField(instructionParts: parts as List) + } + + private static FieldTextPart text(String value, String styleId = "style") { + return new FieldTextPart(text: value, styleId: styleId) + } + + @ParameterizedTest + @ValueSource(strings = [" MERGEFIELD PolicyHolderTitle ", "MERGEFIELD Name", "mergefield Name", " MERGEFIELD \"Full Name\" \\* MERGEFORMAT "]) + void "isMergeField recognizes MERGEFIELD instructions regardless of case or spacing"(String instruction) { + assert DocxMergeFields.isMergeField(instruction) + } + + @ParameterizedTest + @ValueSource(strings = [" IF ", " PAGEREF _Toc123 \\h ", " TOC \\o \"1-3\" \\h \\z \\u ", "", " ", "MERGE"]) + void "isMergeField rejects non-MERGEFIELD or blank instructions"(String instruction) { + assert !DocxMergeFields.isMergeField(instruction) + } + + @Test + void "isMergeField rejects null instruction"() { + assert !DocxMergeFields.isMergeField(null) + } + + @ParameterizedTest + @CsvSource(value = [ + " MERGEFIELD PolicyHolderTitle |PolicyHolderTitle", + "MERGEFIELD Name|Name", + "MERGEFIELD Name \\* MERGEFORMAT|Name", + "MERGEFIELD \"Full Name\" \\* MERGEFORMAT|Full Name", + "MERGEFIELD \"Full Name\"|Full Name", + "MERGEFIELD TotalBenefitAmount\\#,0.00|TotalBenefitAmount", + "MERGEFIELD TotalBenefitAmount \\# ,0.00|TotalBenefitAmount", + ], delimiterString = '|') + void "extractMergeFieldName extracts the field name, unquoting and stripping switches"(String instruction, String expectedName) { + assert DocxMergeFields.extractMergeFieldName(instruction) == expectedName + } + + @Test + void "extractMergeFieldName returns null for blank or missing name"() { + assert DocxMergeFields.extractMergeFieldName(null) == null + assert DocxMergeFields.extractMergeFieldName("") == null + assert DocxMergeFields.extractMergeFieldName("MERGEFIELD") == null + assert DocxMergeFields.extractMergeFieldName("MERGEFIELD ") == null + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageLayoutTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageLayoutTest.groovy new file mode 100644 index 00000000..55302071 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageLayoutTest.groovy @@ -0,0 +1,97 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.shared.Size +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr + +class DocxPageLayoutTest { + @Test + void "page dimensions and asymmetric margins are converted from twips"() { + // given: explicit page dimensions and different margins on each side + def section = section(12240, 15840, 720, 1440, 1080, 360) + // when / then: page size is converted to points + assert DocxPageLayout.resolvePageSize(section)*.toPoints() == [612d, 792d] + // when: content bounds are resolved + def position = DocxPageLayout.resolvePageContentPosition(section) + // then: margins determine both the content origin and remaining space + assert [position.x, position.y, position.width, position.height]*.toPoints() == [18d, 36d, 522d, 702d] + } + + @Test + void "missing section geometry uses defaults"() { + // given: absent or empty section properties + [null, CTSectPr.Factory.newInstance()].each { + // when / then: both page size and content bounds use defaults + assert DocxPageLayout.resolvePageSize(it) == DocxPageLayout.DEFAULT_PAGE_SIZE + assert DocxPageLayout.resolvePageContentPosition(it) == DocxPageLayout.DEFAULT_CONTENT_POSITION + } + } + + @ParameterizedTest + @ValueSource(strings = ['w', 'h']) + void "supplied page dimension survives when the other dimension and margins are missing"(String dimension) { + // given: just one page dimension, with no margin element + def incomplete = CTSectPr.Factory.newInstance() + incomplete.addNewPgSz()."${dimension}" = BigInteger.valueOf(14400) + + // when + def size = DocxPageLayout.resolvePageSize(incomplete) + def position = DocxPageLayout.resolvePageContentPosition(incomplete) + + // then: the supplied dimension is 720 points; only the missing values use defaults + double width = dimension == 'w' ? 720d : Size.ofMillimeters(210).toPoints() + double height = dimension == 'h' ? 720d : Size.ofMillimeters(297).toPoints() + assert size*.toPoints() == [width, height] + assert Math.abs(position.width.toPoints() - (width - Size.ofMillimeters(40).toPoints())) < 1e-9 + assert Math.abs(position.height.toPoints() - (height - Size.ofMillimeters(57).toPoints())) < 1e-9 + assert position.x == Size.ofMillimeters(20) + assert position.y == Size.ofMillimeters(20) + } + + @ParameterizedTest + @CsvSource(['left,0', 'right,360', 'top,0', 'bottom,360']) + void "each supplied margin survives when page size and other margins are missing"(String side, long twips) { + // given: one explicit margin, including valid zero margins + def incomplete = CTSectPr.Factory.newInstance() + incomplete.addNewPgMar()."${side}" = BigInteger.valueOf(twips) + + // when + def position = DocxPageLayout.resolvePageContentPosition(incomplete) + + // then: only the specified margin changes the default content bounds + def margins = [left: 20d, right: 20d, top: 20d, bottom: 37d].collectEntries { + key, mm -> [key, Size.ofMillimeters(mm).toPoints()] + } + margins[side] = twips / 20d + def expected = [margins.left, margins.top, + Size.ofMillimeters(210).toPoints() - margins.left - margins.right, + Size.ofMillimeters(297).toPoints() - margins.top - margins.bottom] + [position.x, position.y, position.width, position.height].eachWithIndex { value, i -> + assert Math.abs(value.toPoints() - expected[i]) < 1e-9 + } + } + + @ParameterizedTest + @CsvSource(['100,100,0,50,0,50', '100,100,60,0,50,0', '100,100,0,110,0,0']) + void "nonpositive content dimensions use default position"(long w, long h, long top, long right, long bottom, long left) { + // given: parameterized margins that leave no positive content width or height + // when / then: resolving the content bounds returns the default position + assert DocxPageLayout.resolvePageContentPosition(section(w, h, top, right, bottom, left)) == DocxPageLayout.DEFAULT_CONTENT_POSITION + } + + private static CTSectPr section(long w, long h, long top, long right, long bottom, long left) { + def section = CTSectPr.Factory.newInstance() + def size = section.addNewPgSz() + size.w = BigInteger.valueOf(w) + size.h = BigInteger.valueOf(h) + def margins = section.addNewPgMar() + margins.top = BigInteger.valueOf(top) + margins.right = BigInteger.valueOf(right) + margins.bottom = BigInteger.valueOf(bottom) + margins.left = BigInteger.valueOf(left) + return section + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageSectionsTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageSectionsTest.groovy new file mode 100644 index 00000000..4b5ecfac --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxPageSectionsTest.groovy @@ -0,0 +1,105 @@ +package com.quadient.migration.example.docx.parser + +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STSectionMark + +class DocxPageSectionsTest { + @Test + void "section break belongs to preceding section and trailing content uses body properties"() { + // given: paragraphs and a table separated from trailing content by a section break + new XWPFDocument().withCloseable { doc -> + def first = doc.createParagraph() + def table = doc.createTable() + def boundary = doc.createParagraph() + def firstProperties = boundary.CTP.addNewPPr().addNewSectPr() + def last = doc.createParagraph() + def lastProperties = doc.document.body.addNewSectPr() + // when + def sections = DocxPageSections.splitIntoSections(doc) + // then: the boundary paragraph stays in the first section + assert sections*.elements == [[first, table, boundary], [last]] + assert sections[0].sectPr.is(firstProperties) + assert sections[1].sectPr.is(lastProperties) + // when: the sections are grouped into pages + def pages = DocxPageSections.groupIntoPages(sections) + // then: page identifiers and ordered body elements are preserved + assert pages*.id('sample') == ['sample_page1', 'sample_page2'] + assert pages[0].bodyElements() == [first, table, boundary] + assert pages[0].paragraphs() == [first, boundary] + } + } + + @ParameterizedTest + @CsvSource(['continuous,1', 'nextColumn,1', 'nextPage,2', 'evenPage,2', 'oddPage,2']) + void "section type controls page grouping"(String mark, int count) { + // given: two sections, with the second using the parameterized break type + def properties = CTSectPr.Factory.newInstance() + properties.addNewType().val = STSectionMark.Enum.forString(mark) + def first = new DocxSection() + def second = new DocxSection(sectPr: properties) + // when + def pages = DocxPageSections.groupIntoPages([first, second]) + // then: the break type determines page count without losing sections + assert pages.size() == count + assert pages.collectMany { it.sections } == [first, second] + } + + @Test + void "empty document and final section break do not create phantom sections"() { + // given: an empty document + new XWPFDocument().withCloseable { doc -> + // when / then: splitting and grouping empty content produces no sections or pages + assert DocxPageSections.splitIntoSections(doc).empty + assert DocxPageSections.groupIntoPages([]).empty + // when: the only paragraph ends with a section break + doc.createParagraph().CTP.addNewPPr().addNewSectPr() + // then: no extra trailing section is created + assert DocxPageSections.splitIntoSections(doc).size() == 1 + } + } + + @Test + void "a rendered page break at the start of a paragraph creates a new page"() { + // given: Word has cached a page boundary before the second paragraph's visible text + new XWPFDocument().withCloseable { doc -> + def first = doc.createParagraph() + first.createRun().setText('First-page content') + def second = doc.createParagraph() + second.createRun().CTR.addNewLastRenderedPageBreak() + second.createRun().setText('Second-page content') + def third = doc.createParagraph() + third.createRun().setText('More second-page content') + doc.document.body.addNewSectPr() + ByteArrayOutputStream serialized = new ByteArrayOutputStream() + doc.write(serialized) + + new XWPFDocument(new ByteArrayInputStream(serialized.toByteArray())).withCloseable { reloaded -> + // when + def pages = DocxPageSections.groupIntoPages(DocxPageSections.splitIntoSections(reloaded)) + + // then: the paragraph carrying the marker and subsequent content belong to the next page + assert pages*.bodyElements()*.collect { it.text } == [['First-page content'], ['Second-page content', 'More second-page content']] + } + } + } + + @Test + void "a rendered page break after paragraph content is not used as a boundary"() { + // given: a marker after content cannot be represented without splitting a paragraph + new XWPFDocument().withCloseable { doc -> + def paragraph = doc.createParagraph() + paragraph.createRun().setText('Content before the cached marker') + paragraph.createRun().CTR.addNewLastRenderedPageBreak() + paragraph.createRun().setText('Content after the cached marker') + doc.document.body.addNewSectPr() + + // when / then: preserve the paragraph as one flow item instead of guessing at an internal split + assert DocxPageSections.groupIntoPages(DocxPageSections.splitIntoSections(doc))*.bodyElements() == [[paragraph]] + } + } + +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy new file mode 100644 index 00000000..96dd9c96 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy @@ -0,0 +1,179 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.api.dto.migrationmodel.DisplayRule +import com.quadient.migration.api.dto.migrationmodel.VariableRef +import com.quadient.migration.example.docx.util.DocxUtils +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType + +import static org.mockito.ArgumentMatchers.any +import static org.mockito.Mockito.times +import static org.mockito.Mockito.verify +import static com.quadient.migration.example.docx.DocxFieldFixtures.* +import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration + +class DocxParagraphParserTest { + + @Test + void "adjacent equal formatting coalesces while style changes preserve text order"() { + // given: adjacent plain runs, a bold run and a final plain run + new XWPFDocument().withCloseable { doc -> + doc.createStyles() + def paragraph = doc.createParagraph() + paragraph.createRun().setText('Hello ') + paragraph.createRun().setText('world') + def bold = paragraph.createRun() + bold.setBold(true) + bold.setText('!') + paragraph.createRun().setText(' Again') + // when + def parsed = DocxParagraphParser.parseParagraph(mockMigration(), paragraph, 'sample') + // then: equal formatting merges while formatting boundaries and text order survive + assert parsed.content.size() == 3 + assert parsed.content.collect { it.content*.value.join() } == ['Hello world', '!', ' Again'] + assert DocxUtils.extractParagraphText(parsed) == 'Hello world! Again' + } + } + + @Test + void "tabs line breaks and blank paragraphs survive parsing"() { + // given: a run containing text separated by a tab and a line break + new XWPFDocument().withCloseable { doc -> + doc.createStyles() + def paragraph = doc.createParagraph() + def run = paragraph.createRun() + run.setText('A') + run.addTab() + run.setText('B') + run.addBreak() + run.setText('C') + def migration = mockMigration() + // when / then: parsing preserves the tab and line break + assert DocxUtils.extractParagraphText(DocxParagraphParser.parseParagraph(migration, paragraph, 'sample')) == 'A\tB\nC' + // when: an empty paragraph is parsed + def blank = DocxParagraphParser.parseParagraph(migration, doc.createParagraph(), 'sample') + // then: a blank paragraph is retained rather than discarded + assert blank != null + assert DocxUtils.extractParagraphText(blank) == '' + } + } + + @Test + void "configured text markers are parsed as variables while surrounding text is retained"() { + // Each pattern has a capture group for the variable name. These markers are authoring conventions, not DOCX fields. + def migration = mockMigration(docxVariablePatterns: [/\$\{\s*([^}]+?)\s*}/, /<<\s*([^>]+?)\s*>>/]) + new XWPFDocument().withCloseable { doc -> + doc.createStyles() + def paragraph = doc.createParagraph() + paragraph.createRun().setText('Dear ${customer.name}, reference << PolicyNumber >>.') + + def parsed = DocxParagraphParser.parseParagraph(migration, paragraph, 'sample') + + assert parsed.content.collect { it.content[0] instanceof VariableRef ? it.content[0].id : it.content[0].value } == + ['Dear ', 'customer.name', ', reference ', 'PolicyNumber', '.'] + verify(migration.variableRepository, times(2)).upsert(any()) + } + } + + @Test + void "parseParagraph flushes a final IF with split instructions and cached field results"() { + // given + def migration = mockMigration() + def variables = migration.variableRepository + def displayRules = migration.displayRuleRepository + + def document = new XWPFDocument() + document.createStyles() + def paragraph = document.createParagraph() + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " if ") + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " ") + instruction(paragraph.createRun(), "MERGEFIELD BEN0") + instruction(paragraph.createRun(), "2") + instruction(paragraph.createRun(), "EscWording ") + fieldCharacter(paragraph.createRun(), STFldCharType.SEPARATE) + instruction(paragraph.createRun(), "No increase will apply") + fieldCharacter(paragraph.createRun(), STFldCharType.END) + instruction(paragraph.createRun(), ' = "') + instruction(paragraph.createRun(), 'No increase will apply') + instruction(paragraph.createRun(), '" "Pension will not increase" "') + instruction(paragraph.createRun(), '1 April each year') + instruction(paragraph.createRun(), '"') + fieldCharacter(paragraph.createRun(), STFldCharType.SEPARATE) + paragraph.createRun().setText("1 April each year") + fieldCharacter(paragraph.createRun(), STFldCharType.END) + + // when + def parsedParagraph = DocxParagraphParser.parseParagraph(migration, paragraph, "sample") + + // then + assert parsedParagraph.content.collect { it.content[0].value } == + ["Pension will not increase", "1 April each year"] + assert parsedParagraph.content*.displayRuleRef*.id.every { it.startsWith("docx_if_") } + def ruleCaptor = ArgumentCaptor.forClass(DisplayRule) + verify(displayRules, times(2)).upsert(ruleCaptor.capture()) + assert ruleCaptor.allValues*.name as Set == [ + 'BEN02EscWording = "No increase will apply"', + 'BEN02EscWording <> "No increase will apply"' + ] as Set + verify(variables).upsert(any()) + + document.close() + } + + @Test + void "nested IF in an else branch spanning two cell paragraphs yields text with an AND rule"() { + // given + def migration = mockMigration() + def displayRules = migration.displayRuleRepository + + def document = new XWPFDocument() + document.createStyles() + def cell = document.createTable(1, 1).getRow(0).getCell(0) + def first = cell.paragraphs[0] + // Mirrors Word output: IF «PCD» > "20250401" «IF «Increase» = "FIX0" "" "«IF «Element» = "PRE88GMP" "" "¶text"»"» + fieldCharacter(first.createRun(), STFldCharType.BEGIN) + instruction(first.createRun(), " if ") + appendMergeField(first, ' MERGEFIELD PensionCommencementDate \\@"YYYYMMDD"') + instruction(first.createRun(), ' > "20250401" ') + fieldCharacter(first.createRun(), STFldCharType.BEGIN) + instruction(first.createRun(), " IF ") + appendMergeField(first, " MERGEFIELD Ben01Increase ", "C(0:3)7F") + instruction(first.createRun(), ' = "FIX0" "" "') + fieldCharacter(first.createRun(), STFldCharType.BEGIN) + instruction(first.createRun(), " if ") + appendMergeField(first, " MERGEFIELD Ben01Element ", "POS88GMP") + instruction(first.createRun(), ' = "PRE88GMP" "" "') + def second = cell.addParagraph() + instruction(second.createRun(), "Your first increase will reflect the number of months") + // The closing quote (with the space before it) sits in a differently formatted run, as in the sample. + def closingQuote = second.createRun() + closingQuote.bold = true + instruction(closingQuote, ' " ') + fieldCharacter(second.createRun(), STFldCharType.END) + instruction(second.createRun(), '" ') + fieldCharacter(second.createRun(), STFldCharType.END) + instruction(second.createRun(), ' ') + fieldCharacter(second.createRun(), STFldCharType.END) + + // when + def state = new FieldParseState() + def parsedFirst = DocxParagraphParser.parseFlowParagraph(migration, first, "sample", state) + def parsedSecond = DocxParagraphParser.parseFlowParagraph(migration, second, "sample", state) + + // then + assert parsedFirst == null + assert state.depth == 0 + assert parsedSecond.content.collect { it.content[0].value } == ["Your first increase will reflect the number of months "] + def ruleCaptor = ArgumentCaptor.forClass(DisplayRule) + verify(displayRules).upsert(ruleCaptor.capture()) + assert ruleCaptor.value.name == 'PensionCommencementDate > "20250401" AND Ben01Increase <> "FIX0" AND Ben01Element <> "PRE88GMP"' + assert parsedSecond.content[0].displayRuleRef.id == ruleCaptor.value.id + assert ruleCaptor.value.definition.group.items.size() == 3 + + document.close() + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy new file mode 100644 index 00000000..7a95c6ea --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy @@ -0,0 +1,172 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef +import com.quadient.migration.api.dto.migrationmodel.Paragraph +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.apache.poi.xwpf.usermodel.XWPFParagraph +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.mockito.ArgumentCaptor +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STStyleType + +import static org.mockito.Mockito.times +import static org.mockito.Mockito.verify +import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration + +class DocxSectionsTest { + + @Test + void "heading level comes from the outline level of the style chain"() { + // given + def document = new XWPFDocument() + document.createStyles() + addStyle(document, "Heading1", "heading 1", null, 0) + addStyle(document, "SLRHDR1", "SLR HDR1", "Heading1", null) + addStyle(document, "1SLRHDR1", "1. SLR HDR1", "SLRHDR1", null) + addStyle(document, "TOCHeading", "TOC Heading", "Heading1", 9) + addStyle(document, "SLRBody", "SLR Body", "Heading1", null) + addStyle(document, "SLRURL", "SLR URL", "SLRBody", null) + + // when / then + assert DocxHeadings.headingLevel(paragraph(document, "Heading1", "About us")) == 1 + assert DocxHeadings.headingLevel(paragraph(document, "SLRHDR1", "About us")) == 1 + assert DocxHeadings.headingLevel(paragraph(document, "1SLRHDR1", "About us")) == 1 + assert DocxHeadings.headingLevel(paragraph(document, "TOCHeading", "Contents")) == null + assert DocxHeadings.headingLevel(paragraph(document, "SLRBody", "Body based on a heading")) == null + assert DocxHeadings.headingLevel(paragraph(document, "SLRURL", "www.example.com")) == null + assert DocxHeadings.headingLevel(paragraph(document, "SLRHDR1", " ")) == null + assert DocxHeadings.headingLevel(paragraph(document, null, "Body text")) == null + + XWPFParagraph copiedFormatting = paragraph(document, null, "Body text with a direct outline level") + copiedFormatting.CTP.addNewPPr().addNewOutlineLvl().val = BigInteger.ZERO + assert DocxHeadings.headingLevel(copiedFormatting) == null + + document.close() + } + + @ParameterizedTest + @CsvSource([ + "SLRHDR2, SLR HDR2, 2", + "Heading3, heading 3, 3", + "Title, Title, 1", + "Subtitle, Subtitle, 2", + "BodyText, Body Text, ", + "TableParagraph, Table Paragraph, ", + ]) + void "heading level falls back to the style name"(String styleId, String styleName, Integer expected) { + // given + def document = new XWPFDocument() + document.createStyles() + addStyle(document, styleId, styleName, null, null) + + // when / then + assert DocxHeadings.headingLevel(paragraph(document, styleId, "Some heading")) == expected + + document.close() + } + + @Test + void "page content is divided into one block per top-level heading"() { + // given + def migration = mockMigration() + def body = new DocxBodyContent(migration, "sample", "sample_page1") + body.add(paragraph("Intro line")) + body.add(paragraph("Your policy"), 1) + body.add(paragraph("Policy details"), 2) + body.add(paragraph("Some text")) + body.add(paragraph("More information and help"), 1) + body.add(paragraph("")) + + // when + List content = body.sectionBlocks() + + // then + assert content.size() == 3 + assert content.every { it instanceof DocumentObjectRef } + assert content*.id == ["sample_page1_section1", "sample_page1_section2", "sample_page1_section3"] + def captor = ArgumentCaptor.forClass(DocumentObject) + verify(migration.documentObjectRepository, times(3)).upsert(captor.capture()) + List blocks = captor.allValues + assert blocks*.name == ["sample_page1_section1", "Your policy", "More information and help"] + assert blocks.every { it.internal } + assert blocks[0].content.size() == 1 + assert blocks[1].content.size() == 3 + assert blocks[2].content.size() == 2 + } + + @Test + void "lower level headings alone still divide the page when no higher level is present"() { + // given + def migration = mockMigration() + def body = new DocxBodyContent(migration, "sample", "sample_page1") + body.add(paragraph("What is in this pack?"), 2) + body.add(paragraph("Text")) + body.add(paragraph("What do you need to do?"), 2) + + // when / then + assert body.sectionBlocks()*.id == ["sample_page1_section1", "sample_page1_section2"] + } + + @Test + void "page without headings stays a plain flow"() { + // given + def migration = mockMigration() + def body = new DocxBodyContent(migration, "sample", "sample_page1") + body.add(paragraph("Just text")) + body.add(paragraph("And more")) + + // when + List content = body.sectionBlocks() + + // then + assert content.size() == 2 + assert content.every { it instanceof Paragraph } + verify(migration.documentObjectRepository, times(0)).upsert(org.mockito.ArgumentMatchers.any()) + } + + @Test + void "block names are shortened at a word boundary and fall back when empty"() { + assert DocxBlocks.blockName(" Your lifetime allowance ", "table", "x") == "Your lifetime allowance table" + assert DocxBlocks.blockName("Explanation of the technical words", null, "x") == "Explanation of the technical words" + assert DocxBlocks.blockName("", "table", "fallback") == "fallback" + assert DocxBlocks.blockName(null, null, "fallback") == "fallback" + String longText = "If you need more support or would like this information in Braille, large print or audio, please call" + String name = DocxBlocks.blockName(longText, null, "x") + assert name.length() <= 60 + assert name == "If you need more support or would like this information in" + } + + private static Paragraph paragraph(String text) { + def document = new XWPFDocument() + document.createStyles() + XWPFParagraph paragraph = document.createParagraph() + paragraph.createRun().setText(text) + return DocxParagraphParser.parseParagraph(mockMigration(), paragraph, "sample") + } + + private static XWPFParagraph paragraph(XWPFDocument document, String styleId, String text) { + XWPFParagraph paragraph = document.createParagraph() + if (styleId) { + paragraph.style = styleId + } + paragraph.createRun().setText(text) + return paragraph + } + + private static void addStyle(XWPFDocument document, String styleId, String name, String basedOn, Integer outlineLevel) { + CTStyle ctStyle = CTStyle.Factory.newInstance() + ctStyle.type = STStyleType.PARAGRAPH + ctStyle.styleId = styleId + ctStyle.addNewName().val = name + if (basedOn) { + ctStyle.addNewBasedOn().val = basedOn + } + if (outlineLevel != null) { + ctStyle.addNewPPr().addNewOutlineLvl().val = BigInteger.valueOf(outlineLevel) + } + document.styles.addStyle(new org.apache.poi.xwpf.usermodel.XWPFStyle(ctStyle, document.styles)) + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy new file mode 100644 index 00000000..beaae4cb --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy @@ -0,0 +1,190 @@ +package com.quadient.migration.example.docx.parser + +import com.quadient.migration.api.dto.migrationmodel.DisplayRule +import com.quadient.migration.api.dto.migrationmodel.FirstMatch +import com.quadient.migration.shared.BinOp +import com.quadient.migration.shared.Binary +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType + +import static org.mockito.ArgumentMatchers.any +import static org.mockito.Mockito.times +import static org.mockito.Mockito.verify +import static com.quadient.migration.example.docx.DocxFieldFixtures.* +import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration + +class ParagraphContentCollectorTest { + + @Test + void "collector creates display rule and conditional text from nested Word field"() { + // given + def migration = mockMigration() + def variables = migration.variableRepository + def displayRules = migration.displayRuleRepository + + def document = new XWPFDocument() + def paragraph = document.createParagraph() + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " IF ") + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " MERGEFIELD PaymentFrequency ") + fieldCharacter(paragraph.createRun(), STFldCharType.END) + instruction(paragraph.createRun(), ' = 4 "quarterly" "" ') + fieldCharacter(paragraph.createRun(), STFldCharType.END) + + // when + def collector = new ParagraphContentCollector(migration, "sample") + paragraph.runs.each { collector.addRun(it, "body") } + def textBuilders = collector.finish() + + // then + assert textBuilders.size() == 1 + def text = textBuilders[0].build() + assert text.content[0].value == "quarterly" + assert text.styleRef.id == "body" + assert text.displayRuleRef.id.startsWith("docx_if_") + + def ruleCaptor = ArgumentCaptor.forClass(DisplayRule) + verify(displayRules).upsert(ruleCaptor.capture()) + Binary comparison = ruleCaptor.value.definition.group.items[0] as Binary + assert comparison.left.value == "PaymentFrequency" + assert comparison.operator == BinOp.Equals + assert comparison.right.value == "4" + verify(variables).upsert(any()) + + document.close() + } + + @Test + void "collector preserves instruction and field-character order within one run"() { + // given + def migration = mockMigration() + def document = new XWPFDocument() + def paragraph = document.createParagraph() + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " IF ") + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + def mergeInstructionAndEnd = paragraph.createRun() + instruction(mergeInstructionAndEnd, " MERGEFIELD GmpPayable ") + fieldCharacter(mergeInstructionAndEnd, STFldCharType.END) + instruction(paragraph.createRun(), ' = "Y" "GMP applies" "" ') + fieldCharacter(paragraph.createRun(), STFldCharType.END) + + // when + def collector = new ParagraphContentCollector(migration, "sample") + paragraph.runs.each { collector.addRun(it, "body") } + def textBuilders = collector.finish() + + // then + assert textBuilders[0].build().content[0].value == "GMP applies" + verify(migration.displayRuleRepository).upsert(any()) + + document.close() + } + + @Test + void "collector continues to emit a simple MERGEFIELD variable reference"() { + // given + def migration = mockMigration() + def document = new XWPFDocument() + def paragraph = document.createParagraph() + fieldCharacter(paragraph.createRun(), STFldCharType.BEGIN) + instruction(paragraph.createRun(), " MERGEFIELD PolicyHolderTitle ") + fieldCharacter(paragraph.createRun(), STFldCharType.END) + + // when + def collector = new ParagraphContentCollector(migration, "sample") + paragraph.runs.each { collector.addRun(it, "body") } + def textBuilders = collector.finish() + + // then + def text = textBuilders[0].build() + assert text.content[0].id == "PolicyHolderTitle" + assert text.styleRef.id == "body" + assert text.displayRuleRef == null + + document.close() + } + + @Test + void "adjacent exclusive equality fields become one styled FirstMatch in source order"() { + // given + def migration = mockMigration() + def document = new XWPFDocument() + def paragraph = document.createParagraph() + appendEqualityIfField(paragraph, "PaymentFrequency", "4", "quarterly") + appendEqualityIfField(paragraph, "PaymentFrequency", "1", "annually") + appendEqualityIfField(paragraph, "PaymentFrequency", "12", "monthly") + + // when + def collector = new ParagraphContentCollector(migration, "sample") + paragraph.runs.each { collector.addRun(it, "body") } + def textBuilders = collector.finish() + + // then + assert textBuilders.size() == 1 + def text = textBuilders[0].build() + assert text.styleRef.id == "body" + FirstMatch firstMatch = text.content[0] as FirstMatch + assert firstMatch.cases*.name == [ + "PaymentFrequency = 4", + "PaymentFrequency = 1", + "PaymentFrequency = 12" + ] + assert firstMatch.cases.collect { it.content[0].value } == ["quarterly", "annually", "monthly"] + verify(migration.displayRuleRepository, times(3)).upsert(any()) + + document.close() + } + + @Test + void "adjacent conditions on different variables remain independent conditional text"() { + // given + def migration = mockMigration() + def document = new XWPFDocument() + def paragraph = document.createParagraph() + appendEqualityIfField(paragraph, "PaymentFrequency", "4", "quarterly") + appendEqualityIfField(paragraph, "GmpPayable", '"Y"', "GMP applies") + + // when + def collector = new ParagraphContentCollector(migration, "sample") + paragraph.runs.each { collector.addRun(it, "body") } + def textBuilders = collector.finish() + + // then + def texts = textBuilders*.build() + assert texts.collect { it.content[0].value } == ["quarterly", "GMP applies"] + assert texts*.displayRuleRef*.id.every { it.startsWith("docx_if_") } + assert texts.collect { it.content[0] }.every { !(it instanceof FirstMatch) } + + document.close() + } + + @Test + void "plain text separates conditional sequences and retains source order"() { + // given + def migration = mockMigration() + def document = new XWPFDocument() + def paragraph = document.createParagraph() + appendEqualityIfField(paragraph, "PaymentFrequency", "4", "quarterly") + paragraph.createRun().setText(" or ") + appendEqualityIfField(paragraph, "PaymentFrequency", "12", "monthly") + + // when + def collector = new ParagraphContentCollector(migration, "sample") + paragraph.runs.each { collector.addRun(it, "body") } + def textBuilders = collector.finish() + + // then + def texts = textBuilders*.build() + assert texts.collect { it.content[0].value } == ["quarterly", " or ", "monthly"] + assert texts[0].displayRuleRef != null + assert texts[1].displayRuleRef == null + assert texts[2].displayRuleRef != null + assert texts.collect { it.content[0] }.every { !(it instanceof FirstMatch) } + + document.close() + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/DocxParagraphStylesTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/DocxParagraphStylesTest.groovy new file mode 100644 index 00000000..27e8b4e8 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/DocxParagraphStylesTest.groovy @@ -0,0 +1,31 @@ +package com.quadient.migration.example.docx.style + +import com.quadient.migration.api.dto.migrationmodel.builder.ParagraphStyleDefinitionBuilder +import com.quadient.migration.shared.LineSpacing +import org.junit.jupiter.api.Test + +class DocxParagraphStylesTest { + @Test + void "automatic spacing is a line multiple and fixed spacing is in points"() { + // given: an empty style definition + def automatic = new ParagraphStyleDefinitionBuilder() + // when: automatic spacing is supplied in 240ths of a line + DocxParagraphStyles.applyLineSpacing(automatic, 'auto', 360) + // then + assert automatic.build().lineSpacing == new LineSpacing.MultipleOf(1.5d) + // given: another empty style definition + def exact = new ParagraphStyleDefinitionBuilder() + // when: exact spacing is supplied in twips + DocxParagraphStyles.applyLineSpacing(exact, 'exact', 360) + // then + assert exact.build().lineSpacing instanceof LineSpacing.Exact + assert exact.build().lineSpacing.size.toPoints() == 18d + // given: another empty style definition + def minimum = new ParagraphStyleDefinitionBuilder() + // when: minimum spacing is supplied in twips + DocxParagraphStyles.applyLineSpacing(minimum, 'atLeast', 240) + // then + assert minimum.build().lineSpacing instanceof LineSpacing.AtLeast + assert minimum.build().lineSpacing.size.toPoints() == 12d + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/StyleChainResolverTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/StyleChainResolverTest.groovy new file mode 100644 index 00000000..5a801a06 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/style/StyleChainResolverTest.groovy @@ -0,0 +1,114 @@ +package com.quadient.migration.example.docx.style + + +import org.apache.poi.xwpf.usermodel.XWPFDocument +import org.apache.poi.xwpf.usermodel.XWPFStyle +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule + +class StyleChainResolverTest { + @Test + void "run properties inherit individually and direct false overrides inherited true"() { + // given: inherited bold, italic and font properties, with bold explicitly disabled on the run + new XWPFDocument().withCloseable { doc -> + doc.createStyles() + def base = style(doc, 'Base', null) + base.addNewRPr().addNewB() + base.RPr.addNewRFonts().ascii = 'Arial' + base.RPr.addNewSz().val = BigInteger.valueOf(23) + def child = style(doc, 'Child', 'Base') + child.addNewRPr().addNewI() + def paragraph = doc.createParagraph() + def run = paragraph.createRun() + run.setBold(false) + // when: properties are resolved through the fallback style and its parent + def chain = StyleChainResolver.resolveRunPropertyChain(run, 'Child') + // then: the direct override wins and other properties are inherited individually + assert StyleChainResolver.resolveFirst(chain, StyleChainResolver.&resolveBold) == false + assert StyleChainResolver.resolveFirst(chain, StyleChainResolver.&resolveItalic) == true + assert StyleChainResolver.resolveFirst(chain, StyleChainResolver.&resolveFontName) == 'Arial' + assert StyleChainResolver.resolveFirst(chain, StyleChainResolver.&resolveFontSize) == 11.5d + // when: the run explicitly selects the base style + run.setStyle('Base') + // then: the fallback child style no longer contributes italic formatting + assert StyleChainResolver.resolveFirst(StyleChainResolver.resolveRunPropertyChain(run, 'Child'), StyleChainResolver.&resolveItalic) == null + } + } + + @Test + void "paragraph chain terminates cycles and preserves zero overrides"() { + // given: cyclic parent styles and a direct zero-spacing override + new XWPFDocument().withCloseable { doc -> + doc.createStyles() + style(doc, 'A', 'B').addNewPPr().addNewSpacing().before = BigInteger.valueOf(240) + style(doc, 'B', 'A').addNewPPr().addNewInd().left = BigInteger.valueOf(720) + def paragraph = doc.createParagraph() + paragraph.style = 'A' + paragraph.CTP.PPr.addNewSpacing().before = BigInteger.ZERO + // when + def chain = StyleChainResolver.resolveParagraphPropertyChain(paragraph) + // then: traversal terminates, preserving zero and inheriting the remaining properties + assert chain.size() == 4 // Direct properties, A, B, document defaults; no second visit to A. + assert StyleChainResolver.resolveFirst(chain, StyleChainResolver.&resolveSpacingBefore) == 0 + assert StyleChainResolver.resolveFirst(chain, StyleChainResolver.&resolveIndentationLeft) == 720 + // when: the paragraph references a missing style + paragraph.style = 'Missing' + // then: only direct properties and document defaults remain + assert StyleChainResolver.resolveParagraphPropertyChain(paragraph).size() == 2 + } + } + + @ParameterizedTest + @CsvSource(['true,true', 'false,false', '1,true', '0,false', 'on,true', 'off,false']) + void "on off flags distinguish explicit false from absent properties"(String value, boolean expected) { + // given: no bold or italic properties + def properties = CTRPr.Factory.newInstance() + // when / then: an absent bold property has no resolved value + assert StyleChainResolver.resolveBold(properties) == null + // when: explicit bold and italic flags are added + properties.addNewB().val = value + properties.addNewI().val = value + // then: each XML representation resolves to the expected boolean + assert StyleChainResolver.resolveBold(properties) == expected + assert StyleChainResolver.resolveItalic(properties) == expected + } + + @Test + void "color conversion handles unsigned bytes and ignores automatic colors"() { + // given: unsigned RGB bytes, a valid hex color and absent or unsupported color values + // when / then: valid colors resolve, while unsupported values remain absent + assert StyleChainResolver.colorValueToHex([0, 128, 255] as byte[]) == '0080FF' + assert StyleChainResolver.toColor('abcdef') != null + [null, 'auto', '', 'FFF', 'invalid'].each { assert StyleChainResolver.toColor(it) == null } + } + + @Test + void "line spacing defaults to auto only when a line value exists"() { + // given: paragraph properties without spacing + def properties = CTPPr.Factory.newInstance() + // when / then: missing spacing remains absent + assert StyleChainResolver.resolveLineSpacing(properties) == null + // when: a line value is added without a spacing rule + def spacing = properties.addNewSpacing() + spacing.line = BigInteger.valueOf(360) + // then: the rule defaults to automatic + assert StyleChainResolver.resolveLineSpacing(properties) == [line: 360L, lineRule: 'auto'] + // when: an explicit rule is supplied + spacing.lineRule = STLineSpacingRule.EXACT + // then: the explicit rule is retained + assert StyleChainResolver.resolveLineSpacing(properties) == [line: 360L, lineRule: 'exact'] + } + + private static CTStyle style(XWPFDocument doc, String id, String parent) { + def properties = CTStyle.Factory.newInstance() + properties.styleId = id + if (parent) properties.addNewBasedOn().val = parent + doc.styles.addStyle(new XWPFStyle(properties, doc.styles)) + return properties + } +} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/util/DocxUtilsTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/util/DocxUtilsTest.groovy new file mode 100644 index 00000000..374eab61 --- /dev/null +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/util/DocxUtilsTest.groovy @@ -0,0 +1,37 @@ +package com.quadient.migration.example.docx.util + + +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +class DocxUtilsTest { + @ParameterizedTest + @CsvSource(['1440,72', '1,0.05', '0,0', '-240,-12']) + void "twips conversion preserves fractional and negative coordinates"(String value, double expected) { + // given: a twips value and expected points from the parameterized case + // when / then: string and numeric inputs produce the same conversion + assert DocxUtils.twipsToPoints(value) == expected + assert DocxUtils.twipsToPoints(new BigInteger(value)) == expected + } + + @Test + void "missing and invalid dimensions remain absent"() { + // given: absent or nonnumeric dimensions + // when / then: conversion leaves them absent + [null, '', 'auto', '12pt'].each { assert DocxUtils.twipsToPoints(it) == null } + // given: the unset-size sentinel and valid twips / EMU dimensions + // when / then: size conversion handles the sentinel and preserves fractional points + assert DocxUtils.twipsToSize(-1).toPoints() == 0d + assert DocxUtils.twipsToSize(1440).toPoints() == 72d + assert DocxUtils.emuToSize(6350).toPoints() == 0.5d + } + + @Test + void "hash remains compatible with existing style identifiers"() { + // given: a known SHA-256 input and ordered attributes with the same concatenated value + // when / then: hashing preserves the digest and uppercase style-identifier convention + assert DocxUtils.sha256Hex('abc') == 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + assert DocxUtils.calculateSHA([first: 'a', second: 'bc']) == 'BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD' + } +} From 82d511e7b93f99a82e6de0a7075f454dfbfdc91e Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 15 Sep 2026 09:26:41 +0200 Subject: [PATCH 2/2] deduplicate the migration mock util --- .../quadient/migration/example/Utils.groovy | 7 +++- .../example/docx/DocxTestSupport.groovy | 35 ------------------- .../docx/parser/DocxAnchoredAreasTest.groovy | 2 +- .../docx/parser/DocxBodyContentTest.groovy | 2 +- .../docx/parser/DocxHeaderFootersTest.groovy | 2 +- .../example/docx/parser/DocxImagesTest.groovy | 2 +- .../parser/DocxParagraphParserTest.groovy | 2 +- .../docx/parser/DocxSectionsTest.groovy | 2 +- .../ParagraphContentCollectorTest.groovy | 2 +- 9 files changed, 13 insertions(+), 43 deletions(-) delete mode 100644 migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/Utils.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/Utils.groovy index d35a058e..7e5a9754 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/Utils.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/Utils.groovy @@ -16,17 +16,22 @@ import com.quadient.migration.api.repository.VariableRepository import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.service.PreviewProvider import com.quadient.migration.service.RefCollector +import com.quadient.migration.service.Storage import static org.mockito.Mockito.mock import static org.mockito.Mockito.when -static Migration mockMigration() { +static Migration mockMigration(Map context = [:]) { def migration = mock(Migration.class) def projectConfig = mock(ProjectConfig.class) when(migration.getProjectConfig()).thenReturn(projectConfig) when(projectConfig.getName()).thenReturn("testProject") when(projectConfig.getInspireOutput()).thenReturn(InspireOutput.Interactive) + when(projectConfig.getContext()).thenReturn(context) + + def storage = mock(Storage.class) + when(migration.getStorage()).thenReturn(storage) def varRepo = mock(VariableRepository.class) def structureRepo = mock(VariableStructureRepository.class) diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy deleted file mode 100644 index a623f20a..00000000 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/DocxTestSupport.groovy +++ /dev/null @@ -1,35 +0,0 @@ -package com.quadient.migration.example.docx - -import com.quadient.migration.api.Migration -import com.quadient.migration.api.ProjectConfig -import com.quadient.migration.api.repository.* -import com.quadient.migration.service.Storage - -import static org.mockito.Mockito.mock -import static org.mockito.Mockito.when - -class DocxTestSupport { - /** Creates fresh, ordinary mocks for the services used by the DOCX parser. */ - static Migration mockMigration(Map context = [:]) { - def migration = mock(Migration) - def variables = mock(VariableRepository) - def displayRules = mock(DisplayRuleRepository) - def documentObjects = mock(DocumentObjectRepository) - def textStyles = mock(TextStyleRepository) - def paragraphStyles = mock(ParagraphStyleRepository) - def images = mock(ImageRepository) - def storage = mock(Storage) - def projectConfig = mock(ProjectConfig) - - when(migration.variableRepository).thenReturn(variables) - when(migration.displayRuleRepository).thenReturn(displayRules) - when(migration.documentObjectRepository).thenReturn(documentObjects) - when(migration.textStyleRepository).thenReturn(textStyles) - when(migration.paragraphStyleRepository).thenReturn(paragraphStyles) - when(migration.imageRepository).thenReturn(images) - when(migration.storage).thenReturn(storage) - when(migration.projectConfig).thenReturn(projectConfig) - when(projectConfig.context).thenReturn(context) - return migration - } -} diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy index f8da7c85..ec5a1df4 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxAnchoredAreasTest.groovy @@ -16,7 +16,7 @@ import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFro import static org.mockito.Mockito.mock import static org.mockito.Mockito.when -import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration +import static com.quadient.migration.example.Utils.mockMigration class DocxAnchoredAreasTest { @Test diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy index 7b76f732..576264e4 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxBodyContentTest.groovy @@ -13,7 +13,7 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType import static org.mockito.Mockito.verify import static com.quadient.migration.example.docx.DocxFieldFixtures.* -import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration +import static com.quadient.migration.example.Utils.mockMigration class DocxBodyContentTest { diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy index 86fbb8da..7a9fcbc5 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxHeaderFootersTest.groovy @@ -7,7 +7,7 @@ import org.apache.poi.xwpf.usermodel.XWPFDocument import org.junit.jupiter.api.Test import org.openxmlformats.schemas.wordprocessingml.x2006.main.STHdrFtr -import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration +import static com.quadient.migration.example.Utils.mockMigration class DocxHeaderFootersTest { @Test diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy index 9c0082d3..80767fdf 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxImagesTest.groovy @@ -14,7 +14,7 @@ import org.mockito.ArgumentCaptor import static org.mockito.ArgumentMatchers.any import static org.mockito.Mockito.* -import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration +import static com.quadient.migration.example.Utils.mockMigration class DocxImagesTest { @BeforeEach diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy index 96dd9c96..a9667ec7 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxParagraphParserTest.groovy @@ -12,7 +12,7 @@ import static org.mockito.ArgumentMatchers.any import static org.mockito.Mockito.times import static org.mockito.Mockito.verify import static com.quadient.migration.example.docx.DocxFieldFixtures.* -import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration +import static com.quadient.migration.example.Utils.mockMigration class DocxParagraphParserTest { diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy index 7a95c6ea..e9cf3e56 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/DocxSectionsTest.groovy @@ -14,7 +14,7 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.STStyleType import static org.mockito.Mockito.times import static org.mockito.Mockito.verify -import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration +import static com.quadient.migration.example.Utils.mockMigration class DocxSectionsTest { diff --git a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy index beaae4cb..aeadf3eb 100644 --- a/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy +++ b/migration-examples/src/test/groovy/com/quadient/migration/example/docx/parser/ParagraphContentCollectorTest.groovy @@ -13,7 +13,7 @@ import static org.mockito.ArgumentMatchers.any import static org.mockito.Mockito.times import static org.mockito.Mockito.verify import static com.quadient.migration.example.docx.DocxFieldFixtures.* -import static com.quadient.migration.example.docx.DocxTestSupport.mockMigration +import static com.quadient.migration.example.Utils.mockMigration class ParagraphContentCollectorTest {