mirror of
https://github.com/theotherp/nzbhydra2.git
synced 2026-02-06 11:17:18 +00:00
Add syncing with external tools
This commit is contained in:
parent
717e2f12ab
commit
9e4ceb6403
@ -2,7 +2,24 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read(//c/Users/strat/IdeaProjects/nzbhydra2/**)",
|
||||
"Bash(mvn:*)"
|
||||
"Bash(mvn:*)",
|
||||
"mcp__intellij__execute_run_configuration",
|
||||
"Bash(npm install)",
|
||||
"Bash(npx playwright install:*)",
|
||||
"Bash(npx playwright test:*)",
|
||||
"Bash(timeout 60s npx playwright test external-tools.spec.ts --grep \"should open external tool configuration modal with presets\")",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should open Sonarr preset configuration modal\")",
|
||||
"Bash(find:*)",
|
||||
"Bash(timeout 300s npx playwright test external-tools.spec.ts)",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should toggle sync on config change setting\")",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should navigate to External Tools configuration\")",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should trigger manual sync all\")",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should save external tool configuration\" --headed)",
|
||||
"mcp__intellij__get_run_configurations",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should save external tool configuration\")",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should edit existing external tool\")",
|
||||
"Bash(timeout 120s npx playwright test external-tools.spec.ts --grep \"should delete external tool\")",
|
||||
"Read(//c/Users/strat/IdeaProjects/**)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@ -102,6 +102,7 @@ public class BaseConfigHandler {
|
||||
baseConfig.setGenericStorage(newConfig.getGenericStorage());
|
||||
baseConfig.setNotificationConfig(newConfig.getNotificationConfig());
|
||||
baseConfig.setEmby(newConfig.getEmby());
|
||||
baseConfig.setExternalTools(newConfig.getExternalTools());
|
||||
|
||||
|
||||
if (fireConfigChangedEvent) {
|
||||
|
||||
@ -252,7 +252,7 @@ public class ExternalToolsSyncService {
|
||||
if (oldIndexer == null) {
|
||||
// New indexer
|
||||
changed.add(newIndexer.getName());
|
||||
} else if (!oldIndexer.equals(newIndexer)) {
|
||||
} else if (!IndexerConfig.isIndexerEquals(oldIndexer, newIndexer)) {
|
||||
// Modified indexer
|
||||
changed.add(newIndexer.getName());
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
package org.nzbhydra.externaltools;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.nzbhydra.config.ConfigProvider;
|
||||
import org.nzbhydra.config.ConfigReaderWriter;
|
||||
import org.nzbhydra.config.indexer.IndexerConfig;
|
||||
import org.nzbhydra.config.indexer.SearchModuleType;
|
||||
import org.nzbhydra.indexers.IndexerRepository;
|
||||
import org.nzbhydra.web.UrlCalculator;
|
||||
import org.nzbhydra.webaccess.WebAccess;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -17,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@ -35,6 +39,8 @@ public class ExternalToolsWeb {
|
||||
private final ConfigReaderWriter configReaderWriter = new ConfigReaderWriter();
|
||||
@Autowired
|
||||
private ExternalToolsSyncService externalToolsSyncService;
|
||||
@Autowired
|
||||
private WebAccess webAccess;
|
||||
|
||||
@Secured({"ROLE_ADMIN"})
|
||||
@RequestMapping(value = "/internalapi/externalTools/getDialogInfo", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@ -68,23 +74,39 @@ public class ExternalToolsWeb {
|
||||
@RequestMapping(value = "/internalapi/externalTools/testConnection", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ConnectionTestResult testConnection(@RequestBody AddRequest addRequest) {
|
||||
try {
|
||||
// Just test the connection without actually syncing
|
||||
boolean success = externalTools.addNzbhydraAsIndexer(buildTestRequest(addRequest));
|
||||
return new ConnectionTestResult(success, success ? "Connection successful" : "Connection failed");
|
||||
return testSimpleConnection(addRequest.getXdarrHost(), addRequest.getXdarrApiKey());
|
||||
} catch (Exception e) {
|
||||
return new ConnectionTestResult(false, "Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private AddRequest buildTestRequest(AddRequest request) {
|
||||
// Create a test request that only checks connection without modifying anything
|
||||
AddRequest testRequest = new AddRequest();
|
||||
testRequest.setExternalTool(request.getExternalTool());
|
||||
testRequest.setXdarrHost(request.getXdarrHost());
|
||||
testRequest.setXdarrApiKey(request.getXdarrApiKey());
|
||||
testRequest.setAddType(AddRequest.AddType.DELETE_ONLY); // Only delete, don't add
|
||||
testRequest.setNzbhydraName("TEST_CONNECTION_" + System.currentTimeMillis());
|
||||
return testRequest;
|
||||
private ConnectionTestResult testSimpleConnection(String host, String apiKey) {
|
||||
try {
|
||||
// Remove trailing slash if present
|
||||
String cleanHost = host.endsWith("/") ? host.substring(0, host.length() - 1) : host;
|
||||
|
||||
// Build the API URL: http://host/api?apikey=key
|
||||
String apiUrl = cleanHost + "/api?apikey=" + apiKey;
|
||||
|
||||
logger.debug("Testing connection to: {}", apiUrl);
|
||||
|
||||
// Make the API call
|
||||
String response = webAccess.callUrl(URI.create(apiUrl).toString());
|
||||
|
||||
// Parse response as JSON and check for "current" key
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode jsonNode = mapper.readTree(response);
|
||||
|
||||
if (jsonNode.has("current")) {
|
||||
return new ConnectionTestResult(true, "Connection successful");
|
||||
} else {
|
||||
return new ConnectionTestResult(false, "Invalid response: missing 'current' field");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.debug("Connection test failed", e);
|
||||
return new ConnectionTestResult(false, "Connection failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConnectionTestResult {
|
||||
|
||||
@ -177,6 +177,32 @@ angular.module('nzbhydraApp').config(["$stateProvider", "$urlRouterProvider", "$
|
||||
}
|
||||
}
|
||||
})
|
||||
.state("root.config.externalTools", {
|
||||
url: "/externalTools",
|
||||
views: {
|
||||
'container@': {
|
||||
templateUrl: "static/html/states/config.html",
|
||||
controller: "ConfigController",
|
||||
resolve: {
|
||||
loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {
|
||||
return loginRequired($q, $timeout, $state, HydraAuthService, "admin")
|
||||
}],
|
||||
config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
|
||||
return ConfigService.get();
|
||||
}],
|
||||
safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
|
||||
return ConfigService.getSafe();
|
||||
}],
|
||||
activeTab: [function () {
|
||||
return 5;
|
||||
}],
|
||||
$title: ["$stateParams", function ($stateParams) {
|
||||
return "Config (External Tools)"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.state("root.config.indexers", {
|
||||
url: "/indexers",
|
||||
views: {
|
||||
@ -194,7 +220,7 @@ angular.module('nzbhydraApp').config(["$stateProvider", "$urlRouterProvider", "$
|
||||
return ConfigService.getSafe();
|
||||
}],
|
||||
activeTab: [function () {
|
||||
return 5;
|
||||
return 6;
|
||||
}],
|
||||
$title: ["$stateParams", function ($stateParams) {
|
||||
return "Config (Indexers)"
|
||||
@ -220,7 +246,7 @@ angular.module('nzbhydraApp').config(["$stateProvider", "$urlRouterProvider", "$
|
||||
return ConfigService.getSafe();
|
||||
}],
|
||||
activeTab: [function () {
|
||||
return 6;
|
||||
return 7;
|
||||
}],
|
||||
$title: ["$stateParams", function ($stateParams) {
|
||||
return "Config (Notifications)"
|
||||
@ -5036,7 +5062,7 @@ angular
|
||||
}
|
||||
];
|
||||
|
||||
function _showBox(model, parentModel, isInitial, callback) {
|
||||
function _showBox(model, parentModel, isInitial, form, callback) {
|
||||
var modalInstance = $uibModal.open({
|
||||
templateUrl: 'static/html/config/external-tool-config-box.html',
|
||||
controller: 'ExternalToolConfigBoxInstanceController',
|
||||
@ -5063,7 +5089,7 @@ angular
|
||||
|
||||
|
||||
modalInstance.result.then(function (returnedModel) {
|
||||
$scope.form.$setDirty(true);
|
||||
form.$setDirty(true);
|
||||
if (angular.isDefined(callback)) {
|
||||
callback(true, returnedModel);
|
||||
}
|
||||
@ -5075,7 +5101,7 @@ angular
|
||||
}
|
||||
|
||||
function showBox(model, parentModel) {
|
||||
$scope._showBox(model, parentModel, false)
|
||||
$scope._showBox(model, parentModel, false, $scope.form)
|
||||
}
|
||||
|
||||
$scope.syncAll = function () {
|
||||
@ -5108,7 +5134,8 @@ angular
|
||||
enableInteractiveSearch: true,
|
||||
useHydraPriorities: true,
|
||||
priority: 25,
|
||||
nzbhydraName: "NZBHydra2"
|
||||
nzbhydraName: "NZBHydra2",
|
||||
nzbhydraHost: "http://host.docker.internal:5076"
|
||||
});
|
||||
if (angular.isDefined(preset)) {
|
||||
_.extend(model, preset);
|
||||
@ -5116,7 +5143,7 @@ angular
|
||||
|
||||
$scope.isInitial = true;
|
||||
|
||||
$scope._showBox(model, entriesCollection, true, function (isSubmitted, returnedModel) {
|
||||
$scope._showBox(model, entriesCollection, true, $scope.form, function (isSubmitted, returnedModel) {
|
||||
if (isSubmitted) {
|
||||
entriesCollection.push(angular.isDefined(returnedModel) ? returnedModel : model);
|
||||
}
|
||||
@ -5254,6 +5281,17 @@ angular
|
||||
}
|
||||
});
|
||||
|
||||
fieldset.push({
|
||||
key: 'nzbhydraHost',
|
||||
type: 'horizontalInput',
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'NZBHydra Host',
|
||||
help: 'NZBHydra URL that the external tool can reach (use host.docker.internal for Docker containers)',
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
fieldset.push({
|
||||
key: 'configureForUsenet',
|
||||
type: 'horizontalSwitch',
|
||||
@ -5473,7 +5511,10 @@ angular.module('nzbhydraApp').controller('ExternalToolConfigBoxInstanceControlle
|
||||
$scope.obSubmit = function () {
|
||||
if ($scope.form.$valid) {
|
||||
checkConnection().then(function () {
|
||||
$uibModalInstance.close($scope.model);
|
||||
// When adding/editing a specific external tool, block and show results
|
||||
syncToExternalTool().then(function () {
|
||||
$uibModalInstance.close($scope.model);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
growl.error("Config invalid. Please check your settings.");
|
||||
@ -5574,6 +5615,54 @@ angular.module('nzbhydraApp').controller('ExternalToolConfigBoxInstanceControlle
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function syncToExternalTool() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
$scope.spinnerActive = true;
|
||||
blockUI.start("Configuring NZBHydra in " + model.type + "...");
|
||||
|
||||
var syncRequest = {
|
||||
externalTool: model.type === 'SONARR' ? 'Sonarr' :
|
||||
model.type === 'RADARR' ? 'Radarr' :
|
||||
model.type === 'LIDARR' ? 'Lidarr' : 'Readarr',
|
||||
xdarrHost: model.host,
|
||||
xdarrApiKey: model.apiKey,
|
||||
addType: model.syncType === 'SINGLE' ? 'SINGLE' : 'PER_INDEXER',
|
||||
nzbhydraName: model.nzbhydraName,
|
||||
nzbhydraHost: model.nzbhydraHost,
|
||||
configureForUsenet: model.configureForUsenet,
|
||||
configureForTorrents: model.configureForTorrents,
|
||||
enableRss: model.enableRss,
|
||||
enableAutomaticSearch: model.enableAutomaticSearch,
|
||||
enableInteractiveSearch: model.enableInteractiveSearch,
|
||||
enableCategories: true,
|
||||
categories: model.categories || '',
|
||||
additionalParameters: model.additionalParameters
|
||||
};
|
||||
|
||||
$http.post("internalapi/externalTools/configure", syncRequest).then(
|
||||
function (response) {
|
||||
blockUI.reset();
|
||||
$scope.spinnerActive = false;
|
||||
if (response.data === true) {
|
||||
growl.success("Successfully configured NZBHydra in " + model.type);
|
||||
deferred.resolve();
|
||||
} else {
|
||||
growl.error("Failed to configure NZBHydra in " + model.type);
|
||||
deferred.reject();
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
blockUI.reset();
|
||||
$scope.spinnerActive = false;
|
||||
growl.error("Error configuring NZBHydra in " + model.type + ": " + (error.data ? error.data : "Unknown error"));
|
||||
deferred.reject();
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
$scope.$on("modal.closing", function (targetScope, reason) {
|
||||
if (reason === "backdrop click") {
|
||||
$scope.reset($scope);
|
||||
@ -9520,6 +9609,14 @@ function ConfigController($scope, $http, activeTab, ConfigService, config, Downl
|
||||
fields: $scope.fields.downloading,
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
state: 'root.config.externalTools',
|
||||
name: 'External Tools',
|
||||
model: ConfigModel.externalTools,
|
||||
fields: $scope.fields.externalTools,
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
state: 'root.config.indexers',
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
39
core/ui-src/html/config/external-tool-config-box.html
Normal file
39
core/ui-src/html/config/external-tool-config-box.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
~ (C) Copyright 2023 TheOtherP (theotherp@posteo.net)
|
||||
~
|
||||
~ Licensed 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.
|
||||
-->
|
||||
|
||||
<span class="config-box">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">External Tool Configuration</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<formly-form fields="fields"
|
||||
model="model"
|
||||
form="form"
|
||||
options="options"
|
||||
>
|
||||
</formly-form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger pull-left" ng-click="deleteEntry()" ng-if="!isInitial">Delete</button>
|
||||
<button class="btn btn-warning" ng-click="reset()">Reset</button>
|
||||
<button class="btn btn-info" ng-click="testConnection()" ng-if="model.host && model.apiKey">Test connection</button>
|
||||
<button class="btn btn-default" ng-click="cancel()">Cancel</button>
|
||||
<button class="btn btn-success has-spinner" ng-class="{'active': spinnerActive}" ng-click="obSubmit()">
|
||||
<span class="spinner"><i class="fa fa-refresh fa-spin"></i></span>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
67
core/ui-src/html/config/external-tool-config.html
Normal file
67
core/ui-src/html/config/external-tool-config.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!--
|
||||
~ (C) Copyright 2023 TheOtherP (theotherp@posteo.net)
|
||||
~
|
||||
~ Licensed 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.
|
||||
-->
|
||||
|
||||
<div class="row" style="margin-top: 20px;">
|
||||
<div class="col-md-12">
|
||||
<button type="button" class="btn btn-info" ng-click="syncAll()">
|
||||
<span class="glyphicon glyphicon-refresh" style="margin-right: 10px"></span>Sync All Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 30px;">
|
||||
<div class="btn-group" style="margin-bottom: 30px; margin-left: auto; margin-right: auto; float:none !important;">
|
||||
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-plus" style="margin-right: 10px"></span>Add external tool
|
||||
<span class="caret"></span>
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="preset in presets">
|
||||
<a ng-click="addEntry(model.externalTools, preset)">{{ preset.name }}</a>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>
|
||||
<a ng-click="addEntry(model.externalTools)">Custom</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="model.externalTools.length === 0" class="row">
|
||||
<div class="col-md-12 text-center" style="margin-top: 30px;">
|
||||
<h4>No external tools configured</h4>
|
||||
<p>Use the "Add external tool" button above to configure Sonarr, Radarr, Lidarr, or Readarr instances.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="entry in model.externalTools | orderBy: ['name'] track by $index">
|
||||
<div class="row">
|
||||
<div style="margin-left: auto; margin-right: auto; float:none !important;">
|
||||
<div style="margin-bottom: 30px;">
|
||||
<form class="form-inline">
|
||||
<button ng-click="showBox(entry, model.externalTools)" class="btn btn-secondary indexer-button indexer-input btn-default" style="margin-right: 10px;">
|
||||
{{ entry.name }}
|
||||
</button>
|
||||
<span style="margin-right: 10px;">{{ entry.host }}</span>
|
||||
<button ng-click="model.externalTools.splice($index, 1)" class="btn btn-danger btn-xs">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -16,24 +16,6 @@
|
||||
<input type="checkbox" ng-model="showAdvanced" class="ng-untouched ng-valid ng-not-empty ng-dirty" aria-invalid="false"></div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn config-button btn-default" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
uib-tooltip="Allows you to automatically configure NZBHydra2 as an indexer in an external tool."
|
||||
tooltip-placement="top"
|
||||
tooltip-trigger="mouseenter"
|
||||
>Configure NZBHydra in...
|
||||
<span class="caret"></span>
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li>
|
||||
<a ng-click="configureIn('Lidarr')">Lidarr</a>
|
||||
<a ng-click="configureIn('Sonarr')">Sonarr</a>
|
||||
<a ng-click="configureIn('Radarr')">Radarr</a>
|
||||
<a ng-click="configureIn('Readarr')">Readarr</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button ng-click="apiHelp()" class="btn config-button config-api-button btn-default">API?</button>
|
||||
<button ng-click="submit()" class="btn config-button" ng-class="{'btn-info': isSavingNeeded(), 'pulse2': isSavingNeeded(), 'btn-success': !isSavingNeeded()}">
|
||||
|
||||
@ -232,6 +232,14 @@ function ConfigController($scope, $http, activeTab, ConfigService, config, Downl
|
||||
fields: $scope.fields.downloading,
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
state: 'root.config.externalTools',
|
||||
name: 'External Tools',
|
||||
model: ConfigModel.externalTools,
|
||||
fields: $scope.fields.externalTools,
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
state: 'root.config.indexers',
|
||||
|
||||
@ -56,7 +56,7 @@ angular
|
||||
}
|
||||
];
|
||||
|
||||
function _showBox(model, parentModel, isInitial, callback) {
|
||||
function _showBox(model, parentModel, isInitial, form, callback) {
|
||||
var modalInstance = $uibModal.open({
|
||||
templateUrl: 'static/html/config/external-tool-config-box.html',
|
||||
controller: 'ExternalToolConfigBoxInstanceController',
|
||||
@ -83,7 +83,7 @@ angular
|
||||
|
||||
|
||||
modalInstance.result.then(function (returnedModel) {
|
||||
$scope.form.$setDirty(true);
|
||||
form.$setDirty(true);
|
||||
if (angular.isDefined(callback)) {
|
||||
callback(true, returnedModel);
|
||||
}
|
||||
@ -95,7 +95,7 @@ angular
|
||||
}
|
||||
|
||||
function showBox(model, parentModel) {
|
||||
$scope._showBox(model, parentModel, false)
|
||||
$scope._showBox(model, parentModel, false, $scope.form)
|
||||
}
|
||||
|
||||
$scope.syncAll = function () {
|
||||
@ -128,7 +128,8 @@ angular
|
||||
enableInteractiveSearch: true,
|
||||
useHydraPriorities: true,
|
||||
priority: 25,
|
||||
nzbhydraName: "NZBHydra2"
|
||||
nzbhydraName: "NZBHydra2",
|
||||
nzbhydraHost: "http://host.docker.internal:5076"
|
||||
});
|
||||
if (angular.isDefined(preset)) {
|
||||
_.extend(model, preset);
|
||||
@ -136,7 +137,7 @@ angular
|
||||
|
||||
$scope.isInitial = true;
|
||||
|
||||
$scope._showBox(model, entriesCollection, true, function (isSubmitted, returnedModel) {
|
||||
$scope._showBox(model, entriesCollection, true, $scope.form, function (isSubmitted, returnedModel) {
|
||||
if (isSubmitted) {
|
||||
entriesCollection.push(angular.isDefined(returnedModel) ? returnedModel : model);
|
||||
}
|
||||
@ -274,6 +275,17 @@ angular
|
||||
}
|
||||
});
|
||||
|
||||
fieldset.push({
|
||||
key: 'nzbhydraHost',
|
||||
type: 'horizontalInput',
|
||||
templateOptions: {
|
||||
type: 'text',
|
||||
label: 'NZBHydra Host',
|
||||
help: 'NZBHydra URL that the external tool can reach (use host.docker.internal for Docker containers)',
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
fieldset.push({
|
||||
key: 'configureForUsenet',
|
||||
type: 'horizontalSwitch',
|
||||
@ -493,7 +505,10 @@ angular.module('nzbhydraApp').controller('ExternalToolConfigBoxInstanceControlle
|
||||
$scope.obSubmit = function () {
|
||||
if ($scope.form.$valid) {
|
||||
checkConnection().then(function () {
|
||||
$uibModalInstance.close($scope.model);
|
||||
// When adding/editing a specific external tool, block and show results
|
||||
syncToExternalTool().then(function () {
|
||||
$uibModalInstance.close($scope.model);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
growl.error("Config invalid. Please check your settings.");
|
||||
@ -594,6 +609,54 @@ angular.module('nzbhydraApp').controller('ExternalToolConfigBoxInstanceControlle
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function syncToExternalTool() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
$scope.spinnerActive = true;
|
||||
blockUI.start("Configuring NZBHydra in " + model.type + "...");
|
||||
|
||||
var syncRequest = {
|
||||
externalTool: model.type === 'SONARR' ? 'Sonarr' :
|
||||
model.type === 'RADARR' ? 'Radarr' :
|
||||
model.type === 'LIDARR' ? 'Lidarr' : 'Readarr',
|
||||
xdarrHost: model.host,
|
||||
xdarrApiKey: model.apiKey,
|
||||
addType: model.syncType === 'SINGLE' ? 'SINGLE' : 'PER_INDEXER',
|
||||
nzbhydraName: model.nzbhydraName,
|
||||
nzbhydraHost: model.nzbhydraHost,
|
||||
configureForUsenet: model.configureForUsenet,
|
||||
configureForTorrents: model.configureForTorrents,
|
||||
enableRss: model.enableRss,
|
||||
enableAutomaticSearch: model.enableAutomaticSearch,
|
||||
enableInteractiveSearch: model.enableInteractiveSearch,
|
||||
enableCategories: true,
|
||||
categories: model.categories || '',
|
||||
additionalParameters: model.additionalParameters
|
||||
};
|
||||
|
||||
$http.post("internalapi/externalTools/configure", syncRequest).then(
|
||||
function (response) {
|
||||
blockUI.reset();
|
||||
$scope.spinnerActive = false;
|
||||
if (response.data === true) {
|
||||
growl.success("Successfully configured NZBHydra in " + model.type);
|
||||
deferred.resolve();
|
||||
} else {
|
||||
growl.error("Failed to configure NZBHydra in " + model.type);
|
||||
deferred.reject();
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
blockUI.reset();
|
||||
$scope.spinnerActive = false;
|
||||
growl.error("Error configuring NZBHydra in " + model.type + ": " + (error.data ? error.data : "Unknown error"));
|
||||
deferred.reject();
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
$scope.$on("modal.closing", function (targetScope, reason) {
|
||||
if (reason === "backdrop click") {
|
||||
$scope.reset($scope);
|
||||
|
||||
@ -177,6 +177,32 @@ angular.module('nzbhydraApp').config(function ($stateProvider, $urlRouterProvide
|
||||
}
|
||||
}
|
||||
})
|
||||
.state("root.config.externalTools", {
|
||||
url: "/externalTools",
|
||||
views: {
|
||||
'container@': {
|
||||
templateUrl: "static/html/states/config.html",
|
||||
controller: "ConfigController",
|
||||
resolve: {
|
||||
loginRequired: ['$q', '$timeout', '$state', 'HydraAuthService', function ($q, $timeout, $state, HydraAuthService) {
|
||||
return loginRequired($q, $timeout, $state, HydraAuthService, "admin")
|
||||
}],
|
||||
config: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
|
||||
return ConfigService.get();
|
||||
}],
|
||||
safeConfig: ['loginRequired', 'ConfigService', function (loginRequired, ConfigService) {
|
||||
return ConfigService.getSafe();
|
||||
}],
|
||||
activeTab: [function () {
|
||||
return 5;
|
||||
}],
|
||||
$title: function ($stateParams) {
|
||||
return "Config (External Tools)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.state("root.config.indexers", {
|
||||
url: "/indexers",
|
||||
views: {
|
||||
@ -194,7 +220,7 @@ angular.module('nzbhydraApp').config(function ($stateProvider, $urlRouterProvide
|
||||
return ConfigService.getSafe();
|
||||
}],
|
||||
activeTab: [function () {
|
||||
return 5;
|
||||
return 6;
|
||||
}],
|
||||
$title: function ($stateParams) {
|
||||
return "Config (Indexers)"
|
||||
@ -220,7 +246,7 @@ angular.module('nzbhydraApp').config(function ($stateProvider, $urlRouterProvide
|
||||
return ConfigService.getSafe();
|
||||
}],
|
||||
activeTab: [function () {
|
||||
return 6;
|
||||
return 7;
|
||||
}],
|
||||
$title: function ($stateParams) {
|
||||
return "Config (Notifications)"
|
||||
|
||||
@ -48,6 +48,7 @@ public class ExternalToolConfig {
|
||||
private boolean enabled = true;
|
||||
private SyncType syncType = AddRequest.AddType.SINGLE.name().equals("SINGLE") ? SyncType.SINGLE : SyncType.PER_INDEXER;
|
||||
private String nzbhydraName = "NZBHydra2";
|
||||
private String nzbhydraHost = "http://host.docker.internal:5076";
|
||||
|
||||
// Sync settings
|
||||
private boolean configureForUsenet = true;
|
||||
|
||||
3
tests/.gitignore
vendored
3
tests/.gitignore
vendored
@ -27,3 +27,6 @@ nbdist/
|
||||
logs/
|
||||
cacerts
|
||||
system/data
|
||||
system/node_modules
|
||||
system/playwright-report
|
||||
system/test-results
|
||||
96
tests/system/package-lock.json
generated
Normal file
96
tests/system/package-lock.json
generated
Normal file
@ -0,0 +1,96 @@
|
||||
{
|
||||
"name": "nzbhydra2-system-tests",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nzbhydra2-system-tests",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@types/node": "^20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
|
||||
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.55.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
|
||||
"integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
|
||||
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.55.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
|
||||
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
tests/system/package.json
Normal file
15
tests/system/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "nzbhydra2-system-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "System tests for NZBHydra2",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:headed": "playwright test --headed",
|
||||
"test:ui": "playwright test --ui",
|
||||
"playwright:install": "playwright install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@types/node": "^20.0.0"
|
||||
}
|
||||
}
|
||||
27
tests/system/playwright.config.ts
Normal file
27
tests/system/playwright.config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {defineConfig, devices} from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [["html", {open: "never", outputFolder: "playwright-report"}], ["list"]],
|
||||
use: {
|
||||
baseURL: "http://127.0.0.1:5076",
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {...devices["Desktop Chrome"]},
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: "echo \"NZBHydra should be running on port 5076\"",
|
||||
url: "http://127.0.0.1:5076",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
265
tests/system/tests/external-tools.spec.ts
Normal file
265
tests/system/tests/external-tools.spec.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import {expect, test} from "@playwright/test";
|
||||
|
||||
test.describe("External Tools Configuration", () => {
|
||||
test.beforeEach(async ({page}) => {
|
||||
// Navigate to the config page
|
||||
await page.goto("/");
|
||||
// Wait for the page to load
|
||||
await page.waitForLoadState("networkidle");
|
||||
});
|
||||
|
||||
test("should display External Tools tab in configuration", async ({page}) => {
|
||||
// Navigate to config page
|
||||
await page.click("a[href=\"/config/main\"]");
|
||||
await page.waitForSelector(".nav-tabs");
|
||||
|
||||
// Check that External Tools tab exists
|
||||
await expect(page.locator(".nav-tabs").locator("text=External Tools")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should navigate to External Tools configuration", async ({page}) => {
|
||||
// Navigate to External Tools config
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check that the sync settings are visible
|
||||
await expect(page.locator(".bootstrap-switch-id-formly_1_horizontalSwitch_syncOnConfigChange_0")).toBeVisible();
|
||||
|
||||
// Check that the "Add external tool" button is present
|
||||
await expect(page.locator("button:has-text(\"Add external tool\")")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show empty state when no external tools configured", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector(".form-horizontal");
|
||||
|
||||
// Check that empty state message is shown
|
||||
await expect(page.locator("text=No external tools configured")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open external tool configuration modal with presets", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Add external tool\")");
|
||||
|
||||
// Click the dropdown button
|
||||
await page.click("button:has-text(\"Add external tool\")");
|
||||
|
||||
// Check that preset options are available in the dropdown menu
|
||||
await expect(page.locator(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Sonarr\")")).toBeVisible();
|
||||
await expect(page.locator(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Radarr\")")).toBeVisible();
|
||||
await expect(page.locator(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Lidarr\")")).toBeVisible();
|
||||
await expect(page.locator(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Readarr\")")).toBeVisible();
|
||||
await expect(page.locator(".dropdown-menu a[ng-click=\"addEntry(model.externalTools)\"]:has-text(\"Custom\")")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open Sonarr preset configuration modal", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Add external tool\")");
|
||||
|
||||
// Click dropdown and select Sonarr
|
||||
await page.click("button:has-text(\"Add external tool\")");
|
||||
await page.click(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Sonarr\")");
|
||||
|
||||
// Wait for modal to open
|
||||
await page.waitForSelector(".modal-title:has-text(\"External Tool Configuration\")");
|
||||
|
||||
// Check that the modal is opened with Sonarr preset values
|
||||
await expect(page.locator(".modal-title")).toContainText("External Tool Configuration");
|
||||
await expect(page.locator("input[id*=\"name\"]")).toHaveValue("Sonarr");
|
||||
await expect(page.locator("input[id*=\"host\"]")).toHaveValue("http://localhost:8989");
|
||||
await expect(page.locator("input[id*=\"categories\"]")).toHaveValue("5030,5040");
|
||||
|
||||
// Check that switches are in correct state
|
||||
await expect(page.locator("input[id*=\"enabled\"]")).toBeChecked();
|
||||
await expect(page.locator("input[id*=\"configureForUsenet\"]")).toBeChecked();
|
||||
await expect(page.locator("input[id*=\"enableRss\"]")).toBeChecked();
|
||||
await expect(page.locator("input[id*=\"enableAutomaticSearch\"]")).toBeChecked();
|
||||
await expect(page.locator("input[id*=\"enableInteractiveSearch\"]")).toBeChecked();
|
||||
|
||||
// Close modal
|
||||
await page.click("button:has-text(\"Cancel\")");
|
||||
});
|
||||
|
||||
test("should validate required fields in external tool configuration", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Add external tool\")");
|
||||
|
||||
// Open custom configuration
|
||||
await page.click("button:has-text(\"Add external tool\")");
|
||||
await page.click("a:has-text(\"Custom\")");
|
||||
|
||||
// Wait for modal
|
||||
await page.waitForSelector(".modal-title:has-text(\"External Tool Configuration\")");
|
||||
|
||||
// Clear required fields
|
||||
await page.fill("input[id*=\"name\"]", "");
|
||||
await page.fill("input[id*=\"host\"]", "");
|
||||
|
||||
// Try to submit
|
||||
await page.click("button:has-text(\"OK\")");
|
||||
|
||||
// Check that validation errors are shown
|
||||
await expect(page.locator(".has-error").first()).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.click("button:has-text(\"Cancel\")");
|
||||
});
|
||||
|
||||
test("should save external tool configuration", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Add external tool\")");
|
||||
|
||||
// Open Radarr preset
|
||||
await page.click("button:has-text(\"Add external tool\")");
|
||||
await page.click(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Radarr\")");
|
||||
|
||||
// Wait for modal
|
||||
await page.waitForSelector(".modal-title:has-text(\"External Tool Configuration\")");
|
||||
|
||||
// Fill in API key
|
||||
await page.fill("input[id*=\"apiKey\"]", "766c3461d6fe44cf83cea3e3c16b5428");
|
||||
|
||||
// Submit form
|
||||
await page.click("button:has-text(\"OK\")");
|
||||
|
||||
// Wait for modal to close and tool to appear in list
|
||||
await page.waitForSelector(".btn:has-text(\"Radarr (RADARR)\")");
|
||||
|
||||
// Wait for the form to stabilize before saving
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Save the configuration to persist the changes
|
||||
await page.click("button:has-text(\"Save\")", {force: true});
|
||||
|
||||
// Check that the tool appears in the list
|
||||
await expect(page.locator(".btn:has-text(\"Radarr (RADARR)\")")).toBeVisible();
|
||||
await expect(page.locator("text=http://localhost:7878")).toBeVisible();
|
||||
|
||||
// Check that delete button is present
|
||||
await expect(page.locator(".btn-danger .glyphicon-remove")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should test connection to external tool", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Add external tool\")");
|
||||
|
||||
// Open Sonarr preset
|
||||
await page.click("button:has-text(\"Add external tool\")");
|
||||
await page.click(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Sonarr\")");
|
||||
|
||||
// Wait for modal
|
||||
await page.waitForSelector(".modal-title:has-text(\"External Tool Configuration\")");
|
||||
|
||||
// Fill in API key
|
||||
await page.fill("input[id*=\"apiKey\"]", "52a631c9cab346bca59c32bfffdd2669");
|
||||
|
||||
// Click test connection button
|
||||
await page.click("button:has-text(\"Test connection\")");
|
||||
|
||||
// Wait for response (should show error since we're not actually running Sonarr)
|
||||
await page.waitForSelector(".growl-message", {timeout: 10000});
|
||||
|
||||
// Close modal
|
||||
await page.click("button:has-text(\"Cancel\")");
|
||||
});
|
||||
|
||||
test("should trigger manual sync all", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Sync All Now\")");
|
||||
|
||||
// Click sync all button
|
||||
await page.click("button:has-text(\"Sync All Now\")");
|
||||
|
||||
// Wait for notification (should show some message about sync)
|
||||
await page.waitForSelector(".growl-message", {timeout: 5000});
|
||||
|
||||
// Should show a message about syncing (even if no tools configured)
|
||||
await expect(page.locator(".growl-message").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("should toggle sync on config change setting", async ({page}) => {
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector(".bootstrap-switch-id-formly_1_horizontalSwitch_syncOnConfigChange_0");
|
||||
|
||||
// Get current state from the hidden checkbox
|
||||
const isChecked = await page.locator("input#formly_1_horizontalSwitch_syncOnConfigChange_0").isChecked();
|
||||
|
||||
// Toggle by clicking the bootstrap switch wrapper
|
||||
await page.click(".bootstrap-switch-id-formly_1_horizontalSwitch_syncOnConfigChange_0");
|
||||
|
||||
// Verify state changed
|
||||
const newState = await page.locator("input#formly_1_horizontalSwitch_syncOnConfigChange_0").isChecked();
|
||||
expect(newState).toBe(!isChecked);
|
||||
});
|
||||
|
||||
test("should edit existing external tool", async ({page}) => {
|
||||
// First add a tool
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Add external tool\")");
|
||||
|
||||
await page.click("button:has-text(\"Add external tool\")");
|
||||
await page.click(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Radarr\")");
|
||||
await page.waitForSelector(".modal-title:has-text(\"External Tool Configuration\")");
|
||||
await page.fill("input[id*=\"apiKey\"]", "766c3461d6fe44cf83cea3e3c16b5428");
|
||||
await page.click("button:has-text(\"OK\")");
|
||||
|
||||
// Wait for tool to appear
|
||||
await page.waitForSelector(".btn:has-text(\"Radarr (RADARR)\")");
|
||||
|
||||
// Save the configuration first
|
||||
await page.click("button:has-text(\"Save\")", {force: true});
|
||||
|
||||
// Click on the tool to edit it
|
||||
await page.click(".btn:has-text(\"Radarr (RADARR)\")");
|
||||
|
||||
// Wait for modal to open in edit mode
|
||||
await page.waitForSelector(".modal-title:has-text(\"External Tool Configuration\")");
|
||||
|
||||
// Check that delete button is present (indicates edit mode)
|
||||
await expect(page.locator("button:has-text(\"Delete\")")).toBeVisible();
|
||||
|
||||
// Modify the name
|
||||
await page.fill("input[id*=\"name\"]", "My Radarr Instance");
|
||||
|
||||
// Save changes
|
||||
await page.click("button:has-text(\"OK\")");
|
||||
|
||||
// Save the configuration again
|
||||
await page.click("button:has-text(\"Save\")", {force: true});
|
||||
|
||||
// Verify the name changed
|
||||
await page.waitForSelector(".btn:has-text(\"My Radarr Instance (RADARR)\")");
|
||||
await expect(page.locator(".btn:has-text(\"My Radarr Instance (RADARR)\")")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should delete external tool", async ({page}) => {
|
||||
// First add a tool
|
||||
await page.goto("/config/externalTools");
|
||||
await page.waitForSelector("button:has-text(\"Add external tool\")");
|
||||
|
||||
await page.click("button:has-text(\"Add external tool\")");
|
||||
await page.click(".dropdown-menu a[ng-click=\"addEntry(model.externalTools, preset)\"]:has-text(\"Radarr\")");
|
||||
await page.waitForSelector(".modal-title:has-text(\"External Tool Configuration\")");
|
||||
await page.fill("input[id*=\"apiKey\"]", "766c3461d6fe44cf83cea3e3c16b5428");
|
||||
await page.click("button:has-text(\"OK\")");
|
||||
|
||||
// Wait for tool to appear
|
||||
await page.waitForSelector(".btn:has-text(\"Radarr (RADARR)\")");
|
||||
|
||||
// Save the configuration first
|
||||
await page.click("button:has-text(\"Save\")", {force: true});
|
||||
|
||||
// Wait for save to complete
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Click delete button
|
||||
await page.click(".btn-danger .glyphicon-remove", {force: true});
|
||||
|
||||
// Save the configuration after deletion
|
||||
await page.click("button:has-text(\"Save\")", {force: true});
|
||||
|
||||
// Verify tool is removed from list
|
||||
await expect(page.locator(".btn:has-text(\"Radarr (RADARR)\")")).not.toBeVisible();
|
||||
await expect(page.locator("text=No external tools configured")).toBeVisible();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user