diff --git a/frontend/editor/src/core/tests/stubbed/cert-sign-wizard.spec.ts b/frontend/editor/src/core/tests/stubbed/cert-sign-wizard.spec.ts index 807e4231d4..17f9070a78 100644 --- a/frontend/editor/src/core/tests/stubbed/cert-sign-wizard.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/cert-sign-wizard.spec.ts @@ -59,7 +59,7 @@ async function mockHardwareEndpoints(page: Page) { } test.describe("CertSign tool - certificate source model", () => { - test("renders, accepts a PDF, and exposes the Upload source", async ({ + test("skips the redundant source step and goes straight to certificate format when Upload is the only source", async ({ page, }) => { await page.route("**/api/v1/security/cert-sign", (route) => @@ -76,10 +76,21 @@ test.describe("CertSign tool - certificate source model", () => { await uploadFiles(page, SAMPLE_PDF); await expect(page).toHaveURL(/\/cert-sign/); - // Source step always offers "Upload" (the former "Manual" mode). + // With no server cert or hardware token there is nothing to choose, so the + // whole "Certificate source" step is hidden and the format picker shows directly. await expect( - page.getByRole("button", { name: /^upload$/i }).first(), - ).toBeAttached({ timeout: 10_000 }); + page.getByRole("button", { name: /pkcs12/i }).first(), + ).toBeVisible({ timeout: 10_000 }); + await expect(page.getByText(/certificate source/i)).toHaveCount(0); + await expect( + page.getByText(/no other certificate sources are available/i), + ).toHaveCount(0); + await expect( + page.getByRole("button", { name: /this device/i }), + ).toHaveCount(0); + await expect(page.getByRole("button", { name: /^server$/i })).toHaveCount( + 0, + ); }); test("does NOT offer 'This device' when not running as desktop", async ({ @@ -89,9 +100,10 @@ test.describe("CertSign tool - certificate source model", () => { await page.waitForLoadState("domcontentloaded"); await uploadFiles(page, SAMPLE_PDF); + // No alternative sources: the source step is hidden, and hardware is never offered. await expect( - page.getByRole("button", { name: /^upload$/i }).first(), - ).toBeAttached({ timeout: 10_000 }); + page.getByRole("button", { name: /pkcs12/i }).first(), + ).toBeVisible({ timeout: 10_000 }); await expect( page.getByRole("button", { name: /this device/i }), ).toHaveCount(0); diff --git a/frontend/editor/src/core/tools/CertSign.tsx b/frontend/editor/src/core/tools/CertSign.tsx index dd0668a491..4f5173b174 100644 --- a/frontend/editor/src/core/tools/CertSign.tsx +++ b/frontend/editor/src/core/tools/CertSign.tsx @@ -1,5 +1,7 @@ +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { useAppConfig } from "@app/contexts/AppConfigContext"; import CertificateTypeSettings from "@app/components/tools/certSign/CertificateTypeSettings"; import CertificateFormatSettings from "@app/components/tools/certSign/CertificateFormatSettings"; import CertificateFilesSettings from "@app/components/tools/certSign/CertificateFilesSettings"; @@ -23,6 +25,25 @@ const CertSign = (props: BaseToolProps) => { props, ); + const { config } = useAppConfig(); + // "Upload" is always available; the source chooser is only meaningful when a + // server certificate or a hardware token gives the user an actual alternative. + const hasCertSourceChoice = + (config?.serverCertificateEnabled ?? false) || + (config?.hardwareSigningAvailable ?? false); + + // With Upload as the only source, keep signMode on MANUAL even if a saved + // automation set AUTO/DEVICE, so the hidden source step can't strand the flow. + useEffect(() => { + if (!hasCertSourceChoice && base.params.parameters.signMode !== "MANUAL") { + base.params.updateParameter("signMode", "MANUAL"); + } + }, [ + hasCertSourceChoice, + base.params.parameters.signMode, + base.params.updateParameter, + ]); + const certTypeTips = useCertificateTypeTips(); const appearanceTips = useSignatureAppearanceTips(); const signModeTips = useSignModeTips(); @@ -63,6 +84,7 @@ const CertSign = (props: BaseToolProps) => { steps: [ { title: t("certSign.source.stepTitle", "Certificate source"), + isVisible: hasCertSourceChoice, isCollapsed: base.settingsCollapsed, onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset