This is an automated email from the ASF dual-hosted git repository.
mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new eab6742628 hardening on widget disable, fixes #7512 (#7515)
eab6742628 is described below
commit eab67426289af07e40066a1f41b1d9eb6d6af68b
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Jul 14 17:18:37 2026 +0200
hardening on widget disable, fixes #7512 (#7515)
* hardening on widget disable, fixes #7512
* fix test from spamming error stacks
---
.../generic/GenericDisabledWidgetsTest.java | 39 ++
.../databases/hive/HiveDisabledWidgetsTest.java | 39 ++
.../impala/ImpalaDisabledWidgetsTest.java | 39 ++
.../informix/InformixDisabledWidgetsTest.java | 39 ++
.../mssql/MsSqlServerDisabledWidgetsTest.java | 39 ++
.../mssqlnative/MsSqlServerNativeDatabaseMeta.java | 8 +-
.../MsSqlServerDisabledWidgetNpeTest.java | 43 ++
.../databases/mysql/MySqlDisabledWidgetsTest.java | 39 ++
.../SingleStoreDisabledWidgetsTest.java | 39 ++
.../snowflake/SnowflakeDisabledWidgetsTest.java | 39 ++
.../hop/beam/engines/BeamDisabledWidgetsTest.java | 39 ++
.../SingleThreadedDisabledWidgetsTest.java | 39 ++
.../DocumentationDisabledWidgetsTest.java | 39 ++
.../org/apache/hop/git/GitCommitPerspective.java | 54 ++-
.../java/org/apache/hop/git/GitPerspective.java | 76 ++--
.../hop/git/config/GitDisabledWidgetsTest.java | 39 ++
.../config/ProjectsDisabledWidgetsTest.java | 39 ++
.../reflection/probe/gui/DataProbeGuiPlugin.java | 9 +-
.../PipelineResolverDisabledWidgetsTest.java | 39 ++
.../arrow/datastream/ArrowDisabledWidgetsTest.java | 39 ++
.../vfs/azure/config/AzureDisabledWidgetsTest.java | 39 ++
.../metadata/CassandraDisabledWidgetsTest.java | 39 ++
.../databricks/DatabricksDisabledWidgetsTest.java | 39 ++
.../dropbox/config/DropboxDisabledWidgetsTest.java | 39 ++
.../elastic/ElasticDisabledWidgetsTest.java | 39 ++
.../vfs/gs/config/GoogleDisabledWidgetsTest.java | 42 ++
.../minio/metadata/MinioDisabledWidgetsTest.java | 39 ++
.../mongo/metadata/MongoDbConnectionEditor.java | 6 +
.../mongo/metadata/MongoDbDisabledWidgetsTest.java | 39 ++
.../neo4j/execution/Neo4jDisabledWidgetsTest.java | 39 ++
.../opensearch/OpenSearchDisabledWidgetsTest.java | 39 ++
.../resolver/vault/VaultDisabledWidgetsTest.java | 39 ++
.../streaminput/DatastreamDisabledWidgetsTest.java | 41 ++
.../transforms/ddl/DdlDisabledWidgetsTest.java | 39 ++
.../hop/ui/hopgui/DisabledGuiWidgetsNpeTest.java | 38 ++
.../hop/ui/core/gui/GuiCompositeWidgets.java | 20 +-
.../hop/ui/core/metadata/MetadataManager.java | 20 +-
.../hop/ui/core/widget/MetaSelectionLine.java | 6 +
.../org/apache/hop/ui/core/widget/OsHelper.java | 11 +-
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 27 +-
.../hopgui/perspective/HopPerspectiveManager.java | 8 +-
.../configuration/ConfigurationPerspective.java | 20 +-
.../execution/ExecutionPerspective.java | 47 +-
.../perspective/explorer/ExplorerPerspective.java | 15 +-
.../perspective/metadata/MetadataPerspective.java | 44 +-
.../search/HopGuiDescribedVariableSearchable.java | 8 +-
.../hop/ui/www/service/WebServiceGuiPlugin.java | 9 +-
.../perspective/DisabledPerspectivesTest.java | 79 ++++
.../ExecutionPerspectiveDisabledTest.java | 86 ++++
.../hop/ui/testing/DisabledGuiWidgetsTestBase.java | 481 +++++++++++++++++++++
50 files changed, 2129 insertions(+), 83 deletions(-)
diff --git
a/plugins/databases/generic/src/test/java/org/apache/hop/databases/generic/GenericDisabledWidgetsTest.java
b/plugins/databases/generic/src/test/java/org/apache/hop/databases/generic/GenericDisabledWidgetsTest.java
new file mode 100644
index 0000000000..1ec1dd79ca
--- /dev/null
+++
b/plugins/databases/generic/src/test/java/org/apache/hop/databases/generic/GenericDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.generic;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class GenericDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/hive/src/test/java/org/apache/hop/databases/hive/HiveDisabledWidgetsTest.java
b/plugins/databases/hive/src/test/java/org/apache/hop/databases/hive/HiveDisabledWidgetsTest.java
new file mode 100644
index 0000000000..2963c636de
--- /dev/null
+++
b/plugins/databases/hive/src/test/java/org/apache/hop/databases/hive/HiveDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.hive;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class HiveDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/impala/src/test/java/org/apache/hop/databases/impala/ImpalaDisabledWidgetsTest.java
b/plugins/databases/impala/src/test/java/org/apache/hop/databases/impala/ImpalaDisabledWidgetsTest.java
new file mode 100644
index 0000000000..b3517a19b5
--- /dev/null
+++
b/plugins/databases/impala/src/test/java/org/apache/hop/databases/impala/ImpalaDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.impala;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class ImpalaDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/informix/src/test/java/org/apache/hop/databases/informix/InformixDisabledWidgetsTest.java
b/plugins/databases/informix/src/test/java/org/apache/hop/databases/informix/InformixDisabledWidgetsTest.java
new file mode 100644
index 0000000000..403b45bbaf
--- /dev/null
+++
b/plugins/databases/informix/src/test/java/org/apache/hop/databases/informix/InformixDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.informix;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class InformixDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/mssql/src/test/java/org/apache/hop/databases/mssql/MsSqlServerDisabledWidgetsTest.java
b/plugins/databases/mssql/src/test/java/org/apache/hop/databases/mssql/MsSqlServerDisabledWidgetsTest.java
new file mode 100644
index 0000000000..bc84335c4d
--- /dev/null
+++
b/plugins/databases/mssql/src/test/java/org/apache/hop/databases/mssql/MsSqlServerDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.mssql;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class MsSqlServerDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
index 68efe29d62..d4b84ce1ec 100644
---
a/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
+++
b/plugins/databases/mssqlnative/src/main/java/org/apache/hop/databases/mssqlnative/MsSqlServerNativeDatabaseMeta.java
@@ -94,7 +94,13 @@ public class MsSqlServerNativeDatabaseMeta extends
MsSqlServerDatabaseMeta
Button wIntegratedSecurity =
(Button)
compositeWidgets.getWidgetsMap().get(ID_INTEGRATED_SECURITY_WIDGET);
- boolean enable = !wIntegratedSecurity.getSelection();
+ // The checkbox is absent when it is switched off in
disabledGuiElements.xml. There is nothing
+ // to read the setting from then, so we go by what is on the metadata
itself.
+ //
+ boolean enable =
+ wIntegratedSecurity == null
+ ? !isUsingIntegratedSecurity()
+ : !wIntegratedSecurity.getSelection();
for (Control control : controls) {
control.setEnabled(enable);
}
diff --git
a/plugins/databases/mssqlnative/src/test/java/org/apache/hop/databases/mssqlnative/MsSqlServerDisabledWidgetNpeTest.java
b/plugins/databases/mssqlnative/src/test/java/org/apache/hop/databases/mssqlnative/MsSqlServerDisabledWidgetNpeTest.java
new file mode 100644
index 0000000000..8564f54151
--- /dev/null
+++
b/plugins/databases/mssqlnative/src/test/java/org/apache/hop/databases/mssqlnative/MsSqlServerDisabledWidgetNpeTest.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.mssqlnative;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables the widgets of the MS SQL Server database metas, one at a time.
+ *
+ * <p>This is the case that put the harness here: {@code
+ * MsSqlServerNativeDatabaseMeta.enableField()} used to look the {@code
usingIntegratedSecurity}
+ * checkbox up by id and call {@code getSelection()} on it without a null
check - the username and
+ * password lookups right above it <em>were</em> guarded - so excluding that
one checkbox in
+ * disabledGuiElements.xml turned the database dialog into a
NullPointerException.
+ */
+@Tag("uitest")
+class MsSqlServerDisabledWidgetNpeTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // The hop-core/hop-engine/hop-ui widgets that also sit on this module's
classpath are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/mysql/src/test/java/org/apache/hop/databases/mysql/MySqlDisabledWidgetsTest.java
b/plugins/databases/mysql/src/test/java/org/apache/hop/databases/mysql/MySqlDisabledWidgetsTest.java
new file mode 100644
index 0000000000..99484dfa10
--- /dev/null
+++
b/plugins/databases/mysql/src/test/java/org/apache/hop/databases/mysql/MySqlDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.mysql;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class MySqlDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/singlestore/src/test/java/org/apache/hop/databases/singlestore/SingleStoreDisabledWidgetsTest.java
b/plugins/databases/singlestore/src/test/java/org/apache/hop/databases/singlestore/SingleStoreDisabledWidgetsTest.java
new file mode 100644
index 0000000000..11eaa850ee
--- /dev/null
+++
b/plugins/databases/singlestore/src/test/java/org/apache/hop/databases/singlestore/SingleStoreDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.singlestore;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class SingleStoreDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/databases/snowflake/src/test/java/org/apache/hop/databases/snowflake/SnowflakeDisabledWidgetsTest.java
b/plugins/databases/snowflake/src/test/java/org/apache/hop/databases/snowflake/SnowflakeDisabledWidgetsTest.java
new file mode 100644
index 0000000000..10ea65a6e1
--- /dev/null
+++
b/plugins/databases/snowflake/src/test/java/org/apache/hop/databases/snowflake/SnowflakeDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.snowflake;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class SnowflakeDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases");
+ }
+}
diff --git
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/BeamDisabledWidgetsTest.java
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/BeamDisabledWidgetsTest.java
new file mode 100644
index 0000000000..477e8a2def
--- /dev/null
+++
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/BeamDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.beam.engines;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class BeamDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.beam");
+ }
+}
diff --git
a/plugins/engines/single-threaded/src/test/java/org/apache/hop/pipeline/engines/singlethreaded/SingleThreadedDisabledWidgetsTest.java
b/plugins/engines/single-threaded/src/test/java/org/apache/hop/pipeline/engines/singlethreaded/SingleThreadedDisabledWidgetsTest.java
new file mode 100644
index 0000000000..c1991436df
--- /dev/null
+++
b/plugins/engines/single-threaded/src/test/java/org/apache/hop/pipeline/engines/singlethreaded/SingleThreadedDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.engines.singlethreaded;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class SingleThreadedDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.pipeline.engines.singlethreaded");
+ }
+}
diff --git
a/plugins/misc/documentation/src/test/java/org/apache/hop/documentation/DocumentationDisabledWidgetsTest.java
b/plugins/misc/documentation/src/test/java/org/apache/hop/documentation/DocumentationDisabledWidgetsTest.java
new file mode 100644
index 0000000000..6477e3b793
--- /dev/null
+++
b/plugins/misc/documentation/src/test/java/org/apache/hop/documentation/DocumentationDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.documentation;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class DocumentationDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.documentation");
+ }
+}
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
index d5216ff009..b919e537cf 100644
---
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
@@ -73,6 +73,7 @@ import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
@@ -136,9 +137,28 @@ public class GitCommitPerspective implements
IHopPerspective {
@GuiOsxKeyboardShortcut(command = true, shift = true, key = 'o', global =
true)
@Override
public void activate() {
+ if (hopGui == null) {
+ // The perspective is switched off in disabledGuiElements.xml: it was
never initialized, so
+ // there is nothing to switch to. The git toolbar and menu items that
lead here live on the
+ // explorer perspective and are still there.
+ //
+ return;
+ }
hopGui.setActivePerspective(this);
}
+ /**
+ * Enable or disable a context menu item. Every menu item can be switched
off with an exclusion
+ * for its id in disabledGuiElements.xml, in which case it was never created
and there is nothing
+ * here to enable.
+ */
+ private static void setMenuItemEnabled(GuiMenuWidgets menuWidgets, String
id, boolean enabled) {
+ MenuItem menuItem = menuWidgets.findMenuItem(id);
+ if (menuItem != null) {
+ menuItem.setEnabled(enabled);
+ }
+ }
+
@Override
public void perspectiveActivated() {
// TODO: avoid refresh when perspective is activated, detect change in the
config, metadata,
@@ -148,7 +168,7 @@ public class GitCommitPerspective implements
IHopPerspective {
@Override
public boolean isActive() {
- return hopGui.isActivePerspective(this);
+ return hopGui != null && hopGui.isActivePerspective(this);
}
@Override
@@ -232,28 +252,16 @@ public class GitCommitPerspective implements
IHopPerspective {
event -> {
List<UIFile> selectedFiles = this.getSelectedFiles();
- menuWidgets
- .findMenuItem(CONTEXT_MENU_ADD)
- .setEnabled(!getSelectedUntrackedFiles().isEmpty());
- menuWidgets
- .findMenuItem(CONTEXT_MENU_ADD_TO_GIT_IGNORE)
- .setEnabled(!selectedFiles.isEmpty());
- menuWidgets
- .findMenuItem(CONTEXT_MENU_SHOW_TEXT_DIFF)
- // .setEnabled(selectedFiles.stream().anyMatch(file ->
file.getChangeType() ==
- // DiffEntry.ChangeType.MODIFY));
- .setEnabled(!selectedFiles.isEmpty());
- menuWidgets
- .findMenuItem(CONTEXT_MENU_SHOW_GRAPH_DIFF)
- // .setEnabled(selectedFiles.stream().anyMatch(file ->
file.getChangeType() ==
- // DiffEntry.ChangeType.MODIFY));
- .setEnabled(
- selectedFiles.stream()
- .anyMatch(file ->
FileTypeUtils.isHopFileType(file.getName())));
- menuWidgets
- .findMenuItem(CONTEXT_MENU_RESTORE)
- .setEnabled(!getSelectedStagedFiles().isEmpty());
-
menuWidgets.findMenuItem(CONTEXT_MENU_DELETE).setEnabled(!selectedFiles.isEmpty());
+ setMenuItemEnabled(menuWidgets, CONTEXT_MENU_ADD,
!getSelectedUntrackedFiles().isEmpty());
+ setMenuItemEnabled(menuWidgets, CONTEXT_MENU_ADD_TO_GIT_IGNORE,
!selectedFiles.isEmpty());
+ setMenuItemEnabled(menuWidgets, CONTEXT_MENU_SHOW_TEXT_DIFF,
!selectedFiles.isEmpty());
+ setMenuItemEnabled(
+ menuWidgets,
+ CONTEXT_MENU_SHOW_GRAPH_DIFF,
+ selectedFiles.stream().anyMatch(file ->
FileTypeUtils.isHopFileType(file.getName())));
+ setMenuItemEnabled(
+ menuWidgets, CONTEXT_MENU_RESTORE,
!getSelectedStagedFiles().isEmpty());
+ setMenuItemEnabled(menuWidgets, CONTEXT_MENU_DELETE,
!selectedFiles.isEmpty());
// Show the menu
menu.setVisible(true);
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
index 71016752b7..c0ffcb7294 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
@@ -236,9 +236,28 @@ public class GitPerspective implements IHopPerspective {
@GuiOsxKeyboardShortcut(command = true, shift = true, key = 'g', global =
true)
@Override
public void activate() {
+ if (hopGui == null) {
+ // The perspective is switched off in disabledGuiElements.xml: it was
never initialized, so
+ // there is nothing to switch to. The git toolbar and menu items that
lead here live on the
+ // explorer perspective and are still there.
+ //
+ return;
+ }
hopGui.setActivePerspective(this);
}
+ /**
+ * Enable or disable a context menu item. Every menu item can be switched
off with an exclusion
+ * for its id in disabledGuiElements.xml, in which case it was never created
and there is nothing
+ * here to enable.
+ */
+ private static void setMenuItemEnabled(GuiMenuWidgets menuWidgets, String
id, boolean enabled) {
+ MenuItem menuItem = menuWidgets.findMenuItem(id);
+ if (menuItem != null) {
+ menuItem.setEnabled(enabled);
+ }
+ }
+
@Override
public void perspectiveActivated() {
// TODO: avoid refresh when perspective is activated, detect change in the
file explorer model
@@ -249,7 +268,7 @@ public class GitPerspective implements IHopPerspective {
@Override
public boolean isActive() {
- return hopGui.isActivePerspective(this);
+ return hopGui != null && hopGui.isActivePerspective(this);
}
@Override
@@ -371,23 +390,26 @@ public class GitPerspective implements IHopPerspective {
boolean isRemotes = ref.getName().startsWith(Constants.R_REMOTES);
boolean isTags = ref.getName().startsWith(Constants.R_TAGS);
-
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_CHECKOUT).setEnabled(!isCurrentBranch);
-
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_MERGE_BRANCH).setEnabled(!isCurrentBranch);
-
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_PUSH).setEnabled(isHeads);
-
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_PULL).setEnabled(isHeads);
-
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_RENAME).setEnabled(isHeads);
-
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_DELETE).setEnabled(!isCurrentBranch);
+ setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_CHECKOUT,
!isCurrentBranch);
+ setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_PUSH, isHeads);
+ setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_PULL, isHeads);
+ setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_RENAME, isHeads);
+ setMenuItemEnabled(refMenuWidgets, REF_CONTEXT_MENU_DELETE,
!isCurrentBranch);
MenuItem menuItem =
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_CREATE_BRANCH);
- menuItem.setEnabled(isHeads || isRemotes);
- menuItem.setText(
- BaseMessages.getString(PKG,
"GitPerspective.Menu.CreateBranchFrom.Text", branch));
+ if (menuItem != null) {
+ menuItem.setEnabled(isHeads || isRemotes);
+ menuItem.setText(
+ BaseMessages.getString(PKG,
"GitPerspective.Menu.CreateBranchFrom.Text", branch));
+ }
menuItem =
refMenuWidgets.findMenuItem(REF_CONTEXT_MENU_MERGE_BRANCH);
- menuItem.setEnabled(!isCurrentBranch);
- menuItem.setText(
- BaseMessages.getString(
- PKG, "GitPerspective.Menu.MergeInto.Text", branch,
currentBranch));
+ if (menuItem != null) {
+ menuItem.setEnabled(!isCurrentBranch);
+ menuItem.setText(
+ BaseMessages.getString(
+ PKG, "GitPerspective.Menu.MergeInto.Text", branch,
currentBranch));
+ }
});
// Create Tree editor for rename
@@ -437,10 +459,11 @@ public class GitPerspective implements IHopPerspective {
RevCommit commit = getSelectedCommit();
String path = getSelectedFile();
if (commit != null && path != null) {
-
fileMenuWidgets.findMenuItem(FILE_CONTEXT_MENU_SHOW_TEXT_DIFF).setEnabled(true);
- fileMenuWidgets
- .findMenuItem(FILE_CONTEXT_MENU_SHOW_GRAPH_DIFF)
- .setEnabled(FileTypeUtils.isHopFileType(path));
+ setMenuItemEnabled(fileMenuWidgets,
FILE_CONTEXT_MENU_SHOW_TEXT_DIFF, true);
+ setMenuItemEnabled(
+ fileMenuWidgets,
+ FILE_CONTEXT_MENU_SHOW_GRAPH_DIFF,
+ FileTypeUtils.isHopFileType(path));
} else {
event.doit = false;
}
@@ -1488,18 +1511,17 @@ public class GitPerspective implements IHopPerspective {
boolean isCommitInCurrentBranch = isCommitInCurrentBranch(commit);
- historyMenuWidgets
- .findMenuItem(HISTORY_CONTEXT_MENU_RESET)
- .setEnabled(isCommitInCurrentBranch);
+ setMenuItemEnabled(
+ historyMenuWidgets, HISTORY_CONTEXT_MENU_RESET,
isCommitInCurrentBranch);
// Revert commit with multiple parents isn't allowed
- historyMenuWidgets
- .findMenuItem(HISTORY_CONTEXT_MENU_REVERT_COMMIT)
- .setEnabled(isCommitInCurrentBranch && commit.getParentCount()
== 1);
+ setMenuItemEnabled(
+ historyMenuWidgets,
+ HISTORY_CONTEXT_MENU_REVERT_COMMIT,
+ isCommitInCurrentBranch && commit.getParentCount() == 1);
- historyMenuWidgets
- .findMenuItem(HISTORY_CONTEXT_MENU_CHERRY_PICK)
- .setEnabled(!isCommitInCurrentBranch);
+ setMenuItemEnabled(
+ historyMenuWidgets, HISTORY_CONTEXT_MENU_CHERRY_PICK,
!isCommitInCurrentBranch);
} else {
event.doit = false;
diff --git
a/plugins/misc/git/src/test/java/org/apache/hop/git/config/GitDisabledWidgetsTest.java
b/plugins/misc/git/src/test/java/org/apache/hop/git/config/GitDisabledWidgetsTest.java
new file mode 100644
index 0000000000..03ba09fbfe
--- /dev/null
+++
b/plugins/misc/git/src/test/java/org/apache/hop/git/config/GitDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.git.config;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class GitDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.git");
+ }
+}
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/config/ProjectsDisabledWidgetsTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/config/ProjectsDisabledWidgetsTest.java
new file mode 100644
index 0000000000..2e9e3397e7
--- /dev/null
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/config/ProjectsDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.config;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class ProjectsDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.projects");
+ }
+}
diff --git
a/plugins/misc/reflection/src/main/java/org/apache/hop/reflection/probe/gui/DataProbeGuiPlugin.java
b/plugins/misc/reflection/src/main/java/org/apache/hop/reflection/probe/gui/DataProbeGuiPlugin.java
index 61f8946299..4165cfcb81 100644
---
a/plugins/misc/reflection/src/main/java/org/apache/hop/reflection/probe/gui/DataProbeGuiPlugin.java
+++
b/plugins/misc/reflection/src/main/java/org/apache/hop/reflection/probe/gui/DataProbeGuiPlugin.java
@@ -115,13 +115,18 @@ public class DataProbeGuiPlugin {
// See if it's open in the metadata perspective...
//
+ // The perspective is absent when it is switched off in
disabledGuiElements.xml, and then
+ // nothing is open in it.
+ //
MetadataPerspective perspective =
(MetadataPerspective)
hopGui.getPerspectiveManager().findPerspective(MetadataPerspective.class);
String key =
PipelineProbe.class.getAnnotation(HopMetadata.class).key();
PipelineProbeEditor editor =
- (PipelineProbeEditor) perspective.findEditor(key,
pipelineProbe.getName());
- if (editor != null) {
+ perspective == null
+ ? null
+ : (PipelineProbeEditor) perspective.findEditor(key,
pipelineProbe.getName());
+ if (perspective != null && editor != null) {
// We're going to change the current metadata and flag it as
changed...
//
pipelineProbe = new PipelineProbe();
diff --git
a/plugins/resolvers/pipeline/src/test/java/org/apache/hop/resolvers/pipeline/PipelineResolverDisabledWidgetsTest.java
b/plugins/resolvers/pipeline/src/test/java/org/apache/hop/resolvers/pipeline/PipelineResolverDisabledWidgetsTest.java
new file mode 100644
index 0000000000..dede9510a0
--- /dev/null
+++
b/plugins/resolvers/pipeline/src/test/java/org/apache/hop/resolvers/pipeline/PipelineResolverDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.resolvers.pipeline;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class PipelineResolverDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.resolvers");
+ }
+}
diff --git
a/plugins/tech/arrow/src/test/java/org/apache/hop/arrow/datastream/ArrowDisabledWidgetsTest.java
b/plugins/tech/arrow/src/test/java/org/apache/hop/arrow/datastream/ArrowDisabledWidgetsTest.java
new file mode 100644
index 0000000000..a14b70495a
--- /dev/null
+++
b/plugins/tech/arrow/src/test/java/org/apache/hop/arrow/datastream/ArrowDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.arrow.datastream;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class ArrowDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.arrow");
+ }
+}
diff --git
a/plugins/tech/azure/src/test/java/org/apache/hop/vfs/azure/config/AzureDisabledWidgetsTest.java
b/plugins/tech/azure/src/test/java/org/apache/hop/vfs/azure/config/AzureDisabledWidgetsTest.java
new file mode 100644
index 0000000000..973c00c1d0
--- /dev/null
+++
b/plugins/tech/azure/src/test/java/org/apache/hop/vfs/azure/config/AzureDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.vfs.azure.config;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class AzureDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.vfs.azure",
"org.apache.hop.core.variables.resolver");
+ }
+}
diff --git
a/plugins/tech/cassandra/src/test/java/org/apache/hop/databases/cassandra/metadata/CassandraDisabledWidgetsTest.java
b/plugins/tech/cassandra/src/test/java/org/apache/hop/databases/cassandra/metadata/CassandraDisabledWidgetsTest.java
new file mode 100644
index 0000000000..77eb6bee32
--- /dev/null
+++
b/plugins/tech/cassandra/src/test/java/org/apache/hop/databases/cassandra/metadata/CassandraDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.databases.cassandra.metadata;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class CassandraDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.databases.cassandra");
+ }
+}
diff --git
a/plugins/tech/databricks/src/test/java/org/apache/hop/database/databricks/DatabricksDisabledWidgetsTest.java
b/plugins/tech/databricks/src/test/java/org/apache/hop/database/databricks/DatabricksDisabledWidgetsTest.java
new file mode 100644
index 0000000000..9d11e5d143
--- /dev/null
+++
b/plugins/tech/databricks/src/test/java/org/apache/hop/database/databricks/DatabricksDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.database.databricks;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class DatabricksDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.database.databricks");
+ }
+}
diff --git
a/plugins/tech/dropbox/src/test/java/org/apache/hop/vfs/dropbox/config/DropboxDisabledWidgetsTest.java
b/plugins/tech/dropbox/src/test/java/org/apache/hop/vfs/dropbox/config/DropboxDisabledWidgetsTest.java
new file mode 100644
index 0000000000..5b00d56817
--- /dev/null
+++
b/plugins/tech/dropbox/src/test/java/org/apache/hop/vfs/dropbox/config/DropboxDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.vfs.dropbox.config;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class DropboxDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.vfs.dropbox");
+ }
+}
diff --git
a/plugins/tech/elastic/src/test/java/org/apache/hop/execution/elastic/ElasticDisabledWidgetsTest.java
b/plugins/tech/elastic/src/test/java/org/apache/hop/execution/elastic/ElasticDisabledWidgetsTest.java
new file mode 100644
index 0000000000..ab3880184b
--- /dev/null
+++
b/plugins/tech/elastic/src/test/java/org/apache/hop/execution/elastic/ElasticDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.execution.elastic;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class ElasticDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.execution.elastic");
+ }
+}
diff --git
a/plugins/tech/google/src/test/java/org/apache/hop/vfs/gs/config/GoogleDisabledWidgetsTest.java
b/plugins/tech/google/src/test/java/org/apache/hop/vfs/gs/config/GoogleDisabledWidgetsTest.java
new file mode 100644
index 0000000000..d973cfd17f
--- /dev/null
+++
b/plugins/tech/google/src/test/java/org/apache/hop/vfs/gs/config/GoogleDisabledWidgetsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.vfs.gs.config;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class GoogleDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of(
+ "org.apache.hop.vfs.gs",
+ "org.apache.hop.vfs.googledrive",
+ "org.apache.hop.core.variables.resolver");
+ }
+}
diff --git
a/plugins/tech/minio/src/test/java/org/apache/hop/vfs/minio/metadata/MinioDisabledWidgetsTest.java
b/plugins/tech/minio/src/test/java/org/apache/hop/vfs/minio/metadata/MinioDisabledWidgetsTest.java
new file mode 100644
index 0000000000..21c66a5ef9
--- /dev/null
+++
b/plugins/tech/minio/src/test/java/org/apache/hop/vfs/minio/metadata/MinioDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.vfs.minio.metadata;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class MinioDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.vfs.minio");
+ }
+}
diff --git
a/plugins/tech/mongodb/src/main/java/org/apache/hop/mongo/metadata/MongoDbConnectionEditor.java
b/plugins/tech/mongodb/src/main/java/org/apache/hop/mongo/metadata/MongoDbConnectionEditor.java
index 23da8a6cb3..3f4629c83a 100644
---
a/plugins/tech/mongodb/src/main/java/org/apache/hop/mongo/metadata/MongoDbConnectionEditor.java
+++
b/plugins/tech/mongodb/src/main/java/org/apache/hop/mongo/metadata/MongoDbConnectionEditor.java
@@ -153,6 +153,12 @@ public class MongoDbConnectionEditor extends
MetadataEditor<MongoDbConnection>
public void getDbNames() {
ComboVar wDbName = (ComboVar)
widgets.getWidgetsMap().get(MongoDbConnection.WIDGET_ID_DB_NAME);
+ if (wDbName == null) {
+ // The database name combo can be switched off in
disabledGuiElements.xml, in which case there
+ // is nothing to fill in.
+ //
+ return;
+ }
IVariables variables = manager.getVariables();
ILogChannel log = LogChannel.UI;
diff --git
a/plugins/tech/mongodb/src/test/java/org/apache/hop/mongo/metadata/MongoDbDisabledWidgetsTest.java
b/plugins/tech/mongodb/src/test/java/org/apache/hop/mongo/metadata/MongoDbDisabledWidgetsTest.java
new file mode 100644
index 0000000000..d9c4c761d5
--- /dev/null
+++
b/plugins/tech/mongodb/src/test/java/org/apache/hop/mongo/metadata/MongoDbDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.mongo.metadata;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class MongoDbDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.mongo");
+ }
+}
diff --git
a/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/Neo4jDisabledWidgetsTest.java
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/Neo4jDisabledWidgetsTest.java
new file mode 100644
index 0000000000..00bf55f234
--- /dev/null
+++
b/plugins/tech/neo4j/src/test/java/org/apache/hop/neo4j/execution/Neo4jDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.neo4j.execution;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class Neo4jDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.neo4j");
+ }
+}
diff --git
a/plugins/tech/opensearch/src/test/java/org/apache/hop/execution/opensearch/OpenSearchDisabledWidgetsTest.java
b/plugins/tech/opensearch/src/test/java/org/apache/hop/execution/opensearch/OpenSearchDisabledWidgetsTest.java
new file mode 100644
index 0000000000..65e01bea9d
--- /dev/null
+++
b/plugins/tech/opensearch/src/test/java/org/apache/hop/execution/opensearch/OpenSearchDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.execution.opensearch;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class OpenSearchDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.execution.opensearch");
+ }
+}
diff --git
a/plugins/tech/vault/src/test/java/org/apache/hop/core/variables/resolver/vault/VaultDisabledWidgetsTest.java
b/plugins/tech/vault/src/test/java/org/apache/hop/core/variables/resolver/vault/VaultDisabledWidgetsTest.java
new file mode 100644
index 0000000000..56c53f14cf
--- /dev/null
+++
b/plugins/tech/vault/src/test/java/org/apache/hop/core/variables/resolver/vault/VaultDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.core.variables.resolver.vault;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class VaultDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.core.variables.resolver.vault");
+ }
+}
diff --git
a/plugins/transforms/datastream/src/test/java/org/apache/hop/pipeline/transforms/streaminput/DatastreamDisabledWidgetsTest.java
b/plugins/transforms/datastream/src/test/java/org/apache/hop/pipeline/transforms/streaminput/DatastreamDisabledWidgetsTest.java
new file mode 100644
index 0000000000..5b92b2ef6a
--- /dev/null
+++
b/plugins/transforms/datastream/src/test/java/org/apache/hop/pipeline/transforms/streaminput/DatastreamDisabledWidgetsTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.streaminput;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class DatastreamDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of(
+ "org.apache.hop.pipeline.transforms.streaminput",
+ "org.apache.hop.pipeline.transforms.streamoutput");
+ }
+}
diff --git
a/plugins/transforms/ddl/src/test/java/org/apache/hop/pipeline/transforms/ddl/DdlDisabledWidgetsTest.java
b/plugins/transforms/ddl/src/test/java/org/apache/hop/pipeline/transforms/ddl/DdlDisabledWidgetsTest.java
new file mode 100644
index 0000000000..d3c3f94605
--- /dev/null
+++
b/plugins/transforms/ddl/src/test/java/org/apache/hop/pipeline/transforms/ddl/DdlDisabledWidgetsTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.ddl;
+
+import java.util.List;
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} this plugin registers, one at a
time, and checks that
+ * its composite still builds and still survives the listener callbacks. See
{@link
+ * DisabledGuiWidgetsTestBase}.
+ */
+@Tag("uitest")
+class DdlDisabledWidgetsTest extends DisabledGuiWidgetsTestBase {
+
+ @Override
+ protected List<String> packagesUnderTest() {
+ // hop-core, hop-engine and hop-ui are on this module's classpath too;
their widgets are covered
+ // by the same harness in hop-ui-rcp.
+ //
+ return List.of("org.apache.hop.pipeline.transforms.ddl");
+ }
+}
diff --git
a/rcp/src/test/java/org/apache/hop/ui/hopgui/DisabledGuiWidgetsNpeTest.java
b/rcp/src/test/java/org/apache/hop/ui/hopgui/DisabledGuiWidgetsNpeTest.java
new file mode 100644
index 0000000000..b6e8ad98d7
--- /dev/null
+++ b/rcp/src/test/java/org/apache/hop/ui/hopgui/DisabledGuiWidgetsNpeTest.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import org.apache.hop.ui.testing.DisabledGuiWidgetsTestBase;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Disables every {@code @GuiWidgetElement} that hop-core, hop-engine and
hop-ui register - one at a
+ * time - and checks that its composite still builds and still survives the
listener callbacks.
+ *
+ * <p>This lives in hop-ui-rcp rather than in hop-ui because the harness needs
the standalone
+ * look-and-feel implementations (TextSizeUtilFacadeImpl and friends) that
this module supplies, and
+ * hop-ui cannot depend on them. The widgets that plugins add are covered by
the same harness in
+ * each plugin's own module: a module only ever sees the widgets on its own
classpath.
+ *
+ * <p>Run: {@code mvn -pl rcp test} (skipped without a display; {@code
-XstartOnFirstThread} is
+ * added automatically on macOS).
+ */
+@Tag("uitest")
+class DisabledGuiWidgetsNpeTest extends DisabledGuiWidgetsTestBase {
+ // No package filter: everything on this module's classpath is hop-core,
hop-engine or hop-ui.
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java
index d400416e1c..ce7b08e218 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java
@@ -856,6 +856,13 @@ public class GuiCompositeWidgets {
Control control = widgetsMap.get(guiElements.getId());
if (control != null) {
+ // A widget element declared on a method - a composite, a link, a
button - has no field
+ // behind it, so there is no value to put into it.
+ //
+ if (guiElements.getFieldName() == null) {
+ return;
+ }
+
// What's the value?
//
Object value = null;
@@ -866,11 +873,7 @@ public class GuiCompositeWidgets {
.invoke(sourceData);
} catch (Exception e) {
LogChannel.UI.logError(
- "Unable to get value for field: '"
- + guiElements.getFieldName()
- + "' : "
- + e.getMessage());
- e.printStackTrace();
+ "Unable to get value for field: '" + guiElements.getFieldName()
+ "'", e);
}
String stringValue = value == null ? "" : Const.NVL(value.toString(),
"");
@@ -976,6 +979,13 @@ public class GuiCompositeWidgets {
Control control = widgetsMap.get(guiElements.getId());
if (control != null) {
+ // A widget element declared on a method - a composite, a link, a
button - has no field
+ // behind it, so there is no value to retrieve from it.
+ //
+ if (guiElements.getFieldName() == null) {
+ return;
+ }
+
// What's the value?
//
Object value = null;
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
index 39d2cef91b..76a7e7889b 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataManager.java
@@ -187,10 +187,11 @@ public class MetadataManager<T extends IHopMetadata> {
MetadataEditor<T> editor = this.createEditor(element);
editor.setTitle(element.getName());
- // Open this element in the metadata perspective if that one is active.
+ // Open this element in the metadata perspective if that one is active.
The perspective is
+ // absent when it is switched off in disabledGuiElements.xml, and we
fall back to the dialog.
//
MetadataPerspective perspective = HopGui.getMetadataPerspective();
- if (perspective.isActive()) {
+ if (perspective != null && perspective.isActive()) {
perspective.addEditor(editor);
return false;
} else {
@@ -498,7 +499,20 @@ public class MetadataManager<T extends IHopMetadata> {
"MetadataManager.New.Label",
TranslateUtil.translate(this.getManagedName(), managedClass)));
- MetadataPerspective perspective = MetadataPerspective.getInstance();
+ // The metadata perspective is absent when it is switched off in
disabledGuiElements.xml.
+ // There is no editor tab to open then, so we edit the new element in a
dialog instead.
+ //
+ MetadataPerspective perspective = HopGui.getMetadataPerspective();
+ if (perspective == null) {
+ MetadataEditorDialog dialog = new
MetadataEditorDialog(hopGui.getActiveShell(), editor);
+ if (dialog.open() == null) {
+ return null;
+ }
+ ExtensionPointHandler.callExtensionPoint(
+ hopGui.getLog(), variables,
HopExtensionPoint.HopGuiMetadataObjectCreated.id, element);
+ return element;
+ }
+
perspective.activate();
perspective.addEditor(editor);
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
index c635aa1bfa..5010739331 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/MetaSelectionLine.java
@@ -286,6 +286,12 @@ public class MetaSelectionLine<T extends IHopMetadata>
extends Composite {
image = "ui/images/metadata.svg")
public void viewInPerspective() {
MetadataPerspective perspective = HopGui.getMetadataPerspective();
+ if (perspective == null) {
+ // The metadata perspective is switched off in disabledGuiElements.xml:
there is nowhere to
+ // view the element.
+ //
+ return;
+ }
perspective.activate();
String elementName = variables.resolve(wCombo.getText());
if (StringUtils.isEmpty(elementName)) {
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/OsHelper.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/OsHelper.java
index 5c6ed0339b..c11a40da8c 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/OsHelper.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/OsHelper.java
@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.ui.hopgui.HopGui;
+import
org.apache.hop.ui.hopgui.perspective.configuration.ConfigurationPerspective;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
@@ -127,8 +128,16 @@ public class OsHelper {
break;
case SWT.ID_PREFERENCES:
+ // The perspective is absent when it is switched off in
disabledGuiElements.xml.
+ //
item.addListener(
- SWT.Selection, event ->
HopGui.getConfigurationPerspective().activate());
+ SWT.Selection,
+ event -> {
+ ConfigurationPerspective perspective =
HopGui.getConfigurationPerspective();
+ if (perspective != null) {
+ perspective.activate();
+ }
+ });
break;
default:
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 920bbd46a8..c0163521e7 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -571,7 +571,10 @@ public class HopGui
// Activate the default perspective
//
- getExplorerPerspective().activate();
+ IHopPerspective defaultPerspective = getDefaultPerspective();
+ if (defaultPerspective != null) {
+ defaultPerspective.activate();
+ }
// See if we need to show the Welcome dialog
//
@@ -2026,7 +2029,12 @@ public class HopGui
public void setActivePerspective(IHopPerspective perspective) {
if (perspective == null) {
- perspective = getExplorerPerspective();
+ perspective = getDefaultPerspective();
+ }
+ if (perspective == null) {
+ // Every single perspective is disabled: there is nothing to put on top.
+ //
+ return;
}
activePerspective = perspective;
@@ -2406,6 +2414,21 @@ public class HopGui
return
HopGui.getInstance().getPerspectiveManager().findPerspective(ExplorerPerspective.class);
}
+ /**
+ * The perspective to show when no other one is selected. That is normally
the explorer
+ * perspective, but it can be switched off with an exclusion in
disabledGuiElements.xml, in which
+ * case we settle for the first perspective that did get loaded.
+ *
+ * @return The default perspective, or null if every perspective is disabled.
+ */
+ public IHopPerspective getDefaultPerspective() {
+ ExplorerPerspective explorerPerspective = getExplorerPerspective();
+ if (explorerPerspective != null) {
+ return explorerPerspective;
+ }
+ return
perspectiveManager.getPerspectives().stream().findFirst().orElse(null);
+ }
+
public static ConfigurationPerspective getConfigurationPerspective() {
return HopGui.getInstance()
.getPerspectiveManager()
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/HopPerspectiveManager.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/HopPerspectiveManager.java
index c722073b5c..e64366ae3b 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/HopPerspectiveManager.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/HopPerspectiveManager.java
@@ -19,7 +19,7 @@ package org.apache.hop.ui.hopgui.perspective;
import java.util.ArrayList;
import java.util.Comparator;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
@@ -43,7 +43,11 @@ public class HopPerspectiveManager {
public HopPerspectiveManager(HopGui hopGui) {
this.hopGui = hopGui;
- this.perspectivesMap = new HashMap<>();
+ // Insertion ordered: HopGui adds the perspectives sorted by plugin id,
which is the order they
+ // appear in the sidebar. getPerspectives() then hands them out in that
same order, so falling
+ // back to "the first perspective" is predictable.
+ //
+ this.perspectivesMap = new LinkedHashMap<>();
this.listeners = new ConcurrentLinkedQueue<>();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
index d37028ea62..4f19a70ac5 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
@@ -131,9 +131,22 @@ public class ConfigurationPerspective implements
IHopPerspective {
@GuiOsxKeyboardShortcut(command = true, shift = true, key = 'c', global =
true)
@Override
public void activate() {
+ if (!isInitialized()) {
+ return;
+ }
hopGui.setActivePerspective(this);
}
+ /**
+ * When this perspective is disabled (an exclusion in
disabledGuiElements.xml) HopGui skips it
+ * while loading the perspectives, so initialize() never runs. The singleton
still exists because
+ * the class is instantiated to register the GUI elements it declares, so
callers reaching it
+ * through getInstance() get an instance without a HopGui or any widgets to
work with.
+ */
+ private boolean isInitialized() {
+ return hopGui != null;
+ }
+
@Override
public void perspectiveActivated() {
// Reload values in all tabs when perspective is activated
@@ -164,7 +177,7 @@ public class ConfigurationPerspective implements
IHopPerspective {
@Override
public boolean isActive() {
- return hopGui.isActivePerspective(this);
+ return isInitialized() && hopGui.isActivePerspective(this);
}
@Override
@@ -848,6 +861,11 @@ public class ConfigurationPerspective implements
IHopPerspective {
}
public void showSystemVariablesTab() {
+ if (!isInitialized()) {
+ // There is no tree to navigate.
+ //
+ return;
+ }
// Navigate to system variables in the tree
for (TreeItem item : categoryTree.getItems()) {
if (item.getText().toLowerCase().contains("variable")) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
index 28a30dd754..6f2293cde6 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspective.java
@@ -197,6 +197,17 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
instance = this;
}
+ /**
+ * When this perspective is disabled (an exclusion in
disabledGuiElements.xml) HopGui skips it
+ * while loading the perspectives, so {@link #initialize(HopGui, Composite)}
never runs. The
+ * singleton still exists because the @GuiPlugin class is instantiated to
register the GUI
+ * elements it declares, so callers reaching us through {@link
#getInstance()} get an
+ * uninitialized instance which has no widgets and no state to work with.
+ */
+ private boolean isInitialized() {
+ return hopGui != null;
+ }
+
@Override
public String getId() {
return "execution-perspective";
@@ -206,6 +217,9 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
@GuiOsxKeyboardShortcut(command = true, shift = true, key = 'i', global =
true)
@Override
public void activate() {
+ if (!isInitialized()) {
+ return;
+ }
// Prevents refreshes when not needed.
if (!hopGui.isActivePerspective(this)) {
hopGui.setActivePerspective(this);
@@ -221,7 +235,7 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
@Override
public boolean isActive() {
- return hopGui.isActivePerspective(this);
+ return isInitialized() && hopGui.isActivePerspective(this);
}
@Override
@@ -690,7 +704,7 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
// Only refresh if we're actually displaying anything.
// Let's be conservative with responsiveness.
//
- if (!hopGui.isActivePerspective(this)) {
+ if (!isInitialized() || !hopGui.isActivePerspective(this)) {
return;
}
@@ -936,8 +950,10 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
toolTip = "i18n::ExecutionPerspective.ToolbarElement.TimeFilter.Tooltip")
public void selectTimeFilter() {
ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_TIME_FILTER);
- String lastPeriodDescription = ((Combo) item.getControl()).getText();
- this.timeFilter = LastPeriod.lookupDescription(lastPeriodDescription);
+ if (item == null || !(item.getControl() instanceof Combo combo)) {
+ return;
+ }
+ this.timeFilter = LastPeriod.lookupDescription(combo.getText());
// Update the icon && apply the filter
updateGui();
@@ -958,8 +974,13 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
this.onlyShowingFailed = false;
this.filterText = "";
+ // The filter box itself can be switched off in disabledGuiElements.xml
while this button is
+ // not, in which case there is no box to clear.
+ //
ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_FILTER_TEXT);
- ((Text) item.getControl()).setText(FILTER_NAME_DATE_ID);
+ if (item != null && item.getControl() instanceof Text text) {
+ text.setText(FILTER_NAME_DATE_ID);
+ }
// Update the icon && apply the filter
updateGui();
@@ -974,7 +995,10 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
defaultText = FILTER_NAME_DATE_ID)
public void selectTextFilter() {
ToolItem item = toolBarWidgets.findToolItem(TOOLBAR_ITEM_FILTER_TEXT);
- this.filterText = ((Text) item.getControl()).getText();
+ if (item == null || !(item.getControl() instanceof Text text)) {
+ return;
+ }
+ this.filterText = text.getText();
// Update the icon && apply the filter
updateGui();
@@ -1175,6 +1199,12 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
}
public void saveState() {
+ // Nothing was ever restored or shown, so saving would only overwrite the
state on disk with
+ // the field defaults.
+ //
+ if (!isInitialized()) {
+ return;
+ }
try {
AuditStateMap stateMap = new AuditStateMap();
stateMap.add(
@@ -1207,6 +1237,11 @@ public class ExecutionPerspective implements
IHopPerspective, TabClosable {
/** Restore the state of this perspective. */
public void restoreState() {
+ // There is no toolbar or tree to restore the state into.
+ //
+ if (!isInitialized()) {
+ return;
+ }
try {
AuditStateMap stateMap =
AuditManager.getActive()
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index a6adbaed89..4c943ddeac 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -316,7 +316,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public boolean isActive() {
- return hopGui.isActivePerspective(this);
+ return hopGui != null && hopGui.isActivePerspective(this);
}
@Override
@@ -634,10 +634,13 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
MenuItem openInExplorerItem =
menuWidgets.findMenuItem(CONTEXT_MENU_OPEN_IN_EXPLORER);
if (openInExplorerItem != null) {
- openInExplorerItem.setEnabled(HopVfs.isLocalFileSystem(tif.path));
+ openInExplorerItem.setEnabled(tif != null &&
HopVfs.isLocalFileSystem(tif.path));
}
-
menuWidgets.findMenuItem(CONTEXT_MENU_RENAME).setEnabled(selection.length == 1);
+ MenuItem renameItem = menuWidgets.findMenuItem(CONTEXT_MENU_RENAME);
+ if (renameItem != null) {
+ renameItem.setEnabled(selection.length == 1);
+ }
// Finding references is only meaningful for a single pipeline or
workflow file.
//
@@ -4117,6 +4120,12 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
/** Save the file explorer panel visibility state so it persists across
restarts. */
public void saveExplorerStateOnShutdown() {
+ // When the perspective is disabled it was never initialized, so saving
would only overwrite
+ // the state on disk with the field defaults.
+ //
+ if (hopGui == null) {
+ return;
+ }
try {
AuditStateMap stateMap = new AuditStateMap();
stateMap.add(
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
index f9e2fc874d..56a4eb1d9b 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
@@ -241,6 +241,17 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
this.metadataFileType = new MetadataFileType();
}
+ /**
+ * When this perspective is disabled (an exclusion in
disabledGuiElements.xml) HopGui skips it
+ * while loading the perspectives, so initialize() never runs. The singleton
still exists because
+ * the class is instantiated to register the GUI elements it declares, so
callers reaching it
+ * through {@link #getInstance()} get an instance without a HopGui, tree or
tab folder to work
+ * with.
+ */
+ private boolean isInitialized() {
+ return hopGui != null;
+ }
+
@Override
public String getId() {
return "metadata-perspective";
@@ -250,6 +261,9 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
@GuiOsxKeyboardShortcut(command = true, shift = true, key = 'm', global =
true)
@Override
public void activate() {
+ if (!isInitialized()) {
+ return;
+ }
hopGui.setActivePerspective(this);
}
@@ -261,7 +275,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
@Override
public boolean isActive() {
- return hopGui.isActivePerspective(this);
+ return isInitialized() && hopGui.isActivePerspective(this);
}
@Override
@@ -736,6 +750,11 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
* at that position; otherwise it is appended at the end.
*/
public void addEditor(MetadataEditor<?> editor, int tabIndex) {
+ if (!isInitialized()) {
+ // There is no tab folder to add the editor to.
+ //
+ return;
+ }
// Create tab item
//
@@ -890,6 +909,9 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
}
public void setActiveEditor(MetadataEditor<?> editor) {
+ if (!isInitialized()) {
+ return;
+ }
for (CTabItem item : tabFolder.getItems()) {
if (item.getData().equals(editor)) {
tabFolder.setSelection(item);
@@ -904,7 +926,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
}
public MetadataEditor<?> getActiveEditor() {
- if (tabFolder.getSelectionIndex() < 0) {
+ if (!isInitialized() || tabFolder.getSelectionIndex() < 0) {
return null;
}
@@ -1997,7 +2019,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
public void updateEditor(MetadataEditor<?> editor) {
- if (editor == null) return;
+ if (editor == null || !isInitialized()) return;
// Update TabItem
//
@@ -2075,6 +2097,11 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
@GuiKeyboardShortcut(key = SWT.F5)
@GuiOsxKeyboardShortcut(key = SWT.F5)
public void refresh() {
+ if (!isInitialized()) {
+ // There is no tree to render the metadata into.
+ //
+ return;
+ }
reloadModel();
renderTree();
// Keep the overview catalog in sync when it is the visible panel (no
editor open).
@@ -2720,6 +2747,9 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
@Override
public boolean remove(IHopFileTypeHandler typeHandler) {
+ if (!isInitialized()) {
+ return false;
+ }
if (typeHandler instanceof MetadataEditor<?> editor &&
editor.isCloseable()) {
editors.remove(editor);
@@ -2754,6 +2784,9 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
@Override
public List<TabItemHandler> getItems() {
List<TabItemHandler> items = new ArrayList<>();
+ if (!isInitialized()) {
+ return items;
+ }
for (CTabItem tabItem : tabFolder.getItems()) {
for (MetadataEditor<?> editor : editors) {
if (tabItem.getData().equals(editor)) {
@@ -2847,6 +2880,11 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
/** Finds the tree node for a metadata type (by key), searching inside the
category headers. */
private TreeItem findTypeItem(String key) {
+ if (!isInitialized()) {
+ // There is no tree to navigate to.
+ //
+ return null;
+ }
for (TreeItem categoryItem : tree.getItems()) {
for (TreeItem typeItem : categoryItem.getItems()) {
if (TYPE.equals(typeItem.getData(KEY_TYPE)) &&
key.equals(typeItem.getData())) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiDescribedVariableSearchable.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiDescribedVariableSearchable.java
index 1c7e8181af..ee0ef8eabc 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiDescribedVariableSearchable.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiDescribedVariableSearchable.java
@@ -68,9 +68,13 @@ public class HopGuiDescribedVariableSearchable implements
ISearchable<DescribedV
String realConfigFilename =
HopGui.getInstance().getVariables().resolve(configFilename);
if (realConfigFilename == null) {
+ // The perspective is absent when it is switched off in
disabledGuiElements.xml.
+ //
ConfigurationPerspective perspective =
HopGui.getConfigurationPerspective();
- perspective.activate();
- perspective.showSystemVariablesTab();
+ if (perspective != null) {
+ perspective.activate();
+ perspective.showSystemVariablesTab();
+ }
} else {
if (new File(realConfigFilename).exists()) {
DescribedVariablesConfigFile configFile =
diff --git
a/ui/src/main/java/org/apache/hop/ui/www/service/WebServiceGuiPlugin.java
b/ui/src/main/java/org/apache/hop/ui/www/service/WebServiceGuiPlugin.java
index 001bc90709..6b2bc89532 100644
--- a/ui/src/main/java/org/apache/hop/ui/www/service/WebServiceGuiPlugin.java
+++ b/ui/src/main/java/org/apache/hop/ui/www/service/WebServiceGuiPlugin.java
@@ -132,13 +132,18 @@ public class WebServiceGuiPlugin {
// See if it's open in the metadata perspective...
//
+ // The perspective is absent when it is switched off in
disabledGuiElements.xml, and then
+ // nothing is open in it.
+ //
MetadataPerspective perspective =
(MetadataPerspective)
hopGui.getPerspectiveManager().findPerspective(MetadataPerspective.class);
String key = WebService.class.getAnnotation(HopMetadata.class).key();
WebServiceEditor editor =
- (WebServiceEditor) perspective.findEditor(key,
webService.getName());
- if (editor != null) {
+ perspective == null
+ ? null
+ : (WebServiceEditor) perspective.findEditor(key,
webService.getName());
+ if (perspective != null && editor != null) {
// We're going to change the current metadata and flag it as
changed...
//
webService = new WebService();
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/DisabledPerspectivesTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/DisabledPerspectivesTest.java
new file mode 100644
index 0000000000..a426959e42
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/DisabledPerspectivesTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import
org.apache.hop.ui.hopgui.perspective.configuration.ConfigurationPerspective;
+import org.apache.hop.ui.hopgui.perspective.metadata.MetadataPerspective;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Any perspective can be switched off with an exclusion for its plugin id in
+ * disabledGuiElements.xml. HopGui then skips it while loading the
perspectives, so initialize() is
+ * never called - but the singleton still exists, because the class is
instantiated to register the
+ * GUI elements it declares. Everything that reaches such a perspective
through getInstance() gets
+ * an instance with no HopGui, no tree and no tab folder.
+ *
+ * <p>These tests pin down that those instances stay inert instead of throwing
a
+ * NullPointerException. No display is needed: not calling initialize() is
exactly what being
+ * disabled amounts to.
+ *
+ * <p>The Execution perspective has its own test next to it; the git
perspectives are covered in the
+ * git plugin.
+ */
+class DisabledPerspectivesTest {
+
+ /**
+ * The metadata perspective is the one that hurts most: every metadata
editor, every "edit" button
+ * next to a metadata combo, and the audit log that restores tabs on startup
all reach for it.
+ */
+ @Test
+ void aDisabledMetadataPerspectiveStaysInert() {
+ MetadataPerspective perspective = new MetadataPerspective();
+
+ assertFalse(perspective.isActive());
+ assertDoesNotThrow(
+ () -> {
+ perspective.activate();
+ // HopGui#open restores the metadata tabs of the last session
through the audit log.
+ perspective.refresh();
+ perspective.updateEditor(null);
+ perspective.setActiveEditor(null);
+ });
+ assertNull(perspective.getActiveEditor());
+ assertTrue(perspective.getItems().isEmpty());
+ assertFalse(perspective.remove(null));
+ }
+
+ @Test
+ void aDisabledConfigurationPerspectiveStaysInert() {
+ ConfigurationPerspective perspective = new ConfigurationPerspective();
+
+ assertFalse(perspective.isActive());
+ assertDoesNotThrow(
+ () -> {
+ // The macOS Preferences menu item and the variable search results
both land here.
+ perspective.activate();
+ perspective.showSystemVariablesTab();
+ });
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveDisabledTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveDisabledTest.java
new file mode 100644
index 0000000000..acda548165
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionPerspectiveDisabledTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective.execution;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The Execution perspective can be switched off with an exclusion for its
plugin id
+ * ("150-HopExecutionPerspective") in disabledGuiElements.xml.
HopGui#loadPerspectives() then skips
+ * the perspective, so initialize() is never called on it. The singleton still
exists, because the
+ * class is instantiated to register the GUI elements it declares, which
leaves callers that reach
+ * it through getInstance() holding an instance without a HopGui or any
widgets.
+ *
+ * <p>These tests pin down that such an instance stays inert instead of
throwing a
+ * NullPointerException. No display is needed: not calling initialize() is
exactly what being
+ * disabled amounts to.
+ */
+class ExecutionPerspectiveDisabledTest {
+
+ /** A perspective as HopGui leaves it behind when it is disabled:
constructed, not initialized. */
+ private ExecutionPerspective disabledPerspective;
+
+ @BeforeEach
+ void createDisabledPerspective() {
+ disabledPerspective = new ExecutionPerspective();
+
+ // The constructor is what publishes the singleton, so every getInstance()
caller in the code
+ // base gets this uninitialized instance.
+ assertSame(disabledPerspective, ExecutionPerspective.getInstance());
+ }
+
+ @Test
+ void startingHopGuiDoesNotFail() {
+ // HopGui#open restores the state of the perspective in its async open
runnable.
+ assertDoesNotThrow(() -> disabledPerspective.restoreState());
+ }
+
+ @Test
+ void openingAProjectDoesNotFail() {
+ // ProjectsGuiPlugin#enableHopGuiProject restores the state and refreshes
when active.
+ assertDoesNotThrow(
+ () -> {
+ disabledPerspective.restoreState();
+ if (disabledPerspective.isActive()) {
+ disabledPerspective.refresh();
+ }
+ });
+ }
+
+ @Test
+ void closingAProjectDoesNotFail() {
+ // ProjectsGuiPlugin#enableHopGuiProject and HopGuiFileDelegate#fileExit
save the state.
+ assertDoesNotThrow(() -> disabledPerspective.saveState());
+ }
+
+ @Test
+ void theKeyboardShortcutDoesNotFail() {
+ // The Ctrl-Shift-I shortcut on activate() is registered even though the
perspective is not.
+ assertDoesNotThrow(() -> disabledPerspective.activate());
+ }
+
+ @Test
+ void aDisabledPerspectiveIsNeverActive() {
+ assertFalse(disabledPerspective.isActive());
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/testing/DisabledGuiWidgetsTestBase.java
b/ui/src/test/java/org/apache/hop/ui/testing/DisabledGuiWidgetsTestBase.java
new file mode 100644
index 0000000000..f304b1935e
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/testing/DisabledGuiWidgetsTestBase.java
@@ -0,0 +1,481 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.testing;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.DynamicTest.dynamicTest;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.apache.hop.core.gui.plugin.GuiElements;
+import org.apache.hop.core.gui.plugin.GuiRegistry;
+import org.apache.hop.core.gui.plugin.menu.GuiMenuItem;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarItem;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.GuiMenuWidgets;
+import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.apache.hop.ui.core.gui.IToolbarContainer;
+import org.apache.hop.ui.hopgui.HopGuiEnvironment;
+import org.apache.hop.ui.hopgui.ToolbarFacade;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTError;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Shell;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+
+/**
+ * Anything in Hop that carries an id can be switched off with an {@code
<exclusion>} for that id in
+ * {@code disabledGuiElements.xml}: a {@code @GuiWidgetElement}, a {@code
@GuiToolbarElement}, a
+ * {@code @GuiMenuElement}, and more. The filtering happens at registration
time, so the net effect
+ * is always the same - the element is simply absent from the registry and
never gets built. Code
+ * that later looks it up by id gets a {@code null} back, and any unguarded
dereference turns a
+ * supported configuration into a crash.
+ *
+ * <p>This harness makes that class of bugs testable. It discovers every
element registered on the
+ * classpath of the module under test and, for each one, rebuilds the thing
that holds it -
+ * composite, toolbar or menu - with exactly that element disabled, driving
the callbacks the
+ * framework invokes. One dynamic test per element, so a failure names the
element that cannot be
+ * disabled.
+ *
+ * <p>To cover a module, subclass this and narrow {@link #packagesUnderTest()}
to the module's own
+ * packages - hop-core, hop-engine and hop-ui ride along on every plugin's
classpath and are already
+ * covered by the hop-ui-rcp test.
+ *
+ * <p>What is <em>not</em> covered: an element that is only ever dereferenced
from an event handler
+ * (a tree selection listener, say) is not reached by building the widget, so
those call sites have
+ * to use the null-safe accessors ({@code GuiMenuWidgets.enableMenuItem(id,
enabled)} rather than
+ * {@code findMenuItem(id).setEnabled(enabled)}). Keyboard shortcuts, context
actions and config
+ * plugins are disabled by simply not being registered and are never looked up
by id, so there is
+ * nothing to break.
+ *
+ * <p>A container that cannot be built at all in a test JVM (no public no-arg
constructor, class not
+ * on the classpath, a widget that needs a live back end, ...) is <em>skipped,
with the reason</em>
+ * rather than failed: each is first built with nothing disabled, and only if
that baseline works do
+ * the disable cases have to hold. That keeps a failure here meaning one thing
- disabling this
+ * element broke something.
+ */
+public abstract class DisabledGuiWidgetsTestBase extends SwtBotTestBase {
+
+ /**
+ * Registers the {@code @GuiPlugin} classes so the elements we enumerate
below are in the
+ * registry. Runs after {@link SwtBotTestBase}'s own setup, which skips the
whole class when there
+ * is no display.
+ */
+ @BeforeAll
+ static void registerGuiPluginElements() throws Exception {
+ HopGuiEnvironment.init();
+ }
+
+ /** Per container: why it cannot be built at all here, or "" when it builds
fine. */
+ private final Map<String, String> baselineFailures = new HashMap<>();
+
+ /**
+ * The packages whose elements are tested, subpackages included. Empty - the
default - tests every
+ * element registered on the classpath. A plugin narrows this to its own
packages, because
+ * hop-core, hop-engine and hop-ui are on its classpath too and their
elements are already covered
+ * in hop-ui-rcp.
+ */
+ protected List<String> packagesUnderTest() {
+ return List.of();
+ }
+
+ /** One disable-able element: the class that owns it, the container it sits
in, and its id. */
+ private record Element(String owner, String container, String id) {}
+
+ @Test
+ void theModuleRegistersSomethingThatCanBeDisabled() {
+ assertFalse(
+ compositeElements().isEmpty() && toolbarElements().isEmpty() &&
menuElements().isEmpty(),
+ "Nothing that disabledGuiElements.xml can switch off is registered for
"
+ + (packagesUnderTest().isEmpty() ? "any package" :
packagesUnderTest())
+ + ", so the tests below would pass without testing anything");
+ }
+
+ @TestFactory
+ Stream<DynamicTest> disablingAWidgetMustNotBreakItsComposite() {
+ return casesFor(compositeElements(), "composite", this::buildComposite);
+ }
+
+ @TestFactory
+ Stream<DynamicTest> disablingAToolbarItemMustNotBreakItsToolbar() {
+ return casesFor(toolbarElements(), "toolbar", this::buildToolbar);
+ }
+
+ @TestFactory
+ Stream<DynamicTest> disablingAMenuItemMustNotBreakItsMenu() {
+ return casesFor(menuElements(), "menu", this::buildMenu);
+ }
+
+ /** Builds one dynamic test per element: disable it, rebuild what holds it,
nothing may break. */
+ private Stream<DynamicTest> casesFor(List<Element> elements, String kind,
Builder builder) {
+ return elements.stream()
+ .map(
+ element ->
+ dynamicTest(
+ simpleName(element.owner()) + " " + kind + " without " +
element.id(),
+ () -> disablingIsHarmless(element, builder)));
+ }
+
+ private void disablingIsHarmless(Element element, Builder builder) {
+ String baselineFailure =
+ baselineFailures.computeIfAbsent(
+ element.owner() + "#" + element.container(), key ->
baselineFailure(element, builder));
+
+ Assumptions.assumeTrue(
+ baselineFailure.isEmpty(),
+ () ->
+ element.container()
+ + " of "
+ + element.owner()
+ + " cannot be built in a test JVM even with all its elements
present, so disabling"
+ + " one of them proves nothing: "
+ + baselineFailure);
+
+ try {
+ builder.build(element.owner(), element.container(), element.id());
+ } catch (Exception | SWTError e) {
+ // Surefire reports a dynamic test as "[n]", so the failure itself has
to say which element it
+ // was: that name is the whole point of the test.
+ //
+ throw new AssertionError(
+ "Disabling '"
+ + element.id()
+ + "' of "
+ + element.owner()
+ + " breaks it. Add an exclusion for that id to
disabledGuiElements.xml and the GUI"
+ + " stops working.",
+ e);
+ }
+ }
+
+ /**
+ * Why this container cannot be built with all its elements present, or ""
when it builds fine.
+ */
+ private String baselineFailure(Element element, Builder builder) {
+ try {
+ builder.build(element.owner(), element.container(), null);
+ return "";
+ } catch (Exception | SWTError e) {
+ return e.toString();
+ }
+ }
+
+ //
----------------------------------------------------------------------------------------------
+ // Discovery: what an exclusion in disabledGuiElements.xml can remove
+ //
----------------------------------------------------------------------------------------------
+
+ /** {@code @GuiWidgetElement}: the class declaring them -> the parent id ->
the widget tree. */
+ private List<Element> compositeElements() {
+ List<Element> elements = new ArrayList<>();
+ GuiRegistry.getInstance()
+ .getDataElementsMap()
+ .forEach(
+ (className, perParent) -> {
+ if (isUnderTest(className)) {
+ perParent.forEach(
+ (parentId, tree) ->
+ collectWidgetIds(tree)
+ .forEach(id -> elements.add(new Element(className,
parentId, id))));
+ }
+ });
+ return elements;
+ }
+
+ /** {@code @GuiToolbarElement}: the toolbar root -> the items on it. */
+ private List<Element> toolbarElements() {
+ List<Element> elements = new ArrayList<>();
+ GuiRegistry.getInstance()
+ .getGuiToolbarMap()
+ .forEach(
+ (root, items) ->
+ items
+ .values()
+ .forEach(
+ item -> {
+ if (!item.isIgnored() &&
isUnderTest(item.getListenerClass())) {
+ elements.add(new Element(item.getListenerClass(),
root, item.getId()));
+ }
+ }));
+ return elements;
+ }
+
+ /** {@code @GuiMenuElement}: the menu root -> the items in it. */
+ private List<Element> menuElements() {
+ List<Element> elements = new ArrayList<>();
+ GuiRegistry.getInstance()
+ .getGuiMenuMap()
+ .forEach(
+ (root, items) ->
+ items
+ .values()
+ .forEach(
+ item -> {
+ if (isUnderTest(item.getListenerClassName())) {
+ elements.add(
+ new Element(item.getListenerClassName(), root,
item.getId()));
+ }
+ }));
+ return elements;
+ }
+
+ //
----------------------------------------------------------------------------------------------
+ // Builders: recreate what the application builds, with one element taken
out of the registry
+ //
----------------------------------------------------------------------------------------------
+
+ /** Builds a container with one element disabled, or with everything present
when id is null. */
+ @FunctionalInterface
+ private interface Builder {
+ void build(String owner, String container, String disabledId);
+ }
+
+ /**
+ * Recreates what the application does with a composite: build the widgets,
push the values of the
+ * source object into them, and let the source object - which is its own
listener, the way {@code
+ * ConfigPluginOptionsTab} and the database dialogs wire it up - react to
them.
+ */
+ private void buildComposite(String className, String parentId, String
disabledWidgetId) {
+ ensureDisplay();
+
+ GuiElements tree = GuiRegistry.getInstance().findGuiElements(className,
parentId);
+
+ // The element that holds the widget, and where in it the widget sits, so
it can go back exactly
+ // where it was. Null when we build the composite as-is.
+ //
+ GuiElements owner = disabledWidgetId == null ? null : findOwner(tree,
disabledWidgetId);
+ GuiElements disabled = owner == null ? null :
owner.findChild(disabledWidgetId);
+ int position = owner == null ? -1 : owner.getChildren().indexOf(disabled);
+
+ Shell shell = new Shell(display);
+ shell.setLayout(new FormLayout());
+ try {
+ if (owner != null) {
+ owner.getChildren().remove(position);
+ }
+
+ Object source = newSourceObject(className);
+ GuiCompositeWidgets widgets = new GuiCompositeWidgets(new Variables());
+ widgets.createCompositeWidgets(source, null, shell, parentId, null);
+ widgets.setWidgetsContents(source, shell, parentId);
+
+ if (source instanceof IGuiPluginCompositeWidgetsListener listener) {
+ widgets.setWidgetsListener(listener);
+ listener.widgetsCreated(widgets);
+ listener.widgetsPopulated(widgets);
+ // Every widget that is still there can be modified by the user, and
each of those fires the
+ // callback that tends to reach for the widget that is no longer there.
+ //
+ for (Map.Entry<String, Control> widget :
+ new ArrayList<>(widgets.getWidgetsMap().entrySet())) {
+ listener.widgetModified(widgets, widget.getValue(), widget.getKey());
+ }
+ listener.persistContents(widgets);
+ }
+ } finally {
+ // Put the widget back: the registry is a singleton shared with every
other case.
+ //
+ if (owner != null) {
+ owner.getChildren().add(position, disabled);
+ }
+ disposeQuietly(shell);
+ }
+ }
+
+ /**
+ * Builds the toolbar the way a perspective or dialog does, minus the
disabled item.
+ *
+ * <p>The owning object is registered first, exactly as the application does
it. That object is
+ * not decoration: Hop looks the gui plugin class up for every item, and a
combo item asks it for
+ * its values ({@code getZoomLevels()} and friends). So a toolbar whose
owner is a live UI object
+ * that cannot be constructed in a test JVM - HopGuiPipelineGraph,
TableView, the execution
+ * viewers - cannot be built the way the application builds it, and is
skipped with that as the
+ * reason rather than built half-way while Hop logs an error per item.
+ */
+ private void buildToolbar(String owner, String root, String disabledItemId) {
+ ensureDisplay();
+
+ Map<String, GuiToolbarItem> items =
GuiRegistry.getInstance().getGuiToolbarMap().get(root);
+ GuiToolbarItem disabled = disabledItemId == null ? null :
items.get(disabledItemId);
+
+ Shell shell = new Shell(display);
+ shell.setLayout(new FormLayout());
+ try {
+ if (disabled != null) {
+ items.remove(disabledItemId);
+ }
+
+ // Hop resolves the gui plugin class of every item that is left on the
toolbar - to ask it for
+ // an image, for its combo values - so all of them have to be
constructible here, not only the
+ // owner. Items can be contributed to a root by more than one class.
+ //
+ for (GuiToolbarItem item : items.values()) {
+ newSourceObject(item.getListenerClass());
+ }
+
+ IToolbarContainer container =
+ ToolbarFacade.createToolbarContainer(shell, SWT.WRAP | SWT.LEFT |
SWT.HORIZONTAL);
+ GuiToolbarWidgets widgets = new GuiToolbarWidgets();
+ widgets.registerGuiPluginObject(newSourceObject(owner));
+ widgets.createToolbarWidgets(container, root);
+ } finally {
+ if (disabled != null) {
+ items.put(disabledItemId, disabled);
+ }
+ disposeQuietly(shell);
+ }
+ }
+
+ /** Builds the menu the way HopGui and the perspectives do, minus the
disabled item. */
+ private void buildMenu(String owner, String root, String disabledItemId) {
+ ensureDisplay();
+
+ Map<String, GuiMenuItem> items =
GuiRegistry.getInstance().getGuiMenuMap().get(root);
+ GuiMenuItem disabled = disabledItemId == null ? null :
items.get(disabledItemId);
+
+ Shell shell = new Shell(display);
+ try {
+ if (disabled != null) {
+ items.remove(disabledItemId);
+ }
+
+ // Unlike the toolbar, building a menu never resolves the owning gui
plugin object - that only
+ // happens when an item is clicked - so there is nothing to register
here.
+ //
+ new GuiMenuWidgets().createMenuWidgets(root, shell, new Menu(shell,
SWT.POP_UP));
+ } finally {
+ if (disabled != null) {
+ items.put(disabledItemId, disabled);
+ }
+ disposeQuietly(shell);
+ }
+ }
+
+ private void disposeQuietly(Shell shell) {
+ shell.dispose();
+ while (display.readAndDispatch()) {
+ // flush the disposal before the next case builds its own shell
+ }
+ }
+
+ private Object newSourceObject(String className) {
+ try {
+ Class<?> sourceClass = Class.forName(className, true,
getClass().getClassLoader());
+
+ // The config plugins hand out an instance filled in from the
configuration, and that is the
+ // one the config dialog builds its widgets around. A bare constructor
would leave their
+ // Boolean fields null, and the composite blows up on unboxing before we
get to test anything.
+ //
+ Method getInstance = singletonAccessor(sourceClass);
+ if (getInstance != null) {
+ Object singleton = getInstance.invoke(null);
+ if (singleton != null) {
+ return singleton;
+ }
+ // The singleton only exists inside a running HopGui. Fall back to a
fresh instance, which
+ // is exactly what BaseGuiWidgets.findGuiPluginInstance does when it
finds nothing
+ // registered.
+ //
+ }
+ return sourceClass.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ // No usable constructor, or the class is not on this module's
classpath. The baseline build
+ // hits this first, so the composite ends up skipped rather than failed.
+ //
+ throw new CompositeBuildException(e);
+ }
+ }
+
+ /**
+ * The class's own {@code static getInstance()}, when it has one that hands
out an instance of
+ * itself - which is how the config dialog gets the object it builds the
widgets around.
+ */
+ private Method singletonAccessor(Class<?> sourceClass) {
+ try {
+ Method getInstance = sourceClass.getMethod("getInstance");
+ boolean handsOutItself =
+ Modifier.isStatic(getInstance.getModifiers())
+ && sourceClass.isAssignableFrom(getInstance.getReturnType());
+ return handsOutItself ? getInstance : null;
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+
+ /** The element that holds the widget, which is where it has to be removed
from (and restored). */
+ private GuiElements findOwner(GuiElements element, String widgetId) {
+ for (GuiElements child : element.getChildren()) {
+ if (widgetId.equals(child.getId())) {
+ return element;
+ }
+ GuiElements owner = findOwner(child, widgetId);
+ if (owner != null) {
+ return owner;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Every widget in the tree that an exclusion could remove, groups and
nested widgets included.
+ */
+ private List<String> collectWidgetIds(GuiElements element) {
+ List<String> ids = new ArrayList<>();
+ for (GuiElements child : element.getChildren()) {
+ if (child.getId() != null && !child.isIgnored()) {
+ ids.add(child.getId());
+ }
+ ids.addAll(collectWidgetIds(child));
+ }
+ return ids;
+ }
+
+ private String simpleName(String className) {
+ return className == null ? "?" :
className.substring(className.lastIndexOf('.') + 1);
+ }
+
+ private boolean isUnderTest(String className) {
+ if (className == null) {
+ return false;
+ }
+ List<String> packages = packagesUnderTest();
+ return packages.isEmpty()
+ || packages.stream().anyMatch(name -> className.startsWith(name +
"."));
+ }
+
+ /**
+ * A container is built through an unchecked call, so the reflective failure
of a source object
+ * that has no usable constructor has to travel as one.
+ */
+ private static class CompositeBuildException extends RuntimeException {
+ CompositeBuildException(Throwable cause) {
+ super(cause);
+ }
+ }
+}