From d0a1f8c6203f76897f80d410ba60bd535aecdda2 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 13 Jun 2021 21:59:19 +0300 Subject: [PATCH 1/3] Coverage fix # Conflicts: # src/main/kotlin/org/xpathqs/core/selector/group/GroupSelectorModificationExtensions.kt # src/test/kotlin/org/xpathqs/core/selector/group/extensions/AddGroupArgTest.kt # src/test/kotlin/org/xpathqs/core/selector/group/extensions/PrefixTest.kt --- .../xpathqs/core/selector/block/BlockSelectorXpathTest.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/test/kotlin/org/xpathqs/core/selector/block/BlockSelectorXpathTest.kt b/src/test/kotlin/org/xpathqs/core/selector/block/BlockSelectorXpathTest.kt index 1acc059..3d7d90e 100644 --- a/src/test/kotlin/org/xpathqs/core/selector/block/BlockSelectorXpathTest.kt +++ b/src/test/kotlin/org/xpathqs/core/selector/block/BlockSelectorXpathTest.kt @@ -76,6 +76,7 @@ class BlockSelectorXpathTest : AnnotationSpec() { }.THEN("//div//p") } + @Test fun xpathForDynamic() { GIVEN { @@ -86,10 +87,6 @@ class BlockSelectorXpathTest : AnnotationSpec() { given.toString() given.toString() given.toXpath() - }.THEN( - "//div[position()=2]" - )/*.AND { - given.toXpath() - }*/ + }.THEN("//div[position()=2]") } } \ No newline at end of file From 5ea53ee904feb8a41b1f458850a89dfac0966c5f Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 11 Nov 2026 09:52:24 +0300 Subject: [PATCH 2/3] issue 199 --- .../core/exception/ReflectionException.kt | 29 --- .../JavaSelectorReflectionFields.kt | 198 ------------------ .../core/reflection/SelectorReflection.kt | 3 +- .../core/selector/base/BaseSelector.kt | 54 +++-- .../core/selector/compose/ComposeSelector.kt | 6 +- .../selector/compose/ComposeSelectorProps.kt | 10 +- .../core/selector/group/GroupSelector.kt | 5 +- .../core/selector/result/ResultSelector.kt | 5 +- .../core/selector/selector/Selector.kt | 6 +- .../core/selector/xpath/XpathSelector.kt | 6 +- .../xpathqs/core/selector/NullSelectorTest.kt | 17 +- .../base/BaseSelectorAnnotationsTest.kt | 13 +- .../compose/ComposeSelectorPropsTests.kt | 46 +++- 13 files changed, 137 insertions(+), 261 deletions(-) delete mode 100644 src/main/kotlin/org/xpathqs/core/exception/ReflectionException.kt delete mode 100644 src/main/kotlin/org/xpathqs/core/reflection/JavaSelectorReflectionFields.kt diff --git a/src/main/kotlin/org/xpathqs/core/exception/ReflectionException.kt b/src/main/kotlin/org/xpathqs/core/exception/ReflectionException.kt deleted file mode 100644 index 6c8d250..0000000 --- a/src/main/kotlin/org/xpathqs/core/exception/ReflectionException.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2023 XPATH-QS - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.xpathqs.core.exception - -class ReflectionException { - class NoSuchField : Exception( - "Member doesn't have a field" - ) -} \ No newline at end of file diff --git a/src/main/kotlin/org/xpathqs/core/reflection/JavaSelectorReflectionFields.kt b/src/main/kotlin/org/xpathqs/core/reflection/JavaSelectorReflectionFields.kt deleted file mode 100644 index d594a33..0000000 --- a/src/main/kotlin/org/xpathqs/core/reflection/JavaSelectorReflectionFields.kt +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2023 XPATH-QS - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.xpathqs.core.reflection - -import org.xpathqs.core.annotations.NoScan -import org.xpathqs.core.selector.NullSelector -import org.xpathqs.core.selector.base.BaseSelector -import org.xpathqs.core.selector.base.ISelector -import org.xpathqs.core.selector.block.Block -import org.xpathqs.core.util.SelectorFactory.tagSelector -import java.lang.Exception -import java.lang.reflect.Field -import kotlin.reflect.KClass - -/** - * Utility class for interaction with [BaseSelector] fields via Reflection - * @param rootObj object for interaction - * @sample org.xpathqs.core.reflection.SelectorReflectionFieldsTest - */ -class JavaSelectorReflectionFields( - private val rootObj: BaseSelector -) { - /** - * Returns collection of [BaseSelector]s inner objects of [rootObj] - */ - val innerSelectors: Collection by lazy { - innerSelectorFields.filter { - it.get(rootObj) !is NullSelector - }.map { - (it.get(rootObj) as BaseSelector).setName(it.name) - } - } - - /** - * Returns collection of [Block]s inner objects of [rootObj] - */ - val innerBlocks: Collection by lazy { - innerObjectClasses - .filter { - it.isObject() - }.map { - it.getObject() - } - } - - /** - * Returns collection of `Field`s of [rootObj] class and all subclasses - * until [BaseSelector] - */ - @Suppress("UNCHECKED_CAST") - val declaredFields: Collection - by lazy { - val res = ArrayList() - - var cls = rootObj::class.java - if(cls.isScanAvailable) { - res.addAll(cls.declaredFields) - - while (cls.superclass.isSelectorSubtype() && cls.isScanAvailable) { - cls = cls.superclass as Class - res.addAll(cls.declaredFields) - } - } - - removeUnnecessary(res) - } - - /** - * Filter [declaredFields] result by [BaseSelector] fields only - */ - val innerSelectorFields: Collection - by lazy { - val res = ArrayList() - - declaredFields.filter { it.name != "base"}.forEach { - if (it.type.isSelectorSubtype()) { - it.isAccessible = true - res.add(it) - } - } - - removeUnnecessary(res) - } - - /** - * Returns a collection of `Class` elements which are inherited from the [BaseSelector] - */ - val innerObjectClasses: Collection> - by lazy { - val res = ArrayList>() - val rootCls = rootObj::class.java - if(rootCls.isScanAvailable) { - if (rootCls.simpleName != BaseSelector::class.java.simpleName) { - rootCls.declaredClasses.forEach { - if (it.isSelectorSubtype()) { - if(it.isScanAvailable) { - res.add(it) - } - } - } - } - } - res - } - - /** - * Filter Unnecessary fields - */ - private fun removeUnnecessary(fields: Collection) = fields - .filter { - it.name != "INSTANCE" //remove object-class instances - && !it.name.contains("\$") //remove fields which was added dynamically - && it.isScanAvailable //remove fields annotated with "@NoScan - } - .distinctBy { it.name } - - private val Field.isScanAvailable: Boolean - get() = !this.isAnnotationPresent(NoScan::class.java) - - private val Class<*>.isScanAvailable: Boolean - get() = !this.isAnnotationPresent(NoScan::class.java) - - internal fun Any.isObject(): Boolean { - if (this is Class<*>) { - return this.declaredFields - .find { - it.name == "INSTANCE" - } != null - } - return this.javaClass.declaredFields - .find { - it.name == "INSTANCE" - } != null - } - - /** - * Check if object is a subtype of [Block] - * - * Requirements: - * #1 - true when object is a subtype of [Block] but not Block directly - * @sample [org.xpathqs.core.reflection.extensions.ReflectionExtensionsIsBlockSubtypeTests.r1] - * #2 - false for all others cases - */ - internal fun Any.isBlockSubtype(): Boolean { - return this is Block && this.javaClass.superclass.simpleName != "Block" - } - /** - * Get an Object instance of [Block] when class is an Object-class inherited from block - * @sample org.xpathqs.core.reflection.extensions.ReflectionExtensionsGetObjectTests - */ - internal fun Class<*>.getObject(): Block { - val instanceField = this.declaredFields - .find { it.name == "INSTANCE" } ?: throw IllegalArgumentException( - "Provided class ${this.name} doesn't used as an object-class" - ) - - return instanceField.get(null) as? Block ?: throw IllegalArgumentException( - "Provided class $name is not inherited from the Block class" - ) - } - - /** - * Check class for having [ISelector] as an inherited parent - */ - @Suppress("ReturnCount") - internal fun Class<*>.isSelectorSubtype(): Boolean { - if (this == BaseSelector::class.java - || this == NullSelector::class.java - || this == ISelector::class.java) { - return true - } - if (this.superclass == null) { - return false - } - return ISelector::class.java.isAssignableFrom(this.superclass) - || this.isAssignableFrom(ISelector::class.java) - } -} \ No newline at end of file diff --git a/src/main/kotlin/org/xpathqs/core/reflection/SelectorReflection.kt b/src/main/kotlin/org/xpathqs/core/reflection/SelectorReflection.kt index 4920661..f6a4e8e 100644 --- a/src/main/kotlin/org/xpathqs/core/reflection/SelectorReflection.kt +++ b/src/main/kotlin/org/xpathqs/core/reflection/SelectorReflection.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,6 @@ package org.xpathqs.core.reflection -import org.xpathqs.core.exception.ReflectionException import org.xpathqs.core.selector.args.SelectorArgs import org.xpathqs.core.selector.base.BaseSelector import org.xpathqs.core.selector.base.BaseSelectorProps diff --git a/src/main/kotlin/org/xpathqs/core/selector/base/BaseSelector.kt b/src/main/kotlin/org/xpathqs/core/selector/base/BaseSelector.kt index 6551b01..f6ab536 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/base/BaseSelector.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/base/BaseSelector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,7 +26,7 @@ import org.xpathqs.core.selector.NullSelector import kotlin.reflect.KProperty /** - * Base class for the Selectors + * Base interface for the Selectors * @param state of the Selector * @param base link to the root element * @param name name of the selector (used for logging purposes) @@ -35,25 +35,55 @@ import kotlin.reflect.KProperty * @param property associated field of the root object. * It may be NULL, when there is no root object */ -abstract class BaseSelector( - internal val state: SelectorState = SelectorState.INIT, +interface BaseSelector: ISelector { - val base: ISelector = NullSelector(), + val state: SelectorState + + val base: ISelector + override val name: String + override val fullName: String + + val props: BaseSelectorProps + val annotations: Collection + + val property: KProperty<*>? + val noBase: Boolean + + /** + * Get selector's tag or an empty string + * if tag is not applicable to the Selector's type + */ + val tag: String + + /** + * A holder collection for some custom properties + * which may be used for the extending the functionality via extension-functions + * + * Require #1 - map should not be cloned. It purposes only for holding some configuration constants + * @sample org.xpathqs.core.selector.base.BaseSelectorCustomPropsMapTest.r1 + */ + val customPropsMap: HashMap +} + +abstract class BaseSelectorImpl( + override val state: SelectorState = SelectorState.INIT, + + override val base: ISelector = NullSelector(), override val name: String = "", override val fullName: String = "", - internal open val props: BaseSelectorProps = BaseSelectorProps(), - val annotations: Collection = emptyList(), + override val props: BaseSelectorProps = BaseSelectorProps(), + override val annotations: Collection = emptyList(), - val property: KProperty<*>? = null, - val noBase: Boolean = false, -) : ISelector { + override val property: KProperty<*>? = null, + override val noBase: Boolean = false, +) : BaseSelector { /** * Get selector's tag or an empty string * if tag is not applicable to the Selector's type */ - abstract val tag: String + override val tag: String = "" /** * Build selector's xpath @@ -87,7 +117,7 @@ abstract class BaseSelector( * Require #1 - map should not be cloned. It purposes only for holding some configuration constants * @sample org.xpathqs.core.selector.base.BaseSelectorCustomPropsMapTest.r1 */ - val customPropsMap = HashMap() + override val customPropsMap = HashMap() companion object { fun mergeXpath(xp1: String, xp2: String): String { diff --git a/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelector.kt b/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelector.kt index 070b42e..ba67cad 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelector.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ package org.xpathqs.core.selector.compose import org.xpathqs.core.selector.NullSelector -import org.xpathqs.core.selector.base.BaseSelector +import org.xpathqs.core.selector.base.BaseSelectorImpl import org.xpathqs.core.selector.base.ISelector import org.xpathqs.core.selector.base.SelectorState @@ -39,7 +39,7 @@ class ComposeSelector( fullName: String = "", override val props: ComposeSelectorProps = ComposeSelectorProps() -) : BaseSelector( +) : BaseSelectorImpl( state = state, base = base, name = name, diff --git a/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorProps.kt b/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorProps.kt index 0519bdb..987803f 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorProps.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorProps.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -40,8 +40,9 @@ class ComposeSelectorProps( /** * Add new selector to the [selectors] collection */ - fun add(sel: ISelector) { + fun add(sel: ISelector) : ComposeSelectorProps { selectors.add(sel) + return this } /** @@ -76,7 +77,10 @@ class ComposeSelectorProps( override fun clone(): ComposeSelectorProps { val clonedSelectors = ArrayList() selectors.forEach { - clonedSelectors.add(it.clone()) + val c = it.clone() + clonedSelectors.add( + c + ) } return ComposeSelectorProps( diff --git a/src/main/kotlin/org/xpathqs/core/selector/group/GroupSelector.kt b/src/main/kotlin/org/xpathqs/core/selector/group/GroupSelector.kt index 3f7e3aa..3c46c30 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/group/GroupSelector.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/group/GroupSelector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,6 +24,7 @@ package org.xpathqs.core.selector.group import org.xpathqs.core.selector.NullSelector import org.xpathqs.core.selector.base.BaseSelector +import org.xpathqs.core.selector.base.BaseSelectorImpl import org.xpathqs.core.selector.base.BaseSelectorProps import org.xpathqs.core.selector.base.ISelector import org.xpathqs.core.selector.base.SelectorState @@ -41,7 +42,7 @@ open class GroupSelector( fullName: String = "", internal var selectorsChain: ArrayList = ArrayList(), -) : BaseSelector( +) : BaseSelectorImpl( state = state, base = base, name = name, props = props, fullName = fullName ) { /** diff --git a/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt b/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt index bd8a637..ca38cd3 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,6 +24,7 @@ package org.xpathqs.core.selector.result import org.xpathqs.core.selector.NullSelector import org.xpathqs.core.selector.base.BaseSelector +import org.xpathqs.core.selector.base.BaseSelectorImpl import org.xpathqs.core.selector.base.BaseSelectorProps import org.xpathqs.core.selector.base.ISelector import org.xpathqs.core.selector.base.SelectorState @@ -40,7 +41,7 @@ class ResultSelector( fullName: String = "", props: BaseSelectorProps = BaseSelectorProps() -) : BaseSelector(state, base, name, fullName, props) { +) : BaseSelectorImpl(state, base, name, fullName, props) { override fun toXpath(): String { return "(${base.toXpath() + wrapper.toXpath()})" + props.toXpath() diff --git a/src/main/kotlin/org/xpathqs/core/selector/selector/Selector.kt b/src/main/kotlin/org/xpathqs/core/selector/selector/Selector.kt index 1fddfb0..1a1fe97 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/selector/Selector.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/selector/Selector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ package org.xpathqs.core.selector.selector import org.xpathqs.core.selector.NullSelector -import org.xpathqs.core.selector.base.BaseSelector +import org.xpathqs.core.selector.base.BaseSelectorImpl import org.xpathqs.core.selector.base.ISelector import org.xpathqs.core.selector.base.SelectorState @@ -38,7 +38,7 @@ open class Selector( name: String = "", fullName: String = "", override val props: SelectorProps = SelectorProps() -) : BaseSelector( +) : BaseSelectorImpl( state = state, base = base, name = name, diff --git a/src/main/kotlin/org/xpathqs/core/selector/xpath/XpathSelector.kt b/src/main/kotlin/org/xpathqs/core/selector/xpath/XpathSelector.kt index 2633679..a000e18 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/xpath/XpathSelector.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/xpath/XpathSelector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ package org.xpathqs.core.selector.xpath import org.xpathqs.core.selector.NullSelector -import org.xpathqs.core.selector.base.BaseSelector +import org.xpathqs.core.selector.base.BaseSelectorImpl import org.xpathqs.core.selector.base.BaseSelectorProps import org.xpathqs.core.selector.base.ISelector import org.xpathqs.core.selector.base.SelectorState @@ -43,7 +43,7 @@ open class XpathSelector( fullName: String = "", props: BaseSelectorProps = BaseSelectorProps() -) : BaseSelector(state, base, name, fullName, props) { +) : BaseSelectorImpl(state, base, name, fullName, props) { override fun toXpath(): String { val baseXpath = if(noBase) "" else base.toXpath() diff --git a/src/test/kotlin/org/xpathqs/core/selector/NullSelectorTest.kt b/src/test/kotlin/org/xpathqs/core/selector/NullSelectorTest.kt index 158757b..c6942ac 100644 --- a/src/test/kotlin/org/xpathqs/core/selector/NullSelectorTest.kt +++ b/src/test/kotlin/org/xpathqs/core/selector/NullSelectorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,6 +23,7 @@ package org.xpathqs.core.selector import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeSameInstanceAs import org.xpathqs.xpathShouldBe @@ -36,4 +37,18 @@ class NullSelectorTest : AnnotationSpec() { val s = NullSelector() s shouldBeSameInstanceAs s.clone() } + + @Test + fun hashCodeTest() { + val s1 = NullSelector() + val s2 = NullSelector() + + s1.hashCode() shouldBe s2.hashCode() + } + + @Test + fun fullName() { + val s1 = NullSelector() + s1.fullName shouldBe "" + } } diff --git a/src/test/kotlin/org/xpathqs/core/selector/base/BaseSelectorAnnotationsTest.kt b/src/test/kotlin/org/xpathqs/core/selector/base/BaseSelectorAnnotationsTest.kt index d3f335e..fde4625 100644 --- a/src/test/kotlin/org/xpathqs/core/selector/base/BaseSelectorAnnotationsTest.kt +++ b/src/test/kotlin/org/xpathqs/core/selector/base/BaseSelectorAnnotationsTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,7 +29,6 @@ import org.xpathqs.core.annotations.NoScan import org.xpathqs.core.reflection.scan import org.xpathqs.core.selector.block.Block import org.xpathqs.core.util.SelectorFactory.tagSelector -import kotlin.reflect.jvm.kotlinProperty @Target( AnnotationTarget.CLASS, @@ -164,4 +163,14 @@ class BaseSelectorAnnotationsTest : AnnotationSpec() { fun r2_findParentWithAnnotation() { Page1.s1.findParentWithAnnotation(NoScan::class) shouldBe null } + + @Test + fun findAnnotationsEmpty() { + Page1.s1.findAnnotations() shouldBe emptyList() + } + + @Test + fun findAnnotationsNotEmpty() { + Page1.findAnnotations() shouldBe listOf(TestAnnotation()) + } } diff --git a/src/test/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorPropsTests.kt b/src/test/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorPropsTests.kt index dd13c47..b065120 100644 --- a/src/test/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorPropsTests.kt +++ b/src/test/kotlin/org/xpathqs/core/selector/compose/ComposeSelectorPropsTests.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,12 +22,56 @@ package org.xpathqs.core.selector.compose +import io.kotest.assertions.assertSoftly import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.inspectors.forAll +import io.kotest.inspectors.forOne +import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import org.xpathqs.core.reflection.freeze +import org.xpathqs.core.selector.NullSelector import org.xpathqs.core.util.SelectorFactory.tagSelector class ComposeSelectorPropsTests : AnnotationSpec() { + @Test + fun add() { + ComposeSelectorProps( + selectors = arrayListOf( + tagSelector("div") + ) + ).add(tagSelector("a")) + .toXpath() shouldBe "(//div) | (//a)" + } + + @Test + fun clone() { + val origin = ComposeSelectorProps( + selectors = arrayListOf( + tagSelector("div").freeze() + ) + ).add( + tagSelector("a").freeze() + ) + + val cloned = origin .clone() + assertSoftly { + cloned shouldNotBe origin + cloned.selectors shouldHaveSize origin.selectors.size + cloned.selectors shouldNotBeSameInstanceAs origin.selectors + cloned.selectors.filter { it !is NullSelector }.forAll { c -> + origin.selectors.forOne { o -> + c.name shouldBe o.name + c.xpath shouldBe o.xpath + c.fullName shouldBe o.fullName + c shouldNotBeSameInstanceAs o + } + } + } + } + @Test fun toXpathForEmpty() { ComposeSelectorProps().toXpath() shouldBe "" From 783e94875e2abd6c7ceb0017f6953d51baaee166 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 12 Nov 2026 07:19:27 +0300 Subject: [PATCH 3/3] issue 199 --- .../org/xpathqs/core/selector/block/Block.kt | 15 +--- .../extensions/SelectorCoreExtensions.kt | 5 +- .../core/selector/result/ResultSelector.kt | 3 +- .../result/ResultSelectorCoreExtensions.kt | 50 +++++++++++ .../selector/base/extension/PositionTest.kt | 37 +++++++- .../core/selector/block/CopyFromTest.kt | 49 +++++++++++ .../block/extensions/AnnotationsTest.kt | 86 +++++++++++++++++++ 7 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 src/main/kotlin/org/xpathqs/core/selector/result/ResultSelectorCoreExtensions.kt create mode 100644 src/test/kotlin/org/xpathqs/core/selector/block/extensions/AnnotationsTest.kt diff --git a/src/main/kotlin/org/xpathqs/core/selector/block/Block.kt b/src/main/kotlin/org/xpathqs/core/selector/block/Block.kt index d12001d..e307b61 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/block/Block.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/block/Block.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -139,6 +139,7 @@ open class Block( is Selector -> copyFrom(sel) is XpathSelector -> copyFrom(sel) is GroupSelector -> copyFrom(sel) + is ResultSelector -> copyFrom(sel) } } @@ -175,9 +176,9 @@ open class Block( } /** - * Will copy properties from the GroupSelector + * Will copy properties from the ResultSelector * - * Require - GroupSelector's properties should be copied + * Require - ResultSelector's properties should be copied * @sample [org.xpathqs.core.selector.block.CopyFromTest.copyFromGroupSelector] */ protected fun copyFrom(sel: ResultSelector) { @@ -247,14 +248,6 @@ open class Block( (property?.call(base) as? Block)?.setProps(originFieldProps) } - fun copyProps(other: Block?) { - if (other == null) { - return - } - selectorsChain = other.selectorsChain - setProps(other.props) - } - /** * Callback invoked during [org.xpathqs.core.reflection.SelectorParser.parse] method */ diff --git a/src/main/kotlin/org/xpathqs/core/selector/extensions/SelectorCoreExtensions.kt b/src/main/kotlin/org/xpathqs/core/selector/extensions/SelectorCoreExtensions.kt index f60f5e0..2936594 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/extensions/SelectorCoreExtensions.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/extensions/SelectorCoreExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,9 +32,11 @@ import org.xpathqs.core.selector.block.Block import org.xpathqs.core.selector.extensions.addArg import org.xpathqs.core.selector.group.GroupSelector import org.xpathqs.core.selector.group.addGroupArg +import org.xpathqs.core.selector.result.ResultSelector import org.xpathqs.core.selector.selector.Selector import org.xpathqs.core.selector.xpath.XpathSelector import org.xpathqs.core.selector.base.deepClone as baseDeepClone +import org.xpathqs.core.selector.result.deepClone as resultDeepClone import org.xpathqs.core.selector.block.deepClone as blockDeepClone import org.xpathqs.core.selector.group.deepClone as groupDeepClone import org.xpathqs.core.selector.xpath.deepClone as xpathDeepClone @@ -49,6 +51,7 @@ fun T.clone(): T { is Block -> blockDeepClone() is GroupSelector -> groupDeepClone() is Selector -> baseDeepClone() + is ResultSelector -> resultDeepClone() is XpathSelector -> xpathDeepClone() as T else -> this } diff --git a/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt b/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt index ca38cd3..6eb59da 100644 --- a/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt +++ b/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelector.kt @@ -23,7 +23,6 @@ package org.xpathqs.core.selector.result import org.xpathqs.core.selector.NullSelector -import org.xpathqs.core.selector.base.BaseSelector import org.xpathqs.core.selector.base.BaseSelectorImpl import org.xpathqs.core.selector.base.BaseSelectorProps import org.xpathqs.core.selector.base.ISelector @@ -34,7 +33,7 @@ import org.xpathqs.core.selector.base.SelectorState * Used for the complex xpath queries */ class ResultSelector( - private val wrapper: BaseSelector, + internal val wrapper: ISelector = NullSelector(), state: SelectorState = SelectorState.INIT, base: ISelector = NullSelector(), name: String = "", diff --git a/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelectorCoreExtensions.kt b/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelectorCoreExtensions.kt new file mode 100644 index 0000000..650a2ec --- /dev/null +++ b/src/main/kotlin/org/xpathqs/core/selector/result/ResultSelectorCoreExtensions.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 XPATH-QS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.xpathqs.core.selector.result + +import org.xpathqs.core.reflection.SelectorReflection +import org.xpathqs.core.reflection.cloned +import org.xpathqs.core.selector.base.BaseSelector +import org.xpathqs.core.selector.base.SelectorState +import org.xpathqs.core.selector.base.deepClone as baseDeepClone + + +/** + * @return clone of a [ResultSelector] Selector + * + */ +@Suppress("UNCHECKED_CAST") +internal fun T.deepClone(): T { + if (this.state != SelectorState.FREEZE) { + return this + } + + val cloned = this.baseDeepClone() + if(wrapper is BaseSelector) { + SelectorReflection( + cloned as BaseSelector, + ).setProp("wrapper", this.wrapper.cloned()) + } + + return cloned +} \ No newline at end of file diff --git a/src/test/kotlin/org/xpathqs/core/selector/base/extension/PositionTest.kt b/src/test/kotlin/org/xpathqs/core/selector/base/extension/PositionTest.kt index 0badf12..886470f 100644 --- a/src/test/kotlin/org/xpathqs/core/selector/base/extension/PositionTest.kt +++ b/src/test/kotlin/org/xpathqs/core/selector/base/extension/PositionTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 XPATH-QS + * Copyright (c) 2026 XPATH-QS * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -24,15 +24,25 @@ package org.xpathqs.core.selector.base.extension import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe import org.xpathqs.core.reflection.parse import org.xpathqs.core.selector.base.ISelector import org.xpathqs.core.selector.block.Block import org.xpathqs.core.selector.extensions.core.get +import org.xpathqs.core.selector.extensions.plus +import org.xpathqs.core.selector.extensions.result +import org.xpathqs.core.util.SelectorFactory.tagSelector import org.xpathqs.core.util.SelectorFactory.textSelector +import org.xpathqs.core.util.SelectorFactory.xpathSelector import org.xpathqs.xpathShouldBe object PositionTestPage: Block(textSelector("base1")) { val inner = Inner1(textSelector("s3")) + val inner2 = Inner1(xpathSelector ("//div")) + val inner3 = Inner1(tagSelector("div") + tagSelector("span")) + val inner4 = Inner1( + (tagSelector("div") + tagSelector("span")).result[2] + ) open class Inner1( ): Block() { @@ -54,5 +64,30 @@ class PositionTest : AnnotationSpec() { PositionTestPage.inner.xpathShouldBe("//*[text()='base1']//*[text()='s3']") PositionTestPage.inner[2].xpathShouldBe("//*[text()='base1']//*[text()='s3' and position()=2]") PositionTestPage.inner.xpathShouldBe("//*[text()='base1']//*[text()='s3']") + PositionTestPage.inner3.isBlank shouldBe false + } + + @Test + fun test2() { + PositionTestPage.inner2.xpathShouldBe("//*[text()='base1']//div") + PositionTestPage.inner2[2].xpathShouldBe("//*[text()='base1']//div[position()=2]") + PositionTestPage.inner2.xpathShouldBe("//*[text()='base1']//div") + PositionTestPage.inner3.isBlank shouldBe false + } + + @Test + fun test3() { + PositionTestPage.inner3.xpathShouldBe("//*[text()='base1']//div//span") + PositionTestPage.inner3[2].xpathShouldBe("//*[text()='base1'](//div//span)[position()=2]") + PositionTestPage.inner3.xpathShouldBe("//*[text()='base1']//div//span") + PositionTestPage.inner3.isBlank shouldBe false + } + + @Test + fun test4() { + PositionTestPage.inner4.xpathShouldBe("//*[text()='base1'](//div//span)[position()=2]") + PositionTestPage.inner4[3].xpathShouldBe("//*[text()='base1'](//div//span)[position()=3]") + PositionTestPage.inner4.xpathShouldBe("//*[text()='base1'](//div//span)[position()=2]") + PositionTestPage.inner4.isBlank shouldBe false } } \ No newline at end of file diff --git a/src/test/kotlin/org/xpathqs/core/selector/block/CopyFromTest.kt b/src/test/kotlin/org/xpathqs/core/selector/block/CopyFromTest.kt index c881c17..a28ac45 100644 --- a/src/test/kotlin/org/xpathqs/core/selector/block/CopyFromTest.kt +++ b/src/test/kotlin/org/xpathqs/core/selector/block/CopyFromTest.kt @@ -25,6 +25,7 @@ package org.xpathqs.core.selector.block import io.kotest.core.spec.style.AnnotationSpec import io.kotest.matchers.shouldBe import org.xpathqs.core.reflection.parse +import org.xpathqs.core.selector.base.ISelector import org.xpathqs.core.selector.extensions.plus import org.xpathqs.core.selector.group.GroupSelector import org.xpathqs.core.selector.selector.Selector @@ -48,6 +49,15 @@ class CopyFromTest : AnnotationSpec() { val s = tagSelector("div") } + class TestCls2(base: ISelector): Block(base) { + val s = tagSelector("div") + } + + @Test + fun isBlackTest() { + TestCls().parse().isBlank shouldBe true + } + /** * Checks [Block.copyFrom] */ @@ -64,6 +74,19 @@ class CopyFromTest : AnnotationSpec() { } } + @Test + fun copyFromSelector2() { + GIVEN { + TestCls2( + tagSelector("div1") + ).parse() + }.WHEN { + given.s.toXpath() + }.THEN { + "//div1//div" shouldBe actual + } + } + /** * Checks [Block.copyFrom] */ @@ -80,6 +103,19 @@ class CopyFromTest : AnnotationSpec() { } } + @Test + fun copyFromXpathSelector2() { + GIVEN { + TestCls2( + xpathSelector("//div1") + ).parse() + }.WHEN { + given.s.toXpath() + }.THEN { + "//div1//div" shouldBe actual + } + } + /** * Checks [Block.copyFrom] */ @@ -95,4 +131,17 @@ class CopyFromTest : AnnotationSpec() { "//div1//div2//div" shouldBe actual } } + + @Test + fun copyFromGroupSelector2() { + GIVEN { + TestCls2( + tagSelector("div1") + tagSelector("div2") + ).parse() + }.WHEN { + given.s.toXpath() + }.THEN { + "//div1//div2//div" shouldBe actual + } + } } \ No newline at end of file diff --git a/src/test/kotlin/org/xpathqs/core/selector/block/extensions/AnnotationsTest.kt b/src/test/kotlin/org/xpathqs/core/selector/block/extensions/AnnotationsTest.kt new file mode 100644 index 0000000..fb08fc3 --- /dev/null +++ b/src/test/kotlin/org/xpathqs/core/selector/block/extensions/AnnotationsTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 XPATH-QS + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.xpathqs.core.selector.block.extensions + +import io.kotest.core.spec.style.AnnotationSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.xpathqs.core.reflection.parse +import org.xpathqs.core.selector.block.Block +import org.xpathqs.core.selector.block.findAllWithAnnotation +import org.xpathqs.core.selector.block.findWithAnnotation +import org.xpathqs.core.util.SelectorFactory.tagSelector + +class AnnotationsTest : AnnotationSpec() { + annotation class TestAnnotation1 + annotation class TestAnnotation2 + + @TestAnnotation1 + object TestAnnBlock : Block() + + object TestAnnBlock3 : Block() { + @TestAnnotation1 + val s1 = tagSelector("div") + + @TestAnnotation1 + @TestAnnotation2 + val s2 = tagSelector("div") + } + + init { + TestAnnBlock.parse() + TestAnnBlock3.parse() + } + + @Test + fun findAllWithAnnotationForEmptyBlock() { + val r = TestAnnBlock.findAllWithAnnotation(TestAnnotation1::class) + r.size shouldBe 0 + } + + @Test + fun findWithAnnotationForEmptyBlock() { + val r = TestAnnBlock.findWithAnnotation(TestAnnotation1::class) + r shouldBe null + } + + @Test + fun findAllWithAnnotationForEmptyBlock2() { + val r = TestAnnBlock3.findAllWithAnnotation(TestAnnotation1::class) + r.size shouldBe 2 + } + + @Test + fun findWithAnnotationForEmptyBlock2() { + val r = TestAnnBlock3.findWithAnnotation(TestAnnotation1::class) + r shouldNotBe null + r!!.name shouldBe "TestAnnBlock3.s1" + } + + @Test + fun findWithAnnotationForEmptyBlock_2() { + val r = TestAnnBlock3.findWithAnnotation(TestAnnotation2::class) + r shouldNotBe null + r!!.name shouldBe "TestAnnBlock3.s2" + } +} \ No newline at end of file