Network: allow configuring external url (BC) (#25285)
This commit is contained in:
parent
bb61acc99c
commit
b535d2ed07
14 changed files with 195 additions and 126 deletions
|
|
@ -1,9 +1,11 @@
|
|||
package globalconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
|
|
@ -11,6 +13,7 @@ import (
|
|||
"github.com/evcc-io/evcc/plugin/mqtt"
|
||||
"github.com/evcc-io/evcc/push"
|
||||
"github.com/evcc-io/evcc/server/eebus"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
"github.com/evcc-io/evcc/util/modbus"
|
||||
)
|
||||
|
|
@ -152,18 +155,45 @@ type Tariffs struct {
|
|||
}
|
||||
|
||||
type Network struct {
|
||||
Schema string `json:"schema"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Schema_ string `json:"schema,omitempty"` // TODO deprecated
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func (c Network) HostPort() string {
|
||||
if c.Schema == "http" && c.Port == 80 || c.Schema == "https" && c.Port == 443 {
|
||||
return c.Host
|
||||
host := "localhost"
|
||||
if h, err := os.Hostname(); err == nil {
|
||||
host = h
|
||||
}
|
||||
return net.JoinHostPort(c.Host, strconv.Itoa(c.Port))
|
||||
if ips := util.LocalIPs(); len(ips) > 0 {
|
||||
host = ips[0].IP.String()
|
||||
}
|
||||
if c.Port == 80 {
|
||||
return host
|
||||
}
|
||||
return net.JoinHostPort(host, strconv.Itoa(c.Port))
|
||||
}
|
||||
|
||||
func (c Network) URI() string {
|
||||
return fmt.Sprintf("%s://%s", c.Schema, c.HostPort())
|
||||
func (c Network) InternalURL() string {
|
||||
return "http://" + c.HostPort()
|
||||
}
|
||||
|
||||
func (c Network) ExternalURL() string {
|
||||
if c.ExternalUrl != "" {
|
||||
return strings.TrimRight(c.ExternalUrl, "/")
|
||||
}
|
||||
return c.InternalURL()
|
||||
}
|
||||
|
||||
// MarshalJSON includes the computed InternalUrl field in JSON output
|
||||
func (c Network) MarshalJSON() ([]byte, error) {
|
||||
type networkAlias Network
|
||||
return json.Marshal(struct {
|
||||
networkAlias
|
||||
InternalUrl string `json:"internalUrl"`
|
||||
}{
|
||||
networkAlias: networkAlias(c),
|
||||
InternalUrl: c.InternalURL(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -576,8 +576,8 @@ input.form-control:read-only,
|
|||
color: var(--evcc-default-text);
|
||||
}
|
||||
|
||||
.dark .form-control::placeholder {
|
||||
color: var(--bs-gray-medium);
|
||||
.form-control::placeholder {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
input[type="time"]::-webkit-calendar-picker-indicator {
|
||||
|
|
|
|||
|
|
@ -97,8 +97,7 @@ export default {
|
|||
return settings.hiddenFeatures === true;
|
||||
},
|
||||
networkStatus() {
|
||||
const { host, port } = store.state?.network || {};
|
||||
return host ? `${host}:${port}` : `${port || ""}`;
|
||||
return store.state?.network?.port || "";
|
||||
},
|
||||
controlStatus() {
|
||||
const sec = store.state?.interval;
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export default {
|
|||
disableRemove: Boolean,
|
||||
noButtons: Boolean,
|
||||
transformReadValues: Function,
|
||||
transformWriteValues: Function,
|
||||
stateKey: String,
|
||||
saveMethod: { type: String, default: "post" },
|
||||
storeValuesInArray: Boolean,
|
||||
|
|
@ -134,8 +135,11 @@ export default {
|
|||
this.saving = true;
|
||||
this.error = "";
|
||||
try {
|
||||
const values = this.trimValues(this.values);
|
||||
const res = await api[this.saveMethod](this.endpoint, values, {
|
||||
const trimmedValues = this.trimValues(deepClone(this.values));
|
||||
const payload = this.transformWriteValues
|
||||
? this.transformWriteValues(trimmedValues)
|
||||
: trimmedValues;
|
||||
const res = await api[this.saveMethod](this.endpoint, payload, {
|
||||
validateStatus: (code) => [200, 202, 400].includes(code),
|
||||
});
|
||||
if (res.status === 200 || res.status === 202) {
|
||||
|
|
|
|||
|
|
@ -4,64 +4,73 @@
|
|||
:title="$t('config.network.title')"
|
||||
endpoint="/config/network"
|
||||
state-key="network"
|
||||
:transform-write-values="transformWriteValues"
|
||||
disable-remove
|
||||
data-testid="network-modal"
|
||||
@changed="$emit('changed')"
|
||||
>
|
||||
<template #default="{ values }">
|
||||
<FormRow
|
||||
id="networkSchema"
|
||||
:label="$t('config.network.labelSchema')"
|
||||
:help="$t('config.network.descriptionSchema')"
|
||||
>
|
||||
<div class="btn-group" role="group">
|
||||
<input
|
||||
id="networkSchemaHttp"
|
||||
v-model="values.schema"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="networkSchema"
|
||||
tabindex="0"
|
||||
value="http"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="networkSchemaHttp">HTTP</label>
|
||||
<input
|
||||
id="networkSchemaHttps"
|
||||
v-model="values.schema"
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="networkSchema"
|
||||
tabindex="0"
|
||||
value="https"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<label class="btn btn-outline-primary" for="networkSchemaHttps">HTTPS</label>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
id="networkHost"
|
||||
:label="$t('config.network.labelHost')"
|
||||
:help="$t('config.network.descriptionHost')"
|
||||
example="evcc.local"
|
||||
>
|
||||
<input id="networkHost" v-model="values.host" class="form-control" />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
id="networkPort"
|
||||
:label="$t('config.network.labelPort')"
|
||||
:help="$t('config.network.descriptionPort')"
|
||||
example="7070"
|
||||
>
|
||||
<input
|
||||
id="networkPort"
|
||||
v-model="values.port"
|
||||
class="form-control w-50 me-2 w-50"
|
||||
type="number"
|
||||
placeholder="7070"
|
||||
required
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
id="networkInternalUrl"
|
||||
:label="$t('config.network.labelInternalUrl')"
|
||||
:help="$t('config.network.descriptionInternalUrl')"
|
||||
>
|
||||
<input
|
||||
id="networkInternalUrl"
|
||||
v-model="values.internalUrl"
|
||||
class="form-control"
|
||||
type="text"
|
||||
readonly
|
||||
tabindex="-1"
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
<FormRow
|
||||
id="networkExternalUrl"
|
||||
:label="$t('config.network.labelExternalUrl')"
|
||||
:help="$t('config.network.descriptionExternalUrl')"
|
||||
example="https://evcc.example.org"
|
||||
optional
|
||||
>
|
||||
<input
|
||||
id="networkExternalUrl"
|
||||
v-model="values.externalUrl"
|
||||
class="form-control"
|
||||
type="text"
|
||||
inputmode="url"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
id="networkHost"
|
||||
:label="$t('config.network.labelHost')"
|
||||
:help="$t('config.network.descriptionHost')"
|
||||
optional
|
||||
>
|
||||
<input
|
||||
id="networkHost"
|
||||
v-model="values.host"
|
||||
class="form-control"
|
||||
spellcheck="false"
|
||||
placeholder="evcc"
|
||||
/>
|
||||
</FormRow>
|
||||
</template>
|
||||
</JsonModal>
|
||||
</template>
|
||||
|
|
@ -74,5 +83,13 @@ export default {
|
|||
name: "NetworkModal",
|
||||
components: { FormRow, JsonModal },
|
||||
emits: ["changed"],
|
||||
methods: {
|
||||
transformWriteValues(values) {
|
||||
const payload = { ...values };
|
||||
delete payload.internalUrl;
|
||||
|
||||
return payload;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# open evcc at http://evcc.local:7070
|
||||
network:
|
||||
schema: http
|
||||
host: evcc.local # .local suffix announces the hostname on MDNS
|
||||
host: evcc # announces the hostname on mDNS
|
||||
port: 7070
|
||||
|
||||
log: debug
|
||||
|
|
|
|||
|
|
@ -261,13 +261,13 @@ func runRoot(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
|
||||
// announce on mDNS
|
||||
if err == nil && strings.HasSuffix(conf.Network.Host, ".local") {
|
||||
if err == nil {
|
||||
err = configureMDNS(conf.Network)
|
||||
}
|
||||
|
||||
// start SHM server
|
||||
if err == nil {
|
||||
err = wrapErrorWithClass(ClassSHM, configureSHM(&conf.SHM, site, httpd))
|
||||
err = wrapErrorWithClass(ClassSHM, configureSHM(&conf.SHM, conf.Network.ExternalURL(), site, httpd))
|
||||
}
|
||||
|
||||
// start HEMS server
|
||||
|
|
|
|||
21
cmd/setup.go
21
cmd/setup.go
|
|
@ -63,9 +63,8 @@ var conf = globalconfig.All{
|
|||
Interval: 10 * time.Second,
|
||||
Log: "info",
|
||||
Network: globalconfig.Network{
|
||||
Schema: "http",
|
||||
Host: "evcc.local",
|
||||
Port: 7070,
|
||||
Host: "",
|
||||
Port: 7070,
|
||||
},
|
||||
Mqtt: globalconfig.Mqtt{
|
||||
Topic: "evcc",
|
||||
|
|
@ -676,14 +675,14 @@ func configureMqtt(conf *globalconfig.Mqtt) error {
|
|||
}
|
||||
|
||||
// setup SHM
|
||||
func configureSHM(conf *shm.Config, site *core.Site, httpd *server.HTTPd) error {
|
||||
func configureSHM(conf *shm.Config, externalUrl string, site *core.Site, httpd *server.HTTPd) error {
|
||||
if settings.Exists(keys.Shm) {
|
||||
if err := settings.Json(keys.Shm, &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := shm.NewFromConfig(*conf, site, httpd.Addr, httpd.Router()); err != nil {
|
||||
if err := shm.NewFromConfig(*conf, externalUrl, site, httpd.Addr, httpd.Router()); err != nil {
|
||||
return fmt.Errorf("failed configuring shm: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -751,8 +750,18 @@ func networkSettings(conf *globalconfig.Network) error {
|
|||
// setup MDNS
|
||||
func configureMDNS(conf globalconfig.Network) error {
|
||||
host := strings.TrimSuffix(conf.Host, ".local")
|
||||
if host == "" {
|
||||
host = "evcc"
|
||||
}
|
||||
|
||||
zc, err := zeroconf.RegisterProxy("evcc", "_http._tcp", "local.", conf.Port, host, nil, []string{"path=/"}, nil)
|
||||
internalURL := conf.InternalURL()
|
||||
text := []string{"path=/", "internal_url=" + internalURL}
|
||||
|
||||
if externalURL := conf.ExternalURL(); externalURL != internalURL {
|
||||
text = append(text, "external_url="+externalURL)
|
||||
}
|
||||
|
||||
zc, err := zeroconf.RegisterProxy("evcc", "_http._tcp", "local.", conf.Port, host, nil, text, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mDNS announcement: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ func tronityToken(conf globalconfig.All, vehicleConf config.Named) (*oauth2.Toke
|
|||
}
|
||||
|
||||
if oc.RedirectURL = cc.RedirectURI; oc.RedirectURL == "" {
|
||||
oc.RedirectURL = fmt.Sprintf("%s/auth/tronity", conf.Network.URI())
|
||||
oc.RedirectURL = fmt.Sprintf("%s/auth/tronity", conf.Network.ExternalURL())
|
||||
}
|
||||
|
||||
return tronityAuthorize(conf.Network.HostPort(), oc)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
network:
|
||||
# schema is the HTTP schema
|
||||
# setting to `https` does not enable https, it only changes the way URLs are generated
|
||||
schema: http
|
||||
# host is the hostname or IP address
|
||||
# if the host name contains a `.local` suffix, the name will be announced on MDNS
|
||||
# docker: MDNS announcements don't work. host must be set to the docker host's name.
|
||||
host: evcc.local
|
||||
# for mDNS announcements. note: mDNS announcements don't work in docker. host must be set to the docker host's name.
|
||||
host: evcc
|
||||
# port is the listening port for UI and api
|
||||
# evcc will listen on all available interfaces
|
||||
port: 7070
|
||||
# externalurl is the user-configurable public url from outside
|
||||
externalurl: https://behind-reverse-proxy
|
||||
|
||||
interval: 30s # control cycle interval. Interval <30s can lead to unexpected behavior, see https://docs.evcc.io/docs/reference/configuration/interval
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ import (
|
|||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -23,8 +20,6 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sempController = "Sunny Home Manager"
|
||||
sempBaseURLEnv = "SEMP_BASE_URL"
|
||||
sempGateway = "urn:schemas-simple-energy-management-protocol:device:Gateway:1"
|
||||
sempDeviceId = "F-%s-%.12x-00" // 6 bytes
|
||||
sempSerialNumber = "%s-%d"
|
||||
|
|
@ -37,13 +32,12 @@ var serverName = "EVCC SEMP Server " + util.Version
|
|||
|
||||
// SEMP is the SMA SEMP server
|
||||
type SEMP struct {
|
||||
log *util.Logger
|
||||
vid string
|
||||
did []byte
|
||||
uid string
|
||||
hostURI string
|
||||
port int
|
||||
site site.API
|
||||
log *util.Logger
|
||||
vid string
|
||||
did []byte
|
||||
uid string
|
||||
uri string
|
||||
site site.API
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
|
@ -53,7 +47,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
// NewFromConfig creates a new SEMP instance from configuration and starts it
|
||||
func NewFromConfig(cfg Config, site site.API, addr string, router *mux.Router) error {
|
||||
func NewFromConfig(cfg Config, hostUri string, site site.API, addr string, router *mux.Router) error {
|
||||
vendorId := cfg.VendorId
|
||||
if vendorId == "" {
|
||||
vendorId = "28081973"
|
||||
|
|
@ -87,20 +81,9 @@ func NewFromConfig(cfg Config, site site.API, addr string, router *mux.Router) e
|
|||
uid: uid.String(),
|
||||
vid: vendorId,
|
||||
did: did,
|
||||
uri: hostUri,
|
||||
}
|
||||
|
||||
// find external port
|
||||
// TODO refactor network config
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err == nil {
|
||||
s.port, err = strconv.Atoi(port)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.hostURI = s.callbackURI()
|
||||
|
||||
s.handlers(router)
|
||||
|
||||
go s.run()
|
||||
|
|
@ -108,7 +91,7 @@ func NewFromConfig(cfg Config, site site.API, addr string, router *mux.Router) e
|
|||
}
|
||||
|
||||
func (s *SEMP) advertise(st, usn string) (*ssdp.Advertiser, error) {
|
||||
descriptor := s.hostURI + basePath + "/description.xml"
|
||||
descriptor := s.uri + basePath + "/description.xml"
|
||||
return ssdp.Advertise(st, usn, descriptor, serverName, maxAge)
|
||||
}
|
||||
|
||||
|
|
@ -139,24 +122,6 @@ func (s *SEMP) run() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *SEMP) callbackURI() string {
|
||||
if uri := os.Getenv(sempBaseURLEnv); uri != "" {
|
||||
return strings.TrimSuffix(uri, "/")
|
||||
}
|
||||
|
||||
ip := "localhost"
|
||||
ips := util.LocalIPs()
|
||||
if len(ips) > 0 {
|
||||
ip = ips[0].IP.String()
|
||||
} else {
|
||||
s.log.ERROR.Printf("couldn't determine ip address- specify %s to override", sempBaseURLEnv)
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("http://%s:%d", ip, s.port)
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
func (s *SEMP) handlers(router *mux.Router) {
|
||||
sempRouter := router.PathPrefix(basePath).Subrouter()
|
||||
getRouter := sempRouter.Methods(http.MethodGet).Subrouter()
|
||||
|
|
@ -198,11 +163,11 @@ func (s *SEMP) gatewayDescription(w http.ResponseWriter, r *http.Request) {
|
|||
FriendlyName: "evcc",
|
||||
Manufacturer: "evcc.io",
|
||||
ModelName: serverName,
|
||||
PresentationURL: s.hostURI,
|
||||
PresentationURL: s.uri,
|
||||
UDN: uid,
|
||||
ServiceDefinition: ServiceDefinition{
|
||||
Xmlns: urnSEMPService,
|
||||
Server: s.hostURI,
|
||||
Server: s.uri,
|
||||
BasePath: basePath,
|
||||
Transport: "HTTP/Pull",
|
||||
ExchangeFormat: "XML",
|
||||
|
|
|
|||
10
i18n/de.json
10
i18n/de.json
|
|
@ -426,12 +426,14 @@
|
|||
"title": "MQTT"
|
||||
},
|
||||
"network": {
|
||||
"descriptionHost": "Verwende den .local-Suffix, um mDNS zu aktivieren. Wird zur Erkennung der mobilen App und einiger OCPP-Wallboxen benötigt.",
|
||||
"descriptionExternalUrl": "Adresse, mit der sich andere Geräte mit evcc verbinden und für die Autodiscovery der evcc-App.",
|
||||
"descriptionHost": "Wird verwendet, um evcc in deinem lokalen Netzwerk anzukündigen.",
|
||||
"descriptionInternalUrl": "Lokale Netzwerkadresse von evcc.",
|
||||
"descriptionPort": "Port für die Web-Oberfläche und API. Du musst deine Browser-URL aktualisieren, wenn du dies änderst.",
|
||||
"descriptionSchema": "Beeinflusst nur die URL-Generierung. Die Auswahl von HTTPS aktiviert keine Verschlüsselung.",
|
||||
"labelHost": "Hostname",
|
||||
"labelExternalUrl": "Externe URL",
|
||||
"labelHost": "mDNS-Hostname",
|
||||
"labelInternalUrl": "Interne URL",
|
||||
"labelPort": "Port",
|
||||
"labelSchema": "Schema",
|
||||
"title": "Netzwerk"
|
||||
},
|
||||
"options": {
|
||||
|
|
|
|||
10
i18n/en.json
10
i18n/en.json
|
|
@ -427,12 +427,14 @@
|
|||
"title": "MQTT"
|
||||
},
|
||||
"network": {
|
||||
"descriptionHost": "Use .local suffix to enable mDNS. Relevant for discovery of the mobile app and some OCPP chargers.",
|
||||
"descriptionExternalUrl": "Address for other devices that want to connect to evcc and for autodiscovery of the evcc app.",
|
||||
"descriptionHost": "Used to announce evcc in your local network.",
|
||||
"descriptionInternalUrl": "Local network address of evcc.",
|
||||
"descriptionPort": "Port for the web interface and API. You'll need to update your browser URL if you change this.",
|
||||
"descriptionSchema": "Only affects how URLs are generated. Selecting HTTPS will not enable encryption.",
|
||||
"labelHost": "Hostname",
|
||||
"labelExternalUrl": "External URL",
|
||||
"labelHost": "mDNS Hostname",
|
||||
"labelInternalUrl": "Internal URL",
|
||||
"labelPort": "Port",
|
||||
"labelSchema": "Schema",
|
||||
"title": "Network"
|
||||
},
|
||||
"options": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import {
|
||||
enableExperimental,
|
||||
expectModalHidden,
|
||||
|
|
@ -9,6 +9,8 @@ import {
|
|||
} from "./utils";
|
||||
|
||||
const CONFIG_GRID_ONLY = "config-grid-only.evcc.yaml";
|
||||
const NETWORK_HOST = "somehostname.local";
|
||||
const NETWORK_EXTERNAL_URL = "https://ext.evcc.example";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
|
|
@ -63,3 +65,44 @@ test.describe("general", async () => {
|
|||
await expect(page.getByRole("heading", { name: "Ahoy World" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("network modal", async () => {
|
||||
test("persists host and external url across restart", async ({ page }) => {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page, false);
|
||||
|
||||
const networkEntry = page.getByTestId("generalconfig-network");
|
||||
await expect(networkEntry).toBeVisible();
|
||||
await networkEntry.getByRole("button", { name: "edit" }).click();
|
||||
|
||||
const modal = page.getByTestId("network-modal");
|
||||
await expectModalVisible(modal);
|
||||
|
||||
const portValue = await modal.getByLabel("Port").inputValue();
|
||||
await modal.getByLabel("External URL").fill(NETWORK_EXTERNAL_URL);
|
||||
await modal.getByLabel("mDNS Hostname").fill(NETWORK_HOST);
|
||||
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expectModalHidden(modal);
|
||||
|
||||
// values immediatelly visible
|
||||
await expect(networkEntry).toContainText(portValue);
|
||||
|
||||
// check persistance
|
||||
await restart(CONFIG_GRID_ONLY);
|
||||
await page.reload();
|
||||
|
||||
await expect(networkEntry).toBeVisible();
|
||||
await networkEntry.getByRole("button", { name: "edit" }).click();
|
||||
await expectModalVisible(modal);
|
||||
|
||||
await expect(modal.getByLabel("mDNS Hostname")).toHaveValue(NETWORK_HOST);
|
||||
await expect(modal.getByLabel("Port")).toHaveValue(portValue);
|
||||
await expect(modal.getByLabel("External URL")).toHaveValue(NETWORK_EXTERNAL_URL);
|
||||
const internalUrl = await modal.getByLabel("Internal URL").inputValue();
|
||||
expect(internalUrl).toContain(`:${portValue}`);
|
||||
|
||||
await modal.getByRole("button", { name: "Cancel" }).click();
|
||||
await expectModalHidden(modal);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue