From d455ba32ae32d13705258c02a872e1761b088c3c Mon Sep 17 00:00:00 2001 From: Baluduvamsi2006 Date: Fri, 20 Mar 2026 19:28:59 +0530 Subject: [PATCH 1/3] feat: add global copyable IDs and stabilize e2e flows --- .../stream_routes.show-disabled-error.spec.ts | 3 +- e2e/utils/common.ts | 3 +- e2e/utils/ui/upstreams.ts | 8 +++- src/components/CopyableText.tsx | 40 +++++++++++++++++++ .../form-slice/FormItemPlugins/PluginCard.tsx | 20 ++++++---- src/locales/de/common.json | 2 + src/locales/en/common.json | 2 + src/locales/es/common.json | 2 + src/locales/tr/common.json | 2 + src/locales/zh/common.json | 2 + src/routes/consumer_groups/index.tsx | 4 ++ .../detail.$username/credentials/index.tsx | 4 ++ src/routes/consumers/index.tsx | 4 ++ src/routes/global_rules/index.tsx | 4 ++ src/routes/plugin_configs/index.tsx | 4 ++ src/routes/protos/index.tsx | 4 ++ src/routes/routes/index.tsx | 4 ++ src/routes/secrets/index.tsx | 4 ++ src/routes/services/index.tsx | 4 ++ src/routes/ssls/index.tsx | 4 ++ src/routes/upstreams/index.tsx | 4 ++ 21 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 src/components/CopyableText.tsx diff --git a/e2e/tests/stream_routes.show-disabled-error.spec.ts b/e2e/tests/stream_routes.show-disabled-error.spec.ts index 91ebfeeb56..58899479ad 100644 --- a/e2e/tests/stream_routes.show-disabled-error.spec.ts +++ b/e2e/tests/stream_routes.show-disabled-error.spec.ts @@ -33,6 +33,7 @@ import { exec } from 'node:child_process'; import { readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import { streamRoutesPom } from '@e2e/pom/stream_routes'; @@ -51,7 +52,7 @@ type APISIXConf = { }; const getE2EServerDir = () => { - const currentDir = new URL('.', import.meta.url).pathname; + const currentDir = path.dirname(fileURLToPath(import.meta.url)); return path.join(currentDir, '../server'); }; diff --git a/e2e/utils/common.ts b/e2e/utils/common.ts index 5c624673b0..d84b70ca24 100644 --- a/e2e/utils/common.ts +++ b/e2e/utils/common.ts @@ -16,6 +16,7 @@ */ import { access, readFile } from 'node:fs/promises'; import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { nanoid } from 'nanoid'; import selfsigned from 'selfsigned'; @@ -25,7 +26,7 @@ type APISIXConf = { deployment: { admin: { admin_key: { key: string }[] } }; }; export const getAPISIXConf = async () => { - const currentDir = new URL('.', import.meta.url).pathname; + const currentDir = path.dirname(fileURLToPath(import.meta.url)); const confPath = path.join(currentDir, '../server/apisix_conf.yml'); const file = await readFile(confPath, 'utf-8'); const res = parse(file) as APISIXConf; diff --git a/e2e/utils/ui/upstreams.ts b/e2e/utils/ui/upstreams.ts index 9def978d95..b4fb10bf21 100644 --- a/e2e/utils/ui/upstreams.ts +++ b/e2e/utils/ui/upstreams.ts @@ -238,7 +238,13 @@ export async function uiFillUpstreamAllFields( await tlsSection .getByRole('textbox', { name: 'Client Key', exact: true }) .fill(tls.key); - await tlsSection.getByRole('switch', { name: 'Verify' }).click(); + const verifySwitch = tlsSection.getByRole('switch', { name: 'Verify' }); + if (!(await verifySwitch.isChecked())) { + const verifyLabel = tlsSection.locator('label:has-text("Verify")').first(); + await verifyLabel.scrollIntoViewIfNeeded(); + await verifyLabel.click(); + } + await expect(verifySwitch).toBeChecked(); // 12. Health Check settings // Activate active health check diff --git a/src/components/CopyableText.tsx b/src/components/CopyableText.tsx new file mode 100644 index 0000000000..f8f9e6be41 --- /dev/null +++ b/src/components/CopyableText.tsx @@ -0,0 +1,40 @@ +/** + * 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. + */ +import { Typography } from 'antd'; +import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +export type CopyableTextProps = { + text: string; + children?: ReactNode; +}; + +export const CopyableText = (props: CopyableTextProps) => { + const { text, children } = props; + const { t } = useTranslation(); + + return ( + + {children ?? text} + + ); +}; diff --git a/src/components/form-slice/FormItemPlugins/PluginCard.tsx b/src/components/form-slice/FormItemPlugins/PluginCard.tsx index 2d9bca8f56..a4bf8348e1 100644 --- a/src/components/form-slice/FormItemPlugins/PluginCard.tsx +++ b/src/components/form-slice/FormItemPlugins/PluginCard.tsx @@ -14,9 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, Card,Group, Text } from '@mantine/core'; +import { Button, Card, Group, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; +import { CopyableText } from '@/components/CopyableText'; + export type PluginCardProps = { name: string; desc?: string; @@ -30,12 +32,16 @@ export type PluginCardProps = { export const PluginCard = (props: PluginCardProps) => { const { name, desc, mode, onAdd, onEdit, onView, onDelete } = props; const { t } = useTranslation(); + const pluginName = String(name); + return ( - + - {name} + + + @@ -50,7 +56,7 @@ export const PluginCard = (props: PluginCardProps) => { size="compact-xs" variant="light" color="blue" - onClick={() => onAdd?.(name)} + onClick={() => onAdd?.(pluginName)} > {t('form.btn.add')} @@ -59,7 +65,7 @@ export const PluginCard = (props: PluginCardProps) => { @@ -70,7 +76,7 @@ export const PluginCard = (props: PluginCardProps) => { size="compact-xs" variant="light" color="blue" - onClick={() => onEdit?.(name)} + onClick={() => onEdit?.(pluginName)} > {t('form.btn.edit')} @@ -78,7 +84,7 @@ export const PluginCard = (props: PluginCardProps) => { size="compact-xs" variant="light" color="red" - onClick={() => onDelete?.(name)} + onClick={() => onDelete?.(pluginName)} > {t('form.btn.delete')} diff --git a/src/locales/de/common.json b/src/locales/de/common.json index 89817b6a10..252f1f8f69 100644 --- a/src/locales/de/common.json +++ b/src/locales/de/common.json @@ -12,6 +12,8 @@ "credentials": { "singular": "Anmeldeinformation" }, + "copy": "Kopieren", + "copy_success": "Kopiert", "form": { "basic": { "desc": "Beschreibung", diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 6544bf790c..66dc2f569a 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -12,6 +12,8 @@ "credentials": { "singular": "Credential" }, + "copy": "Copy", + "copy_success": "Copied", "form": { "basic": { "desc": "Description", diff --git a/src/locales/es/common.json b/src/locales/es/common.json index d46f14f9d7..c23b734e32 100644 --- a/src/locales/es/common.json +++ b/src/locales/es/common.json @@ -12,6 +12,8 @@ "credentials": { "singular": "Credencial" }, + "copy": "Copiar", + "copy_success": "Copiado", "form": { "basic": { "desc": "Descripción", diff --git a/src/locales/tr/common.json b/src/locales/tr/common.json index 63745f663f..2822ce6d45 100644 --- a/src/locales/tr/common.json +++ b/src/locales/tr/common.json @@ -12,6 +12,8 @@ "credentials": { "singular": "Credential" }, + "copy": "Kopyala", + "copy_success": "Kopyalandi", "form": { "basic": { "desc": "Açıklama", diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index b5861692fd..b7c250237b 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -12,6 +12,8 @@ "credentials": { "singular": "凭证" }, + "copy": "复制", + "copy_success": "复制成功", "form": { "basic": { "desc": "描述", diff --git a/src/routes/consumer_groups/index.tsx b/src/routes/consumer_groups/index.tsx index 108ce06b6e..53cf20c44c 100644 --- a/src/routes/consumer_groups/index.tsx +++ b/src/routes/consumer_groups/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getConsumerGroupListQueryOptions, useConsumerGroupList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -43,6 +44,9 @@ function ConsumerGroupsList() { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'name'], diff --git a/src/routes/consumers/detail.$username/credentials/index.tsx b/src/routes/consumers/detail.$username/credentials/index.tsx index d433ba2b59..2501e97f3e 100644 --- a/src/routes/consumers/detail.$username/credentials/index.tsx +++ b/src/routes/consumers/detail.$username/credentials/index.tsx @@ -24,6 +24,7 @@ import { getCredentialListQueryOptions, useCredentialsList, } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -48,6 +49,9 @@ function CredentialsList() { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'desc'], diff --git a/src/routes/consumers/index.tsx b/src/routes/consumers/index.tsx index b431ed3b4b..77f2c441ef 100644 --- a/src/routes/consumers/index.tsx +++ b/src/routes/consumers/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getConsumerListQueryOptions, useConsumerList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -41,6 +42,9 @@ function ConsumersList() { title: t('form.consumers.username'), key: 'username', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'desc'], diff --git a/src/routes/global_rules/index.tsx b/src/routes/global_rules/index.tsx index 248455efae..258018438e 100644 --- a/src/routes/global_rules/index.tsx +++ b/src/routes/global_rules/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getGlobalRuleListQueryOptions, useGlobalRuleList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -56,6 +57,9 @@ function GlobalRulesList() { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { title: t('table.actions'), diff --git a/src/routes/plugin_configs/index.tsx b/src/routes/plugin_configs/index.tsx index bccb89848f..f444626053 100644 --- a/src/routes/plugin_configs/index.tsx +++ b/src/routes/plugin_configs/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getPluginConfigListQueryOptions, usePluginConfigList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -43,6 +44,9 @@ function PluginConfigsList() { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'name'], diff --git a/src/routes/protos/index.tsx b/src/routes/protos/index.tsx index 2754954980..694b35dcc9 100644 --- a/src/routes/protos/index.tsx +++ b/src/routes/protos/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getProtoListQueryOptions, useProtoList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -44,6 +45,9 @@ function RouteComponent() { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { title: t('table.actions'), diff --git a/src/routes/routes/index.tsx b/src/routes/routes/index.tsx index 6b44fbd776..a5034d5a70 100644 --- a/src/routes/routes/index.tsx +++ b/src/routes/routes/index.tsx @@ -22,6 +22,7 @@ import { useTranslation } from 'react-i18next'; import { getRouteListQueryOptions, useRouteList } from '@/apis/hooks'; import type { WithServiceIdFilter } from '@/apis/routes'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -55,6 +56,9 @@ export const RouteList = (props: RouteListProps) => { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'name'], diff --git a/src/routes/secrets/index.tsx b/src/routes/secrets/index.tsx index fa9810c34b..66ebde9c1a 100644 --- a/src/routes/secrets/index.tsx +++ b/src/routes/secrets/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getSecretListQueryOptions, useSecretList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -44,6 +45,9 @@ function SecretList() { key: 'id', valueType: 'text', width: 300, + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'manager'], diff --git a/src/routes/services/index.tsx b/src/routes/services/index.tsx index ea5c5011f6..48020b9f76 100644 --- a/src/routes/services/index.tsx +++ b/src/routes/services/index.tsx @@ -22,6 +22,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getServiceListQueryOptions, useServiceList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -42,6 +43,9 @@ const ServiceList = () => { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'name'], diff --git a/src/routes/ssls/index.tsx b/src/routes/ssls/index.tsx index 9bc31f2073..f315d25171 100644 --- a/src/routes/ssls/index.tsx +++ b/src/routes/ssls/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getSSLListQueryOptions, useSSLList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -41,6 +42,9 @@ function RouteComponent() { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'sni'], diff --git a/src/routes/upstreams/index.tsx b/src/routes/upstreams/index.tsx index a1e6d55d0f..52fe69368e 100644 --- a/src/routes/upstreams/index.tsx +++ b/src/routes/upstreams/index.tsx @@ -21,6 +21,7 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getUpstreamListQueryOptions, useUpstreamList } from '@/apis/hooks'; +import { CopyableText } from '@/components/CopyableText'; import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { ToAddPageBtn, ToDetailPageBtn } from '@/components/page/ToAddPageBtn'; @@ -43,6 +44,9 @@ function RouteComponent() { title: 'ID', key: 'id', valueType: 'text', + render: (_, record) => ( + + ), }, { dataIndex: ['value', 'name'], From a07c435f6f071a04dbb01893cd664b8f011d3c23 Mon Sep 17 00:00:00 2001 From: Baluduvamsi2006 Date: Fri, 20 Mar 2026 20:02:04 +0530 Subject: [PATCH 2/3] refactor: improve copy accessibility and e2e selector stability --- e2e/utils/ui/upstreams.ts | 8 +- src/components/CopyableText.tsx | 133 ++++++++++++++++-- .../form-slice/FormPartUpstream/index.tsx | 1 + 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/e2e/utils/ui/upstreams.ts b/e2e/utils/ui/upstreams.ts index b4fb10bf21..f91c97bbfa 100644 --- a/e2e/utils/ui/upstreams.ts +++ b/e2e/utils/ui/upstreams.ts @@ -238,11 +238,11 @@ export async function uiFillUpstreamAllFields( await tlsSection .getByRole('textbox', { name: 'Client Key', exact: true }) .fill(tls.key); - const verifySwitch = tlsSection.getByRole('switch', { name: 'Verify' }); + const verifyToggle = tlsSection.getByTestId('upstream-tls-verify-switch'); + const verifySwitch = verifyToggle.getByRole('switch'); if (!(await verifySwitch.isChecked())) { - const verifyLabel = tlsSection.locator('label:has-text("Verify")').first(); - await verifyLabel.scrollIntoViewIfNeeded(); - await verifyLabel.click(); + await verifyToggle.scrollIntoViewIfNeeded(); + await verifyToggle.click(); } await expect(verifySwitch).toBeChecked(); diff --git a/src/components/CopyableText.tsx b/src/components/CopyableText.tsx index f8f9e6be41..6922f8fddb 100644 --- a/src/components/CopyableText.tsx +++ b/src/components/CopyableText.tsx @@ -14,27 +14,138 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Typography } from 'antd'; -import type { ReactNode } from 'react'; +import { CheckOutlined, CopyOutlined } from '@ant-design/icons'; +import { + type CSSProperties, + type ReactNode, + useEffect, + useId, + useRef, + useState, +} from 'react'; import { useTranslation } from 'react-i18next'; export type CopyableTextProps = { text: string; children?: ReactNode; + emptyPlaceholder?: ReactNode; +}; + +const wrapperStyle: CSSProperties = { + display: 'inline-flex', + alignItems: 'center', + gap: 4, + maxWidth: '100%', +}; + +const copyButtonStyle: CSSProperties = { + border: 'none', + background: 'transparent', + color: '#1677ff', + cursor: 'pointer', + padding: 0, + display: 'inline-flex', + alignItems: 'center', + lineHeight: 1, +}; + +const srOnlyStyle: CSSProperties = { + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + border: 0, +}; + +const fallbackCopy = (value: string) => { + const textarea = document.createElement('textarea'); + textarea.value = value; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + const copied = document.execCommand('copy'); + document.body.removeChild(textarea); + return copied; +}; + +const writeToClipboard = async (value: string) => { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return; + } + + if (!fallbackCopy(value)) { + throw new Error('Failed to copy text'); + } }; export const CopyableText = (props: CopyableTextProps) => { - const { text, children } = props; + const { text, children, emptyPlaceholder = '-' } = props; const { t } = useTranslation(); + const [copied, setCopied] = useState(false); + const copiedResetTimer = useRef | null>(null); + const announceId = useId(); + + useEffect(() => { + return () => { + if (copiedResetTimer.current) { + clearTimeout(copiedResetTimer.current); + } + }; + }, []); + + const hasValue = text.trim().length > 0; + const displayContent = children ?? (hasValue ? text : emptyPlaceholder); + + const onCopy = async () => { + if (!hasValue) { + return; + } + + try { + await writeToClipboard(text); + setCopied(true); + if (copiedResetTimer.current) { + clearTimeout(copiedResetTimer.current); + } + copiedResetTimer.current = setTimeout(() => { + setCopied(false); + }, 1500); + } catch { + setCopied(false); + } + }; return ( - - {children ?? text} - + + {displayContent} + {hasValue && ( + <> + + + {copied ? t('copy_success') : ''} + + + )} + ); }; diff --git a/src/components/form-slice/FormPartUpstream/index.tsx b/src/components/form-slice/FormPartUpstream/index.tsx index f34492b574..54e9ffe10f 100644 --- a/src/components/form-slice/FormPartUpstream/index.tsx +++ b/src/components/form-slice/FormPartUpstream/index.tsx @@ -44,6 +44,7 @@ export const FormSectionTLS = () => { control={control} name={np('tls.verify')} label={t('form.upstreams.tls.verify')} + data-testid="upstream-tls-verify-switch" /> Date: Fri, 20 Mar 2026 20:36:05 +0530 Subject: [PATCH 3/3] test: harden upstream tls verify e2e toggle --- e2e/utils/ui/upstreams.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/e2e/utils/ui/upstreams.ts b/e2e/utils/ui/upstreams.ts index f91c97bbfa..2ba62577c8 100644 --- a/e2e/utils/ui/upstreams.ts +++ b/e2e/utils/ui/upstreams.ts @@ -238,11 +238,13 @@ export async function uiFillUpstreamAllFields( await tlsSection .getByRole('textbox', { name: 'Client Key', exact: true }) .fill(tls.key); - const verifyToggle = tlsSection.getByTestId('upstream-tls-verify-switch'); - const verifySwitch = verifyToggle.getByRole('switch'); + const verifySwitch = tlsSection + .locator('input[name$="tls.verify"]') + .first(); if (!(await verifySwitch.isChecked())) { - await verifyToggle.scrollIntoViewIfNeeded(); - await verifyToggle.click(); + await verifySwitch.evaluate((node) => { + (node as HTMLInputElement).click(); + }); } await expect(verifySwitch).toBeChecked();