Skip to content
Open
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
23 changes: 22 additions & 1 deletion apps/console/src/routes/BackupCreatePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const h = vi.hoisted(() => ({
strategyRef: { kind: "Strategy", name: "s3" },
takenAt: "2024-01-01T00:00:00Z",
},
// Spec the mocked SchemaForm emits into the page on mount; reset per test.
emitSpec: {} as unknown,
}))

vi.mock("../lib/tenant-context.tsx", () => ({
Expand All @@ -38,7 +40,7 @@ vi.mock("../components/SchemaForm.tsx", () => ({
function MockSchemaForm({ onChange }, ref) {
useImperativeHandle(ref, () => ({ validate: () => h.validateReturn }))
useEffect(() => {
onChange(h.validSpec)
onChange(h.emitSpec)
}, [onChange])
return null
},
Expand All @@ -60,6 +62,7 @@ describe("BackupCreatePage submit validation gate", () => {
h.createMutateAsync.mockReset()
h.createMutateAsync.mockResolvedValue({})
h.validateReturn = true
h.emitSpec = h.validSpec
})

it("does not POST when the form fails RJSF validation", async () => {
Expand All @@ -83,4 +86,22 @@ describe("BackupCreatePage submit validation gate", () => {

expect(h.createMutateAsync).toHaveBeenCalledTimes(1)
})

it("runs RJSF validation before the page-level required-field alerts", async () => {
// Form is RJSF-invalid AND a page-required field is missing. The gate must
// fire first, so no alert() is shown — proving validate() runs before the
// manual checks (under the old ordering the applicationRef alert fired).
h.validateReturn = false
h.emitSpec = {}
const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => {})
const user = userEvent.setup()
renderPage()

await user.type(screen.getByRole("textbox"), "my-backup")
await user.click(screen.getByRole("button", { name: /create/i }))

expect(h.createMutateAsync).not.toHaveBeenCalled()
expect(alertSpy).not.toHaveBeenCalled()
alertSpy.mockRestore()
})
})
8 changes: 4 additions & 4 deletions apps/console/src/routes/BackupCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export function BackupCreatePage() {
return
}

// Run RJSF validation before the page-level checks so schema-required
// fields render inline errors instead of being masked by the alerts below.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

if (!formData.applicationRef?.kind || !formData.applicationRef?.name) {
alert("Application reference is required")
return
Expand All @@ -95,10 +99,6 @@ export function BackupCreatePage() {
return
}

// The submit button lives outside RJSF and bypasses its validation, so
// trigger it explicitly; an invalid spec renders errors inline and aborts.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

const resource = {
apiVersion: "backups.cozystack.io/v1alpha1",
kind: "Backup",
Expand Down
19 changes: 18 additions & 1 deletion apps/console/src/routes/BackupJobCreatePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const h = vi.hoisted(() => ({
applicationRef: { kind: "VirtualMachine", name: "demo" },
backupClassName: "s3",
},
emitSpec: {} as unknown,
}))

vi.mock("../lib/tenant-context.tsx", () => ({
Expand All @@ -37,7 +38,7 @@ vi.mock("../components/SchemaForm.tsx", () => ({
function MockSchemaForm({ onChange }, ref) {
useImperativeHandle(ref, () => ({ validate: () => h.validateReturn }))
useEffect(() => {
onChange(h.validSpec)
onChange(h.emitSpec)
}, [onChange])
return null
},
Expand All @@ -59,6 +60,7 @@ describe("BackupJobCreatePage submit validation gate", () => {
h.createMutateAsync.mockReset()
h.createMutateAsync.mockResolvedValue({})
h.validateReturn = true
h.emitSpec = h.validSpec
})

it("does not POST when the form fails RJSF validation", async () => {
Expand All @@ -82,4 +84,19 @@ describe("BackupJobCreatePage submit validation gate", () => {

expect(h.createMutateAsync).toHaveBeenCalledTimes(1)
})

it("runs RJSF validation before the page-level required-field alerts", async () => {
h.validateReturn = false
h.emitSpec = {}
const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => {})
const user = userEvent.setup()
renderPage()

await user.type(screen.getByRole("textbox"), "my-job")
await user.click(screen.getByRole("button", { name: /create/i }))

expect(h.createMutateAsync).not.toHaveBeenCalled()
expect(alertSpy).not.toHaveBeenCalled()
alertSpy.mockRestore()
})
})
8 changes: 4 additions & 4 deletions apps/console/src/routes/BackupJobCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export function BackupJobCreatePage() {
return
}

// Run RJSF validation before the page-level checks so schema-required
// fields render inline errors instead of being masked by the alerts below.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

if (!formData.applicationRef?.kind || !formData.applicationRef?.name) {
alert("Application reference is required")
return
Expand All @@ -107,10 +111,6 @@ export function BackupJobCreatePage() {
return
}

// The submit button lives outside RJSF and bypasses its validation, so
// trigger it explicitly; an invalid spec renders errors inline and aborts.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

// planRef is optional metadata recording which Plan triggered the job. The
// dropdown ships an empty sentinel; strip it so the API never receives
// `planRef: { name: "" }`, which would otherwise round-trip as a malformed
Expand Down
19 changes: 18 additions & 1 deletion apps/console/src/routes/BackupPlanCreatePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const h = vi.hoisted(() => ({
applicationRef: { kind: "VirtualMachine", name: "demo" },
backupClassName: "s3",
},
emitSpec: {} as unknown,
}))

vi.mock("../lib/tenant-context.tsx", () => ({
Expand All @@ -37,7 +38,7 @@ vi.mock("../components/SchemaForm.tsx", () => ({
function MockSchemaForm({ onChange }, ref) {
useImperativeHandle(ref, () => ({ validate: () => h.validateReturn }))
useEffect(() => {
onChange(h.validSpec)
onChange(h.emitSpec)
}, [onChange])
return null
},
Expand All @@ -59,6 +60,7 @@ describe("BackupPlanCreatePage submit validation gate", () => {
h.createMutateAsync.mockReset()
h.createMutateAsync.mockResolvedValue({})
h.validateReturn = true
h.emitSpec = h.validSpec
})

it("does not POST when the form fails RJSF validation", async () => {
Expand All @@ -82,4 +84,19 @@ describe("BackupPlanCreatePage submit validation gate", () => {

expect(h.createMutateAsync).toHaveBeenCalledTimes(1)
})

it("runs RJSF validation before the page-level required-field alerts", async () => {
h.validateReturn = false
h.emitSpec = {}
const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => {})
const user = userEvent.setup()
renderPage()

await user.type(screen.getByRole("textbox"), "my-plan")
await user.click(screen.getByRole("button", { name: /create/i }))

expect(h.createMutateAsync).not.toHaveBeenCalled()
expect(alertSpy).not.toHaveBeenCalled()
alertSpy.mockRestore()
})
})
8 changes: 4 additions & 4 deletions apps/console/src/routes/BackupPlanCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ export function BackupPlanCreatePage() {
return
}

// Run RJSF validation before the page-level checks so schema-required
// fields render inline errors instead of being masked by the alerts below.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

if (!formData.applicationRef?.kind || !formData.applicationRef?.name) {
alert("Application reference is required")
return
Expand All @@ -112,10 +116,6 @@ export function BackupPlanCreatePage() {
return
}

// The submit button lives outside RJSF and bypasses its validation, so
// trigger it explicitly; an invalid spec renders errors inline and aborts.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

const resource = {
apiVersion: "backups.cozystack.io/v1alpha1",
kind: "Plan",
Expand Down
19 changes: 18 additions & 1 deletion apps/console/src/routes/BackupRestoreJobCreatePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const h = vi.hoisted(() => ({
validSpec: {
backupRef: { name: "backup-1" },
},
emitSpec: {} as unknown,
}))

vi.mock("../lib/tenant-context.tsx", () => ({
Expand All @@ -36,7 +37,7 @@ vi.mock("../components/SchemaForm.tsx", () => ({
function MockSchemaForm({ onChange }, ref) {
useImperativeHandle(ref, () => ({ validate: () => h.validateReturn }))
useEffect(() => {
onChange(h.validSpec)
onChange(h.emitSpec)
}, [onChange])
return null
},
Expand All @@ -58,6 +59,7 @@ describe("BackupRestoreJobCreatePage submit validation gate", () => {
h.createMutateAsync.mockReset()
h.createMutateAsync.mockResolvedValue({})
h.validateReturn = true
h.emitSpec = h.validSpec
})

it("does not POST when the form fails RJSF validation", async () => {
Expand All @@ -81,4 +83,19 @@ describe("BackupRestoreJobCreatePage submit validation gate", () => {

expect(h.createMutateAsync).toHaveBeenCalledTimes(1)
})

it("runs RJSF validation before the page-level required-field alerts", async () => {
h.validateReturn = false
h.emitSpec = {}
const alertSpy = vi.spyOn(window, "alert").mockImplementation(() => {})
const user = userEvent.setup()
renderPage()

await user.type(screen.getByRole("textbox"), "my-restore")
await user.click(screen.getByRole("button", { name: /create/i }))

expect(h.createMutateAsync).not.toHaveBeenCalled()
expect(alertSpy).not.toHaveBeenCalled()
alertSpy.mockRestore()
})
})
8 changes: 4 additions & 4 deletions apps/console/src/routes/BackupRestoreJobCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export function BackupRestoreJobCreatePage() {
return
}

// Run RJSF validation before the page-level checks so schema-required
// fields render inline errors instead of being masked by the alerts below.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

if (!formData.backupRef?.name) {
alert("Backup reference is required")
return
Expand All @@ -134,10 +138,6 @@ export function BackupRestoreJobCreatePage() {
return
}

// The submit button lives outside RJSF and bypasses its validation, so
// trigger it explicitly; an invalid spec renders errors inline and aborts.
if (schemaFormRef.current && !schemaFormRef.current.validate()) return

// Strip an empty targetApplicationRef so the API does not receive an empty
// object that the API server would reject as malformed.
const spec = { ...formData }
Expand Down
Loading