@oj-module/cli 0.2.1 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/devkit/README.md +14 -3
- package/devkit/SKILL.md +16 -4
- package/devkit/framework-dev.md +8 -6
- package/devkit/manual.md +243 -23
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +143 -8
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/args.ts +67 -0
- package/src/build.ts +11 -3
- package/src/bundle.ts +197 -0
- package/src/contract/__tests__/handler-scan.test.ts +82 -0
- package/src/contract/check.ts +2 -1
- package/src/contract/handler-scan.ts +32 -0
- package/src/deterministic-tar.ts +120 -0
- package/src/dev.ts +4 -2
- package/src/enable.ts +28 -0
- package/src/index.ts +63 -2
- package/src/info.ts +3 -2
- package/src/init.ts +147 -14
- package/src/install.ts +268 -0
- package/src/keygen.ts +88 -0
- package/src/layout.ts +31 -0
- package/src/lifecycle/__tests__/authoring.test.ts +109 -0
- package/src/lifecycle/__tests__/bundle.test.ts +150 -0
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
- package/src/lifecycle/__tests__/fixtures/README.md +59 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
- package/src/lifecycle/__tests__/helpers.ts +78 -0
- package/src/lifecycle/__tests__/install.test.ts +514 -0
- package/src/lifecycle/__tests__/keygen.test.ts +61 -0
- package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
- package/src/lifecycle/__tests__/publish.test.ts +80 -0
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
- package/src/lifecycle/__tests__/sign.test.ts +131 -0
- package/src/lifecycle/__tests__/smoke.test.ts +61 -0
- package/src/lifecycle/__tests__/trust.test.ts +177 -0
- package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
- package/src/lifecycle/keygen.ts +40 -0
- package/src/lifecycle/sign.ts +74 -0
- package/src/manifest-contract.ts +696 -0
- package/src/oj-cert.ts +19 -0
- package/src/oj-cmd.ts +3 -3
- package/src/preview.ts +2 -2
- package/src/publish.ts +150 -0
- package/src/sign.ts +128 -0
- package/src/site.ts +146 -0
- package/src/trust.ts +116 -0
- package/src/uninstall.ts +29 -0
- package/src/usage.ts +35 -1
- package/templates/README.md +46 -7
- package/templates/api/config.yaml +47 -0
- package/templates/api/seed.mysql.sql +1 -1
- package/templates/api/seed.pg.sql +1 -1
- package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
- package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
- package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
- package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
- package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
- package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
- package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
- package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
- package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
- package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
- package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
- package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
- package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
- package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
- package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
- package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
- package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
- package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
- package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
- package/templates/api/src/_platform/_shared/service/access.ts +2 -2
- package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
- package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/market/api.ts +883 -0
- package/templates/api/src/_platform/market/bootstrap.ts +39 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
- package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
- package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
- package/templates/api/src/_platform/schema.yaml +21 -4
- package/templates/api/src/_platform/seed.sql +2 -2
- package/templates/api/src/_platform/solutions/api.ts +212 -0
- package/templates/api/src/demo/manifest.yaml +5 -0
- package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
- package/templates/api/src/demo/schema.yaml +11 -0
- package/templates/api/src/demo/seed.sql +5 -0
- package/templates/api/src/demo/todos/api.ts +59 -0
- package/templates/api/src/home/line/api.ts +57 -0
- package/templates/api/src/home/manifest.yaml +6 -0
- package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
- package/templates/api/src/home/pie/api.ts +40 -0
- package/templates/api/src/home/schema.yaml +14 -0
- package/templates/api/src/home/seed.sql +70 -0
- package/templates/api/src/system/oplog/api.ts +44 -2
- package/templates/api/tests/fixture.ts +16 -0
- package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
- package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
- package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
- package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
- package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
- package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
- package/templates/api/tests/lifecycle/register.spec.ts +194 -0
- package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
- package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
- package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
- package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
- package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
- package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
- package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
- package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
- package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
- package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
- package/templates/api/tests/rbac-settings.spec.ts +8 -0
- package/templates/init.include.yaml +56 -0
- package/templates/openapi.json +527 -0
- package/templates/package.json +52 -0
- package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
- package/templates/web/src/_platform/__client/api.ts +335 -0
- package/templates/web/src/about/entry.ts +42 -0
- package/templates/web/src/about/locales/en-US.json +16 -0
- package/templates/web/src/about/locales/zh-CN.json +16 -0
- package/templates/web/src/about/pages/constants.ts +20 -0
- package/templates/web/src/about/pages/index.tsx +135 -0
- package/templates/web/src/access/entry.ts +74 -0
- package/templates/web/src/access/locales/en-US.json +46 -0
- package/templates/web/src/access/locales/zh-CN.json +46 -0
- package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
- package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/button-control/index.tsx +181 -0
- package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/page-control/index.tsx +142 -0
- package/templates/web/src/demo/__client/api.schemas.ts +20 -0
- package/templates/web/src/demo/__client/api.ts +89 -0
- package/templates/web/src/exception/entry.ts +80 -0
- package/templates/web/src/exception/locales/en-US.json +13 -0
- package/templates/web/src/exception/locales/zh-CN.json +13 -0
- package/templates/web/src/exception/pages/403/index.tsx +30 -0
- package/templates/web/src/exception/pages/404/index.tsx +30 -0
- package/templates/web/src/exception/pages/500/index.tsx +30 -0
- package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
- package/templates/web/src/home/__client/api.schemas.ts +22 -0
- package/templates/web/src/home/__client/api.ts +113 -0
- package/templates/web/src/login/locales/en-US.json +6 -0
- package/templates/web/src/login/locales/zh-CN.json +6 -0
- package/templates/web/src/login/pages/login.tsx +67 -18
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
- package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
- package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
- package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
- package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
- package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
- package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
- package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
- package/templates/web/src/oj-market/api/client.ts +135 -0
- package/templates/web/src/oj-market/api/context.ts +11 -0
- package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
- package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
- package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
- package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
- package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
- package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
- package/templates/web/src/oj-market/entry.ts +27 -0
- package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
- package/templates/web/src/oj-market/locales/en-US.json +100 -0
- package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
- package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
- package/templates/web/src/oj-market/types.ts +84 -0
- package/templates/web/src/oj-market/utils/poll.ts +36 -0
- package/templates/web/src/oj-market/utils/semver.ts +14 -0
- package/templates/web/src/outside/entry.ts +80 -0
- package/templates/web/src/outside/locales/en-US.json +10 -0
- package/templates/web/src/outside/locales/zh-CN.json +10 -0
- package/templates/web/src/personal-center/entry.ts +1 -10
- package/templates/web/src/personal-center/locales/en-US.json +1 -5
- package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
- package/templates/web/src/route-nest/entry.ts +73 -0
- package/templates/web/src/route-nest/locales/en-US.json +9 -0
- package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
- package/templates/web/src/system/__client/api.schemas.ts +162 -0
- package/templates/web/src/system/__client/api.ts +382 -0
- package/templates/web/src/system/pages/oplog/index.tsx +1 -1
- package/templates/web/src/system/pages/roles/index.tsx +1 -0
- package/templates/web/src/system/pages/settings/index.tsx +1 -1
- package/templates/web/src/system/pages/users/index.tsx +1 -1
- package/templates/web.config.ts +32 -40
- package/vendor/host-versions.json +3 -3
- package/templates/pnpm-workspace.yaml +0 -14
- package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ArrowLeftOutlined } from "@ant-design/icons";
|
|
2
|
+
import { Button, Result } from "antd";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { useNavigate } from "react-router";
|
|
5
|
+
|
|
6
|
+
export default function Exception404() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
|
|
10
|
+
const Result404 = (
|
|
11
|
+
<Result
|
|
12
|
+
status="404"
|
|
13
|
+
title="404"
|
|
14
|
+
subTitle={t("exception:404SubTitle")}
|
|
15
|
+
extra={(
|
|
16
|
+
<Button
|
|
17
|
+
icon={<ArrowLeftOutlined />}
|
|
18
|
+
type="primary"
|
|
19
|
+
onClick={() => {
|
|
20
|
+
navigate(import.meta.env.VITE_BASE_HOME_PATH);
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
{t("common.backHome")}
|
|
24
|
+
</Button>
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return Result404;
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ArrowLeftOutlined } from "@ant-design/icons";
|
|
2
|
+
import { Button, Result } from "antd";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { useNavigate } from "react-router";
|
|
5
|
+
|
|
6
|
+
export default function Exception500() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
|
|
10
|
+
const Result500 = (
|
|
11
|
+
<Result
|
|
12
|
+
status="500"
|
|
13
|
+
title="500"
|
|
14
|
+
subTitle={t("exception:500SubTitle")}
|
|
15
|
+
extra={(
|
|
16
|
+
<Button
|
|
17
|
+
icon={<ArrowLeftOutlined />}
|
|
18
|
+
type="primary"
|
|
19
|
+
onClick={() => {
|
|
20
|
+
navigate(import.meta.env.VITE_BASE_HOME_PATH);
|
|
21
|
+
}}
|
|
22
|
+
>
|
|
23
|
+
{t("common.backHome")}
|
|
24
|
+
</Button>
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return Result500;
|
|
30
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AppstoreOutlined, ArrowLeftOutlined } from "@ant-design/icons";
|
|
2
|
+
|
|
3
|
+
import { Button, Result, Typography } from "antd";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { useNavigate } from "react-router";
|
|
6
|
+
|
|
7
|
+
const { Paragraph } = Typography;
|
|
8
|
+
|
|
9
|
+
export default function UnknownComponent() {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Result
|
|
15
|
+
status="warning"
|
|
16
|
+
icon={<AppstoreOutlined />}
|
|
17
|
+
title={t("exception:menu.exceptionUnknownComponent")}
|
|
18
|
+
subTitle={t("exception:unknownComponentSubTitle")}
|
|
19
|
+
extra={(
|
|
20
|
+
<div>
|
|
21
|
+
<Paragraph code copyable={{ text: location.href }}>
|
|
22
|
+
{location.href}
|
|
23
|
+
</Paragraph>
|
|
24
|
+
<Button
|
|
25
|
+
icon={<ArrowLeftOutlined />}
|
|
26
|
+
type="primary"
|
|
27
|
+
onClick={() => {
|
|
28
|
+
navigate(import.meta.env.VITE_BASE_HOME_PATH);
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{t("common.backHome")}
|
|
32
|
+
</Button>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
)}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
|
|
3
|
+
// AC-D15:仅供 DEV 校验动态 import,生产构建不进产物
|
|
4
|
+
import { z } from "@oj-module/runtime";
|
|
5
|
+
|
|
6
|
+
export const schemas = {
|
|
7
|
+
"homeLinePost": {
|
|
8
|
+
body: z.object({
|
|
9
|
+
"range": z.string()
|
|
10
|
+
}),
|
|
11
|
+
data: z.array(z.number()),
|
|
12
|
+
},
|
|
13
|
+
"homePieGet": {
|
|
14
|
+
query: z.object({
|
|
15
|
+
"by": z.union([z.string(), z.number()]).optional()
|
|
16
|
+
}),
|
|
17
|
+
data: z.array(z.object({
|
|
18
|
+
"value": z.number(),
|
|
19
|
+
"code": z.string()
|
|
20
|
+
})),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
|
|
3
|
+
import { ContractApiError } from "@oj-module/runtime/contract/errors";
|
|
4
|
+
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
5
|
+
import type { ModuleContext } from "@oj-module/runtime";
|
|
6
|
+
import type { z } from "@oj-module/runtime";
|
|
7
|
+
import type { schemas } from "./api.schemas";
|
|
8
|
+
|
|
9
|
+
/** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
|
|
10
|
+
interface OjEnvelope<T> { code: number, msg?: string, data?: T }
|
|
11
|
+
|
|
12
|
+
let req: ScopedRequestLike | undefined;
|
|
13
|
+
|
|
14
|
+
/** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
|
|
15
|
+
export const API_PREFIX = "/home";
|
|
16
|
+
|
|
17
|
+
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
|
+
export const MODULE_PREFIX = "/home";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
|
+
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
23
|
+
* 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
|
|
24
|
+
*/
|
|
25
|
+
export function createHomeClient(ctx: ModuleContext) {
|
|
26
|
+
// 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
|
|
27
|
+
// ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
|
|
28
|
+
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
|
+
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
|
+
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
|
|
32
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
|
+
}
|
|
34
|
+
ctx.register.apiPrefix(API_PREFIX);
|
|
35
|
+
bindRequest(ctx.utils.request);
|
|
36
|
+
return { homeLinePost, homePieGet };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated 请改用 createHomeClient(ctx)——工厂构造即完成
|
|
41
|
+
* 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
|
|
42
|
+
*/
|
|
43
|
+
export function bindRequest(r: ScopedRequestLike): void {
|
|
44
|
+
req = r;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureReq(): ScopedRequestLike {
|
|
48
|
+
if (!req)
|
|
49
|
+
throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createHomeClient(ctx)(或 bindRequest(ctx.utils.request))。");
|
|
50
|
+
return req;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
|
|
54
|
+
async function toApiError(e: unknown): Promise<unknown> {
|
|
55
|
+
if (e instanceof ContractApiError)
|
|
56
|
+
return e;
|
|
57
|
+
// ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
|
|
58
|
+
// 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
|
|
59
|
+
const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
|
|
60
|
+
const res = err?.response;
|
|
61
|
+
const d = err?.data;
|
|
62
|
+
if (res instanceof Response && d && typeof d.code === "number")
|
|
63
|
+
return new ContractApiError(d.code, d.msg ?? res.statusText);
|
|
64
|
+
return e;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type HomeLinePostBody = z.input<(typeof schemas)["homeLinePost"]["body"]>;
|
|
68
|
+
export type HomeLinePostData = z.infer<(typeof schemas)["homeLinePost"]["data"]>;
|
|
69
|
+
|
|
70
|
+
export async function homeLinePost(body: HomeLinePostBody): Promise<HomeLinePostData> {
|
|
71
|
+
const client = ensureReq();
|
|
72
|
+
try {
|
|
73
|
+
const env = await client.post(`home/line`, { json: body }).json<OjEnvelope<HomeLinePostData>>();
|
|
74
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
75
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
76
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
77
|
+
const data = env.data as HomeLinePostData;
|
|
78
|
+
if (import.meta.env.DEV) {
|
|
79
|
+
const { schemas } = await import("./api.schemas");
|
|
80
|
+
const r = schemas["homeLinePost"].data.safeParse(data);
|
|
81
|
+
if (!r.success)
|
|
82
|
+
throw new ContractApiError(-1, `[契约违例] homeLinePost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
throw await toApiError(e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type HomePieGetQuery = z.input<(typeof schemas)["homePieGet"]["query"]>;
|
|
92
|
+
export type HomePieGetData = z.infer<(typeof schemas)["homePieGet"]["data"]>;
|
|
93
|
+
|
|
94
|
+
export async function homePieGet(query: HomePieGetQuery): Promise<HomePieGetData> {
|
|
95
|
+
const client = ensureReq();
|
|
96
|
+
try {
|
|
97
|
+
const env = await client.get(`home/pie`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<HomePieGetData>>();
|
|
98
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
99
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
100
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
101
|
+
const data = env.data as HomePieGetData;
|
|
102
|
+
if (import.meta.env.DEV) {
|
|
103
|
+
const { schemas } = await import("./api.schemas");
|
|
104
|
+
const r = schemas["homePieGet"].data.safeParse(data);
|
|
105
|
+
if (!r.success)
|
|
106
|
+
throw new ContractApiError(-1, `[契约违例] homePieGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
107
|
+
}
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
throw await toApiError(e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { getRedirectPath, useAuthStore } from "@oj-module/runtime";
|
|
2
|
-
import { Alert, Button, Form, Input, Typography } from "antd";
|
|
3
|
-
import { useState } from "react";
|
|
1
|
+
import { fetchLoginMethods, getRedirectPath, useAuthStore } from "@oj-module/runtime";
|
|
2
|
+
import { Alert, Button, Form, Input, Tabs, Typography } from "antd";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
5
5
|
import { useLocation, useNavigate } from "react-router";
|
|
6
6
|
|
|
7
|
+
type LoginMethod = "local" | "ldap" | "sso";
|
|
8
|
+
|
|
7
9
|
interface LoginFormValues {
|
|
8
10
|
username: string
|
|
9
11
|
password: string
|
|
@@ -13,8 +15,13 @@ interface LoginFormValues {
|
|
|
13
15
|
* 登录页内容区。
|
|
14
16
|
*
|
|
15
17
|
* 只写内容区:视口/品牌区/工具区/页脚由框架 FullscreenLayout 兜住。
|
|
16
|
-
* 只依赖框架出口(useAuthStore / getRedirectPath
|
|
17
|
-
*
|
|
18
|
+
* 只依赖框架出口(useAuthStore / getRedirectPath / fetchLoginMethods)与共享依赖
|
|
19
|
+
* (antd / react-router),零框架内部 import。
|
|
20
|
+
*
|
|
21
|
+
* 登录方式由后端匿名端点 `GET /system/login-methods` 发现(local 恒在,
|
|
22
|
+
* ldap/sso 由系统设置开关)。多于一种方式时渲染切换 tab;local/ldap 走同一表单,
|
|
23
|
+
* 仅把 `method` 透传给运行时 `login()`(其按 method 路由到 auth/login / auth/ldap-login);
|
|
24
|
+
* sso 走重定向(端点由宿主/平台提供,模板仅负责跳转)。
|
|
18
25
|
*/
|
|
19
26
|
export default function LoginPage() {
|
|
20
27
|
const { t } = useTranslation();
|
|
@@ -23,12 +30,29 @@ export default function LoginPage() {
|
|
|
23
30
|
const login = useAuthStore(s => s.login);
|
|
24
31
|
const [error, setError] = useState<string>();
|
|
25
32
|
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [methods, setMethods] = useState<LoginMethod[]>(["local"]);
|
|
34
|
+
const [active, setActive] = useState<LoginMethod>("local");
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetchLoginMethods()
|
|
38
|
+
.then((r) => {
|
|
39
|
+
const m = r.methods.filter((x): x is LoginMethod => x === "local" || x === "ldap" || x === "sso");
|
|
40
|
+
if (!m.length)
|
|
41
|
+
return;
|
|
42
|
+
setMethods(m);
|
|
43
|
+
if (!m.includes("local"))
|
|
44
|
+
setActive(m[0]);
|
|
45
|
+
})
|
|
46
|
+
.catch(() => {
|
|
47
|
+
// 发现失败时回落单表单(local),不阻断登录。
|
|
48
|
+
});
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
async function onFinish(values: LoginFormValues, method: LoginMethod) {
|
|
28
52
|
setLoading(true);
|
|
29
53
|
setError(undefined);
|
|
30
54
|
try {
|
|
31
|
-
await login(values);
|
|
55
|
+
await login({ ...values, method });
|
|
32
56
|
navigate(getRedirectPath(search));
|
|
33
57
|
}
|
|
34
58
|
catch (e) {
|
|
@@ -39,6 +63,27 @@ export default function LoginPage() {
|
|
|
39
63
|
}
|
|
40
64
|
}
|
|
41
65
|
|
|
66
|
+
function ssoLogin() {
|
|
67
|
+
// SSO 重定向端点由宿主/平台提供(如 GET /auth/sso),模板侧仅跳转。
|
|
68
|
+
window.location.assign("/auth/sso");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const showTabs = methods.length > 1;
|
|
72
|
+
|
|
73
|
+
const form = (method: LoginMethod) => (
|
|
74
|
+
<Form<LoginFormValues> layout="vertical" onFinish={values => onFinish(values, method)}>
|
|
75
|
+
<Form.Item name="username" label={t("login:page.username")} rules={[{ required: true }]}>
|
|
76
|
+
<Input autoFocus />
|
|
77
|
+
</Form.Item>
|
|
78
|
+
<Form.Item name="password" label={t("login:page.password")} rules={[{ required: true }]}>
|
|
79
|
+
<Input.Password />
|
|
80
|
+
</Form.Item>
|
|
81
|
+
<Button type="primary" htmlType="submit" block loading={loading}>
|
|
82
|
+
{t("login:page.submit")}
|
|
83
|
+
</Button>
|
|
84
|
+
</Form>
|
|
85
|
+
);
|
|
86
|
+
|
|
42
87
|
return (
|
|
43
88
|
<>
|
|
44
89
|
<Typography.Title level={3}>
|
|
@@ -48,17 +93,21 @@ export default function LoginPage() {
|
|
|
48
93
|
{t("login:page.subtitle")}
|
|
49
94
|
</Typography.Paragraph>
|
|
50
95
|
{error && <Alert style={{ marginBottom: 16 }} type="error" message={error} showIcon />}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
96
|
+
{showTabs
|
|
97
|
+
? (
|
|
98
|
+
<Tabs
|
|
99
|
+
activeKey={active}
|
|
100
|
+
onChange={key => setActive(key as LoginMethod)}
|
|
101
|
+
items={methods.map(m => ({
|
|
102
|
+
key: m,
|
|
103
|
+
label: t(`login:method.${m}`),
|
|
104
|
+
children: m === "sso"
|
|
105
|
+
? <Button type="primary" block onClick={ssoLogin}>{t("login:method.sso.action")}</Button>
|
|
106
|
+
: form(m),
|
|
107
|
+
}))}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
: form("local")}
|
|
62
111
|
</>
|
|
63
112
|
);
|
|
64
113
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { IndexRow } from "../types";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { CardGrid } from "../components/CardGrid";
|
|
5
|
+
import { InMemoryMarketClient } from "./marketClient.fake";
|
|
6
|
+
import { renderWithProviders } from "./render";
|
|
7
|
+
|
|
8
|
+
const installed: IndexRow = {
|
|
9
|
+
name: "alpha",
|
|
10
|
+
latest: "1.2.0",
|
|
11
|
+
installed: "1.1.0",
|
|
12
|
+
enabled: true,
|
|
13
|
+
upgradable: true,
|
|
14
|
+
};
|
|
15
|
+
const notInstalled: IndexRow = {
|
|
16
|
+
name: "beta",
|
|
17
|
+
latest: "0.9.0",
|
|
18
|
+
installed: null,
|
|
19
|
+
enabled: null,
|
|
20
|
+
upgradable: false,
|
|
21
|
+
};
|
|
22
|
+
const disabledRow: IndexRow = {
|
|
23
|
+
name: "gamma",
|
|
24
|
+
latest: "2.0.0",
|
|
25
|
+
installed: "2.0.0",
|
|
26
|
+
enabled: false,
|
|
27
|
+
upgradable: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("cardGrid 卡片网格(BDD)", () => {
|
|
31
|
+
const handlers = {
|
|
32
|
+
onInstall: vi.fn(),
|
|
33
|
+
onEnableToggle: vi.fn(),
|
|
34
|
+
onDetails: vi.fn(),
|
|
35
|
+
onHistory: vi.fn(),
|
|
36
|
+
onUninstall: vi.fn(),
|
|
37
|
+
onRollback: vi.fn(),
|
|
38
|
+
onRetry: vi.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
it("given 已装可升级+已停用的卡片 When 渲染 Then 显示 upgradable/enabled/disabled 标识与动作按钮", () => {
|
|
42
|
+
renderWithProviders(
|
|
43
|
+
<CardGrid rows={[installed, disabledRow]} {...handlers} />,
|
|
44
|
+
new InMemoryMarketClient(),
|
|
45
|
+
);
|
|
46
|
+
expect(screen.getByTestId("card-alpha")).toBeTruthy();
|
|
47
|
+
expect(screen.getByText("可升级")).toBeTruthy();
|
|
48
|
+
expect(screen.getByText("已启用")).toBeTruthy();
|
|
49
|
+
expect(screen.getByTestId("card-gamma")).toBeTruthy();
|
|
50
|
+
expect(screen.getByText("已停用")).toBeTruthy();
|
|
51
|
+
// 已装方案应有 启用/停用 + 卸载 按钮
|
|
52
|
+
expect(screen.getAllByText("卸载").length).toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("given 已装可升级卡片 When 渲染 Then 双版本展示「已装 → 最新」且有回滚按钮", () => {
|
|
56
|
+
renderWithProviders(<CardGrid rows={[installed]} {...handlers} />, new InMemoryMarketClient());
|
|
57
|
+
const versions = screen.getByTestId("card-versions-alpha");
|
|
58
|
+
expect(versions.textContent).toContain("1.1.0");
|
|
59
|
+
expect(versions.textContent).toContain("1.2.0");
|
|
60
|
+
expect(screen.getByTestId("card-rollback-alpha")).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("given 未安装卡片 When 渲染 Then 安装按钮显示「安装」而非「升级」,且无回滚按钮", () => {
|
|
64
|
+
renderWithProviders(<CardGrid rows={[notInstalled]} {...handlers} />, new InMemoryMarketClient());
|
|
65
|
+
expect(screen.getByText("安装")).toBeTruthy();
|
|
66
|
+
expect(screen.queryByText("升级")).toBeNull();
|
|
67
|
+
expect(screen.queryByTestId("card-rollback-beta")).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("given 点击回滚 When 渲染 Then 回调 onRollback 携带该行", () => {
|
|
71
|
+
renderWithProviders(<CardGrid rows={[installed]} {...handlers} />, new InMemoryMarketClient());
|
|
72
|
+
screen.getByTestId("card-rollback-alpha").click();
|
|
73
|
+
expect(handlers.onRollback).toHaveBeenCalledWith(installed);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("given 空列表 When 渲染 Then 显示空态", () => {
|
|
77
|
+
renderWithProviders(<CardGrid rows={[]} {...handlers} />, new InMemoryMarketClient());
|
|
78
|
+
expect(screen.getByTestId("card-grid-empty")).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("given loading=true When 渲染 Then 显示加载态", () => {
|
|
82
|
+
renderWithProviders(<CardGrid rows={[]} loading {...handlers} />, new InMemoryMarketClient());
|
|
83
|
+
expect(screen.getByTestId("card-grid-loading")).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("given error When 渲染 Then 显示错误态并可重试", () => {
|
|
87
|
+
renderWithProviders(<CardGrid rows={[]} error="boom" {...handlers} />, new InMemoryMarketClient());
|
|
88
|
+
const retry = screen.getByTestId("card-grid-retry");
|
|
89
|
+
expect(retry).toBeTruthy();
|
|
90
|
+
retry.click();
|
|
91
|
+
expect(handlers.onRetry).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IndexRow, MarketHistory } from "../types";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { HistoryDrawer } from "../components/HistoryDrawer";
|
|
5
|
+
import { InMemoryMarketClient } from "./marketClient.fake";
|
|
6
|
+
import { renderWithProviders } from "./render";
|
|
7
|
+
|
|
8
|
+
const row: IndexRow = { name: "alpha", latest: "1.2.0", installed: "1.2.0", enabled: true, upgradable: false };
|
|
9
|
+
const rows: MarketHistory[] = [
|
|
10
|
+
{ id: 1, solution: "alpha", version: "1.2.0", action: "install", status: "done", operator: "u1", message: null, created_at: 1700000000 },
|
|
11
|
+
{ id: 2, solution: "beta", version: "0.9.0", action: "uninstall", status: "failed", operator: "u2", message: "locked", created_at: 1700000100 },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe("historyDrawer 操作历史(BDD)", () => {
|
|
15
|
+
it("given 历史记录 When 打开 Then 按当前方案过滤并展示", async () => {
|
|
16
|
+
const client = new InMemoryMarketClient({ historyRows: rows });
|
|
17
|
+
renderWithProviders(<HistoryDrawer row={row} open onClose={() => {}} />, client);
|
|
18
|
+
await screen.findByTestId("history-table");
|
|
19
|
+
expect(screen.getByText("1.2.0")).toBeTruthy();
|
|
20
|
+
expect(screen.queryByText("0.9.0")).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("given 空历史 When 打开 Then 显示空提示", async () => {
|
|
24
|
+
const client = new InMemoryMarketClient({ historyRows: [] });
|
|
25
|
+
renderWithProviders(<HistoryDrawer row={row} open onClose={() => {}} />, client);
|
|
26
|
+
await screen.findByText("暂无操作记录");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { IndexRow, TaskRecord } from "../types";
|
|
2
|
+
import { screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { InstallDialog } from "../components/InstallDialog";
|
|
5
|
+
import { InMemoryMarketClient } from "./marketClient.fake";
|
|
6
|
+
import { renderWithProviders } from "./render";
|
|
7
|
+
|
|
8
|
+
const row: IndexRow = { name: "alpha", latest: "1.2.0", installed: null, enabled: null, upgradable: false };
|
|
9
|
+
|
|
10
|
+
describe("installDialog 安装进度(BDD)", () => {
|
|
11
|
+
it("given 安装成功终态 When 打开 Then 轮询渲染 100% 成功", async () => {
|
|
12
|
+
const done: TaskRecord = {
|
|
13
|
+
id: "t1",
|
|
14
|
+
name: "alpha",
|
|
15
|
+
version: "1.2.0",
|
|
16
|
+
status: "done",
|
|
17
|
+
stage: "done",
|
|
18
|
+
progress: 100,
|
|
19
|
+
message: "ok",
|
|
20
|
+
stages: [],
|
|
21
|
+
};
|
|
22
|
+
const client = new InMemoryMarketClient({ taskRecord: done, installResult: { task_id: "t1" } });
|
|
23
|
+
renderWithProviders(<InstallDialog row={row} open onClose={() => {}} />, client);
|
|
24
|
+
await waitFor(() => expect(screen.getByTestId("install-message").textContent).toBe("ok"), { timeout: 5000 });
|
|
25
|
+
expect(client.calls.filter(c => c.startsWith("install:")).length).toBe(1);
|
|
26
|
+
expect(client.calls.filter(c => c.startsWith("task:")).length).toBeGreaterThanOrEqual(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("given 安装失败终态 When 打开 Then 渲染 reason + 不可重试标记", async () => {
|
|
30
|
+
const failed: TaskRecord = {
|
|
31
|
+
id: "t1",
|
|
32
|
+
name: "alpha",
|
|
33
|
+
version: "1.2.0",
|
|
34
|
+
status: "failed",
|
|
35
|
+
stage: "verifying",
|
|
36
|
+
progress: 40,
|
|
37
|
+
message: "verify failed",
|
|
38
|
+
stages: [{ stage: "verifying", ok: false, message: "bad hash" }],
|
|
39
|
+
error: { reason: "包校验失败", retryable: false, hintKey: "lifecycle.stage.verifying.hint" },
|
|
40
|
+
};
|
|
41
|
+
const client = new InMemoryMarketClient({ taskRecord: failed, installResult: { task_id: "t1" } });
|
|
42
|
+
renderWithProviders(<InstallDialog row={row} open onClose={() => {}} />, client);
|
|
43
|
+
await waitFor(() => expect(screen.getByText("包校验失败")).toBeTruthy(), { timeout: 5000 });
|
|
44
|
+
expect(screen.getByText("不可重试")).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("given 回滚(显式旧版 version)When 打开 Then 以该版本触发 install 且标题为「回滚中」", async () => {
|
|
48
|
+
const done: TaskRecord = {
|
|
49
|
+
id: "t1",
|
|
50
|
+
name: "alpha",
|
|
51
|
+
version: "1.1.0",
|
|
52
|
+
status: "done",
|
|
53
|
+
stage: "done",
|
|
54
|
+
progress: 100,
|
|
55
|
+
message: "ok",
|
|
56
|
+
stages: [],
|
|
57
|
+
};
|
|
58
|
+
const client = new InMemoryMarketClient({ taskRecord: done, installResult: { task_id: "t1" } });
|
|
59
|
+
const rollbackRow: IndexRow = { name: "alpha", latest: "1.2.0", installed: "1.2.0", enabled: true, upgradable: false };
|
|
60
|
+
renderWithProviders(
|
|
61
|
+
<InstallDialog row={rollbackRow} open onClose={() => {}} version="1.1.0" mode="rollback" />,
|
|
62
|
+
client,
|
|
63
|
+
);
|
|
64
|
+
await waitFor(() => expect(screen.getByTestId("install-message").textContent).toBe("ok"), { timeout: 5000 });
|
|
65
|
+
// 以显式旧版(而非 latest 1.2.0)触发安装
|
|
66
|
+
expect(client.calls).toContain("install:alpha@1.1.0");
|
|
67
|
+
expect(screen.getByText("回滚中")).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { IndexRow, MarketHistory } from "../types";
|
|
2
|
+
import { screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { RollbackDialog } from "../components/RollbackDialog";
|
|
5
|
+
import { InMemoryMarketClient } from "./marketClient.fake";
|
|
6
|
+
import { renderWithProviders } from "./render";
|
|
7
|
+
|
|
8
|
+
const row: IndexRow = { name: "alpha", latest: "1.2.0", installed: "1.2.0", enabled: true, upgradable: false };
|
|
9
|
+
|
|
10
|
+
const history: MarketHistory[] = [
|
|
11
|
+
{ id: 3, solution: "alpha", version: "1.2.0", action: "upgrade", status: "done", operator: "admin", message: null, created_at: 3 },
|
|
12
|
+
{ id: 2, solution: "alpha", version: "1.1.0", action: "install", status: "done", operator: "admin", message: null, created_at: 2 },
|
|
13
|
+
{ id: 1, solution: "alpha", version: "1.0.0", action: "install", status: "done", operator: "admin", message: null, created_at: 1 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe("rollbackDialog 回滚版本选择(BDD)", () => {
|
|
17
|
+
it("given 已装 1.2.0 且有更早历史版本 When 打开 Then 列出候选旧版(不含已装)并默认选较新旧版,确认回滚之", async () => {
|
|
18
|
+
const onRollback = vi.fn();
|
|
19
|
+
renderWithProviders(
|
|
20
|
+
<RollbackDialog row={row} open onClose={() => {}} onRollback={onRollback} />,
|
|
21
|
+
new InMemoryMarketClient({ historyRows: history }),
|
|
22
|
+
);
|
|
23
|
+
await waitFor(() => expect(screen.getByTestId("rollback-versions")).toBeTruthy(), { timeout: 5000 });
|
|
24
|
+
const group = screen.getByTestId("rollback-versions");
|
|
25
|
+
// 候选 = < 已装:1.1.0 与 1.0.0;已装 1.2.0 不在候选
|
|
26
|
+
expect(group.textContent).toContain("1.1.0");
|
|
27
|
+
expect(group.textContent).toContain("1.0.0");
|
|
28
|
+
// 默认选中较新的旧版 1.1.0 → 确认回滚携带该版本
|
|
29
|
+
screen.getByRole("button", { name: "确认回滚" }).click();
|
|
30
|
+
expect(onRollback).toHaveBeenCalledWith(row, "1.1.0");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("given 无更早历史版本 When 打开 Then 显示无候选提示且确认禁用", async () => {
|
|
34
|
+
const onRollback = vi.fn();
|
|
35
|
+
renderWithProviders(
|
|
36
|
+
<RollbackDialog row={row} open onClose={() => {}} onRollback={onRollback} />,
|
|
37
|
+
new InMemoryMarketClient({ historyRows: history.slice(0, 1) }), // 仅 1.2.0(已装)
|
|
38
|
+
);
|
|
39
|
+
await waitFor(() => expect(screen.getByTestId("rollback-empty")).toBeTruthy(), { timeout: 5000 });
|
|
40
|
+
expect(screen.getByRole("button", { name: "确认回滚" })).toBeDisabled();
|
|
41
|
+
expect(onRollback).not.toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IndexRow } from "../types";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { UninstallConfirm } from "../components/UninstallConfirm";
|
|
6
|
+
import { InMemoryMarketClient } from "./marketClient.fake";
|
|
7
|
+
import { renderWithProviders } from "./render";
|
|
8
|
+
|
|
9
|
+
const row: IndexRow = { name: "alpha", latest: "1.2.0", installed: "1.2.0", enabled: true, upgradable: false };
|
|
10
|
+
|
|
11
|
+
describe("uninstallConfirm 卸载二次确认(BDD)", () => {
|
|
12
|
+
it("given 影响面预检返回受影响角色 When 打开 Then 展示预检清单并二次确认回调", async () => {
|
|
13
|
+
const client = new InMemoryMarketClient({ impact: { name: "alpha", affectedRoles: 2, affectedUsers: 5 } });
|
|
14
|
+
const onConfirmed = vi.fn();
|
|
15
|
+
renderWithProviders(
|
|
16
|
+
<UninstallConfirm row={row} open onClose={() => {}} onConfirmed={onConfirmed} />,
|
|
17
|
+
client,
|
|
18
|
+
);
|
|
19
|
+
await screen.findByTestId("uninstall-impact");
|
|
20
|
+
expect(screen.getByText("影响面预检")).toBeTruthy();
|
|
21
|
+
await userEvent.click(screen.getByRole("button", { name: "确认卸载" }));
|
|
22
|
+
expect(onConfirmed).toHaveBeenCalledWith(row);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("given 孤立方案(无受影响)When 打开 Then 显示空影响面提示", async () => {
|
|
26
|
+
const client = new InMemoryMarketClient({ impact: { name: "alpha", affectedRoles: 0, affectedUsers: 0 } });
|
|
27
|
+
renderWithProviders(<UninstallConfirm row={row} open onClose={() => {}} onConfirmed={vi.fn()} />, client);
|
|
28
|
+
await screen.findByText("无受影响角色或用户(孤立方案)");
|
|
29
|
+
});
|
|
30
|
+
});
|