Jelajahi Sumber

fix(frontend): map outbound mobile-card actions through the real index too

The desktop outbounds table was keyed by the outbound's real index, but the
mobile card list was left keying the probe trigger and every test-state
lookup by the positional row index. With a hidden balancer-loopback outbound
present, tapping Check on a mobile card probed the wrong outbound and the
Test-All results landed on the wrong card. Key onTest and the
testResult/isTesting reads by record.key, matching the desktop columns.
MHSanaei 1 hari lalu
induk
melakukan
eb769e3f1f

+ 9 - 9
frontend/src/pages/xray/outbounds/OutboundCardList.tsx

@@ -63,8 +63,8 @@ export default function OutboundCardList({
     setShowEgressIp((prev) => ({ ...prev, [key]: visible }));
   };
 
-  const renderEgress = (index: number, rowKey: string) => {
-    const result = testResult(outboundTestStates, index);
+  const renderEgress = (testKey: number, rowKey: string) => {
+    const result = testResult(outboundTestStates, testKey);
     const egress = result?.egress;
     const isEgressVisible = !!showEgressIp[rowKey];
     const flag = countryFlag(egress?.country);
@@ -156,26 +156,26 @@ export default function OutboundCardList({
               ))}
             </div>
           )}
-          {renderEgress(index, String(record.key))}
+          {renderEgress(record.key, String(record.key))}
           <div className="card-foot">
             <span className="traffic-up">↑ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).up)}</span>
             <span className="traffic-sep" />
             <span className="traffic-down">↓ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).down)}</span>
             <span className="card-test">
-              {testResult(outboundTestStates, index) ? (
-                <TestResultPopover result={testResult(outboundTestStates, index)!} />
-              ) : isTesting(outboundTestStates, index) ? (
+              {testResult(outboundTestStates, record.key) ? (
+                <TestResultPopover result={testResult(outboundTestStates, record.key)!} />
+              ) : isTesting(outboundTestStates, record.key) ? (
                 <LoadingOutlined />
               ) : null}
               <Button
                 type="primary"
                 shape="circle"
                 size="small"
-                loading={isTesting(outboundTestStates, index)}
-                disabled={isUntestable(record) || isTesting(outboundTestStates, index)}
+                loading={isTesting(outboundTestStates, record.key)}
+                disabled={isUntestable(record) || isTesting(outboundTestStates, record.key)}
                 icon={<ThunderboltOutlined />}
                 aria-label={t('check')}
-                onClick={() => onTest(index, testMode)}
+                onClick={() => onTest(record.key, testMode)}
               />
             </span>
           </div>

+ 35 - 0
frontend/src/test/outbounds-loopback-index.test.tsx

@@ -53,4 +53,39 @@ describe('OutboundsTab hidden-loopback index mapping', () => {
     expect(onTest).toHaveBeenCalledTimes(1);
     expect(onTest.mock.calls[0][0]).toBe(2);
   });
+
+  it('probes the real array index from the mobile card list as well', () => {
+    const onTest = vi.fn();
+    const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+
+    renderWithProviders(
+      <QueryClientProvider client={queryClient}>
+        <OutboundsTab
+          templateSettings={settingsWithHiddenLoopback()}
+          setTemplateSettings={vi.fn()}
+          outboundsTraffic={[]}
+          outboundTestStates={{}}
+          subscriptionTestStates={{}}
+          testingAll={false}
+          inboundTags={[]}
+          isMobile
+          onResetTraffic={vi.fn()}
+          onTest={onTest}
+          onTestSubscription={vi.fn()}
+          onTestAll={vi.fn()}
+          onShowWarp={vi.fn()}
+          onShowNord={vi.fn()}
+        />
+      </QueryClientProvider>,
+    );
+
+    const cards = document.querySelectorAll('.outbound-card');
+    expect(cards.length).toBe(2);
+
+    const checkButton = cards[1].querySelector('button[aria-label="Check"]') as HTMLButtonElement;
+    fireEvent.click(checkButton);
+
+    expect(onTest).toHaveBeenCalledTimes(1);
+    expect(onTest.mock.calls[0][0]).toBe(2);
+  });
 });