Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/locales/de-DE/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "Das Lesen des Quelltexts legt den Skriptinhalt offen. Bestätige, dass du dieser Anfrage vertraust.",
"decision_reject": "Ablehnen",
"decision_session_allow": "Für diese Sitzung erlauben",
"decision_update_session_allow": "Aktualisieren und für diese Sitzung erlauben",
"decision_allow": "Erlauben",
"err_operation_expired": "Diese Anfrage ist abgelaufen oder nicht mehr ausstehend.",
"approve_install_banner": "Über „Externer Zugriff“ wurde die Installation dieses Skripts angefragt – deine Entscheidung ist erforderlich.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/en-US/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "Reading source exposes the script’s content. Confirm you trust this request.",
"decision_reject": "Reject",
"decision_session_allow": "Allow this session",
"decision_update_session_allow": "Update and allow this session",
"decision_allow": "Allow",
"err_operation_expired": "This request has expired or is no longer pending.",
"approve_install_banner": "Requested to install this script via External Access — your decision is required.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ja-JP/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "ソースを読み取るとスクリプトの内容が公開されます。この要求を信頼できるか確認してください。",
"decision_reject": "拒否",
"decision_session_allow": "このセッションで許可",
"decision_update_session_allow": "更新してこのセッションで許可",
"decision_allow": "許可",
"err_operation_expired": "この要求は期限切れか、保留中ではありません。",
"approve_install_banner": "外部アクセスからこのスクリプトのインストールが要求されました。判断が必要です。",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ko-KR/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "소스를 읽으면 스크립트 내용이 노출됩니다. 이 요청을 신뢰하는지 확인하세요.",
"decision_reject": "거부",
"decision_session_allow": "이 세션에서 허용",
"decision_update_session_allow": "업데이트하고 이 세션에서 허용",
"decision_allow": "허용",
"err_operation_expired": "이 요청이 만료되었거나 더 이상 대기 중이 아닙니다.",
"approve_install_banner": "외부 접근에서 이 스크립트 설치를 요청했습니다. 결정이 필요합니다.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/pt-BR/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "Ler o código-fonte expõe o conteúdo do script. Confirme que você confia nesta solicitação.",
"decision_reject": "Rejeitar",
"decision_session_allow": "Permitir nesta sessão",
"decision_update_session_allow": "Atualizar e permitir nesta sessão",
"decision_allow": "Permitir",
"err_operation_expired": "Esta solicitação expirou ou não está mais pendente.",
"approve_install_banner": "Instalação deste script solicitada via «Acesso externo» — sua decisão é necessária.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/ru-RU/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "Чтение исходного кода раскрывает содержимое скрипта. Подтвердите, что доверяете этому запросу.",
"decision_reject": "Отклонить",
"decision_session_allow": "Разрешить в этом сеансе",
"decision_update_session_allow": "Обновить и разрешить в этом сеансе",
"decision_allow": "Разрешить",
"err_operation_expired": "Этот запрос истёк или больше не ожидает обработки.",
"approve_install_banner": "Через «Внешний доступ» запрошена установка этого скрипта — требуется ваше решение.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/tr-TR/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "Kaynağı okumak betiğin içeriğini açığa çıkarır. Bu isteğe güvendiğinizi onaylayın.",
"decision_reject": "Reddet",
"decision_session_allow": "Bu oturumda izin ver",
"decision_update_session_allow": "Güncelle ve bu oturumda izin ver",
"decision_allow": "İzin ver",
"err_operation_expired": "Bu istek süresi doldu veya artık beklemede değil.",
"approve_install_banner": "«Harici Erişim» üzerinden bu betiğin kurulması istendi — kararınız gerekiyor.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/vi-VN/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "Đọc mã nguồn sẽ tiết lộ nội dung tập lệnh. Hãy xác nhận rằng bạn tin tưởng yêu cầu này.",
"decision_reject": "Từ chối",
"decision_session_allow": "Cho phép phiên này",
"decision_update_session_allow": "Cập nhật và cho phép phiên này",
"decision_allow": "Cho phép",
"err_operation_expired": "Yêu cầu này đã hết hạn hoặc không còn đang chờ xử lý.",
"approve_install_banner": "Yêu cầu cài đặt tập lệnh này qua «Truy cập bên ngoài» — cần bạn quyết định.",
Expand Down
1 change: 1 addition & 0 deletions src/locales/zh-CN/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "读取源码会暴露脚本内容,请确认你信任此请求。",
"decision_reject": "拒绝",
"decision_session_allow": "本会话允许",
"decision_update_session_allow": "更新并本会话允许",
"decision_allow": "允许",
"err_operation_expired": "该请求已过期或不再处于待处理状态。",
"approve_install_banner": "通过「外部接入」请求安装此脚本 —— 需你决定。",
Expand Down
1 change: 1 addition & 0 deletions src/locales/zh-TW/external_access.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"source_privacy_hint": "讀取原始碼會暴露腳本內容,請確認你信任此請求。",
"decision_reject": "拒絕",
"decision_session_allow": "本工作階段允許",
"decision_update_session_allow": "更新並於本工作階段允許",
"decision_allow": "允許",
"err_operation_expired": "此請求已逾期或不再處於待處理狀態。",
"approve_install_banner": "透過「外部接入」請求安裝此腳本 —— 需你決定。",
Expand Down
58 changes: 55 additions & 3 deletions src/pages/install/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, type Mock } from "vitest";
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
import { render, screen, cleanup, fireEvent, within, act } from "@testing-library/react";
import { initTestLanguage } from "@Tests/initTestLanguage";
import { mockMatchMedia } from "@Tests/mockMatchMedia";
import { mockIntersectionObserver } from "@Tests/mockIntersectionObserver";

vi.mock("./useInstallData", () => ({ useInstallData: vi.fn() }));
// Monaco 编辑器无法在 DOM 测试环境中渲染(需 worker + ThemeProvider),用桩替换
Expand Down Expand Up @@ -276,7 +277,7 @@ describe("Install App 安装反馈", () => {
expect(close).toHaveBeenCalledTimes(1);
});

it("安装失败在操作栏上方渲染内联错误条,点击重试触发重装", () => {
it("安装失败在身份卡内渲染内联错误条,点击重试触发重装", () => {
const retryInstall = vi.fn();
mockHook.mockReturnValue({
...baseHook(),
Expand All @@ -285,7 +286,8 @@ describe("Install App 安装反馈", () => {
state: { status: "ready", view: readyView() },
});
render(<App />);
expect(screen.getByTestId("install-error-bar-message")).toHaveTextContent("写入数据库失败");
const card = screen.getByTestId("script-identity");
expect(within(card).getByTestId("install-error-bar-message")).toHaveTextContent("写入数据库失败");
expect(screen.getByTestId("install-primary")).toHaveTextContent("重试安装");
fireEvent.click(screen.getByTestId("install-error-retry"));
expect(retryInstall).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -327,6 +329,56 @@ describe("Install App 安装反馈", () => {
});
});

describe("Install App 桌面端操作位置", () => {
let io: ReturnType<typeof mockIntersectionObserver>;
beforeEach(() => {
io = mockIntersectionObserver();
});

it("主按钮与信任说明位于脚本身份卡内,首屏不渲染吸底栏", () => {
mockHook.mockReturnValue({ ...baseHook(), state: { status: "ready", view: readyView() } });
render(<App />);
const card = screen.getByTestId("script-identity");
expect(within(card).getByTestId("install-primary")).toHaveTextContent("安装");
expect(within(card).getByTestId("action-note")).toHaveTextContent("安装代表您信任该脚本的来源与作者");
expect(screen.queryByTestId("action-bar")).not.toBeInTheDocument();
});

it("外部接入触发时身份卡底部说明切换为渠道说明", () => {
mockHook.mockReturnValue({
...baseHook(),
rejectExternalAccess: vi.fn(),
state: {
status: "ready",
view: readyView({ externalAccess: { operationId: "op-1", contentHash: "abcdef123456" } }),
},
});
render(<App />);
expect(screen.getByTestId("action-note")).toHaveTextContent("由「外部接入」触发");
});

it("身份卡操作组滚出后,吸底栏给出脚本名与可用的主按钮", () => {
const install = vi.fn();
mockHook.mockReturnValue({ ...baseHook(), install, state: { status: "ready", view: readyView() } });
render(<App />);
act(() => {
io.setIntersecting(screen.getByTestId("card-actions"), false);
});
const bar = screen.getByTestId("action-bar");
expect(within(bar).getByText("全网每日签到助手")).toBeInTheDocument();
fireEvent.click(within(bar).getByTestId("install-primary"));
expect(install).toHaveBeenCalledTimes(1);
});

it("移动端主按钮仍在吸底栏,身份卡内没有操作按钮", () => {
mockMatchMedia(true);
mockHook.mockReturnValue({ ...baseHook(), state: { status: "ready", view: readyView() } });
render(<App />);
expect(within(screen.getByTestId("script-identity")).queryByTestId("install-primary")).not.toBeInTheDocument();
expect(within(screen.getByTestId("action-bar")).getByTestId("install-primary")).toBeInTheDocument();
});
});

describe("Install App 权限卡的更新基线", () => {
it("更新场景把已安装版本作为权限对比基线传给权限卡", () => {
mockHook.mockReturnValue({
Expand Down
75 changes: 51 additions & 24 deletions src/pages/install/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Download, RefreshCw, Rss, HardDrive, RotateCcw, PlugZap } from "lucide-react";
import { Download, RefreshCw, Rss, HardDrive, RotateCcw, PlugZap, FileCode } from "lucide-react";
import { useIsMobile } from "@App/pages/components/use-is-mobile";
import { isPermissionOk, openInCurrentTab } from "@App/pkg/utils/utils";
import { InstallLayout } from "./components/InstallLayout";
import { InstallLayout, InstallSummary } from "./components/InstallLayout";
import { ScriptIdentity } from "./components/ScriptIdentity";
import { PermissionCard } from "./components/PermissionCard";
import { SubscribeScripts } from "./components/SubscribeScripts";
Expand All @@ -14,7 +14,6 @@ import { InstallLoading, InstallError, InstallExpired } from "./components/Insta
import { WatchingBanner } from "./components/WatchingBanner";
import { ExternalAccessBanner } from "./components/ExternalAccessBanner";
import { InstallSuccessRibbon } from "./components/InstallSuccessRibbon";
import { InstallErrorBar } from "./components/InstallErrorBar";
import { BackgroundPrompt, backgroundPromptShownKey, keepAlivePromptShownKey } from "./components/BackgroundPrompt";
import { useInstallData } from "./useInstallData";

Expand Down Expand Up @@ -55,10 +54,10 @@ export default function App() {
const codePreviewRef = useRef<CodePreviewHandle>(null);
const installed = outcome.phase === "installed" ? outcome.result : null;
const externalAccessFailure = state.status === "ready" && !!state.view.externalAccess;
const errorBar =
outcome.phase === "failed" ? (
<InstallErrorBar message={outcome.message} onRetry={externalAccessFailure ? undefined : retryInstall} />
) : undefined;
const failure =
outcome.phase === "failed"
? { message: outcome.message, onRetry: externalAccessFailure ? undefined : retryInstall }
: undefined;

// 后台/定时脚本首次安装时,提示开启后台运行(对照 v1.4 checkBackgroundPrompt)
const ready = state.status === "ready" ? state.view : null;
Expand Down Expand Up @@ -122,7 +121,7 @@ export default function App() {
installUrl={state.skill.installUrl}
phase={outcome.phase}
closing={installed?.closing === true}
alert={errorBar}
failure={failure}
onInstall={installSkill}
onCancel={cancelSkill}
/>
Expand All @@ -148,6 +147,16 @@ export default function App() {
: t("external_access:install_context_chip")
: baseTitle;
const titleTone = watching ? "watching" : "default";
const note = watching
? t("install:action_note_watching")
: externalAccess
? // 外部接入触发:提示这是可信渠道请求 + 安装即信任来源/作者(设计稿 ActionNote)
t("external_access:install_action_note")
: view.isUpdate
? t("install:action_note_update")
: view.isSubscribe
? t("install:action_note_subscribe")
: t("install:action_note_install");
const titleIcon = externalAccess
? PlugZap
: view.isSubscribe
Expand Down Expand Up @@ -181,9 +190,24 @@ export default function App() {
/>
) : undefined
}
alert={errorBar}
actions={
failure={failure}
note={note}
summary={
<InstallSummary
icon={
view.iconUrl ? (
<img src={view.iconUrl} alt="" className="size-5 rounded object-contain" />
) : (
<FileCode className="size-4 text-primary" />
)
}
name={view.name}
version={view.version.kind === "install" ? view.version.version : view.version.newVersion}
/>
}
actions={(placement) => (
<InstallActions
placement={placement}
isUpdate={view.isUpdate}
inTrash={view.inTrash}
versionChanged={view.version.kind === "update" && view.version.changed}
Expand All @@ -200,21 +224,24 @@ export default function App() {
view.externalAccess && view.isUpdate ? () => install({ rememberSession: true }) : undefined
}
/>
}
)}
identity={(slots) => (
<ScriptIdentity
name={view.name}
iconUrl={view.iconUrl}
version={view.version}
author={view.author}
source={view.source}
antifeatures={view.antifeatures}
schedule={view.schedule}
scheduleNextRun={view.scheduleNextRun}
description={view.description}
enabled={enabled}
onEnabledChange={setEnabled}
slots={slots}
/>
)}
>
<ScriptIdentity
name={view.name}
iconUrl={view.iconUrl}
version={view.version}
author={view.author}
source={view.source}
antifeatures={view.antifeatures}
schedule={view.schedule}
scheduleNextRun={view.scheduleNextRun}
description={view.description}
enabled={enabled}
onEnabledChange={setEnabled}
/>
{view.externalAccess && (
<ExternalAccessBanner
contentHash={view.externalAccess.contentHash}
Expand Down
56 changes: 47 additions & 9 deletions src/pages/install/components/InstallActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { initTestLanguage } from "@Tests/initTestLanguage";
import { InstallActions } from "./InstallActions";

const baseProps = () => ({
placement: "footer" as const,
isUpdate: false,
isSubscribe: false,
onInstall: vi.fn(),
Expand Down Expand Up @@ -84,11 +85,6 @@ describe("InstallActions 操作区", () => {
expect(screen.getByTestId("watch-toggle")).toHaveTextContent("停止监听");
});

it("操作栏左侧渲染信任提示语(对照设计稿 BarNote)", () => {
render(<InstallActions {...baseProps()} />);
expect(screen.getByTestId("action-bar-note")).toBeInTheDocument();
});

it("未提供 onExternalAccessReject 时不渲染拒绝按钮", () => {
render(<InstallActions {...baseProps()} />);
expect(screen.queryByTestId("external-access-reject")).not.toBeInTheDocument();
Expand All @@ -103,9 +99,49 @@ describe("InstallActions 操作区", () => {
expect(onExternalAccessReject).toHaveBeenCalledTimes(1);
});

it("外部接入触发时操作栏提示语切换为渠道说明(设计稿 ActionNote)", () => {
render(<InstallActions {...baseProps()} onExternalAccessReject={vi.fn()} />);
expect(screen.getByTestId("action-bar-note")).toHaveTextContent("由「外部接入」触发");
it("外部接入更新时「本会话允许」不再是独立按钮,而是主按钮下拉的首项,选中触发会话允许", async () => {
const p = baseProps();
const onExternalAccessSessionAllow = vi.fn();
render(
<InstallActions
{...p}
isUpdate
onExternalAccessReject={vi.fn()}
onExternalAccessSessionAllow={onExternalAccessSessionAllow}
/>
);
expect(screen.queryByTestId("external-access-session-allow")).not.toBeInTheDocument();
await open(screen.getByTestId("install-more"));
const items = screen.getAllByRole("menuitem");
expect(items[0]).toHaveTextContent("更新并本会话允许");
fireEvent.click(items[0]);
expect(onExternalAccessSessionAllow).toHaveBeenCalledTimes(1);
expect(p.onInstall).not.toHaveBeenCalled();
});

it("未提供会话允许回调时下拉不含「更新并本会话允许」", async () => {
render(<InstallActions {...baseProps()} isUpdate onExternalAccessReject={vi.fn()} />);
await open(screen.getByTestId("install-more"));
expect(screen.queryByText("更新并本会话允许")).not.toBeInTheDocument();
});
});

describe("InstallActions 按位置渲染", () => {
it("身份卡内整组按钮放大一号,吸底栏保持常规尺寸", () => {
const { unmount } = render(<InstallActions {...baseProps()} placement="card" />);
expect(screen.getByTestId("install-primary")).toHaveAttribute("data-size", "lg");
expect(screen.getByTestId("close-primary")).toHaveAttribute("data-size", "lg");
unmount();
render(<InstallActions {...baseProps()} placement="sticky" />);
expect(screen.getByTestId("install-primary")).toHaveAttribute("data-size", "default");
});

it("外部接入的拒绝按钮在首屏获得焦点,滚动中途出现的吸底副本不抢焦点", () => {
const { unmount } = render(<InstallActions {...baseProps()} placement="card" onExternalAccessReject={vi.fn()} />);
expect(screen.getByTestId("external-access-reject")).toHaveFocus();
unmount();
render(<InstallActions {...baseProps()} placement="sticky" onExternalAccessReject={vi.fn()} />);
expect(screen.getByTestId("external-access-reject")).not.toHaveFocus();
});
});

Expand Down Expand Up @@ -139,13 +175,15 @@ describe("InstallActions 主按钮就地状态机", () => {
render(
<InstallActions
{...baseProps()}
isUpdate
phase="installed"
onExternalAccessReject={vi.fn()}
onExternalAccessSessionAllow={vi.fn()}
/>
);
expect(screen.getByTestId("external-access-reject")).toBeDisabled();
expect(screen.getByTestId("external-access-session-allow")).toBeDisabled();
// 本会话允许位于主按钮下拉中,下拉触发器锁住即该入口锁住
expect(screen.getByTestId("install-more")).toBeDisabled();
});

it("未传 phase 时保持原有的安装文案与可点状态", () => {
Expand Down
Loading
Loading