Skip to content
Draft
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ pnpm build
```

The console is built into `apps/console/dist/`.

## Test

```sh
pnpm test
```

Runs the workspace test suites (vitest + jsdom for the console).
12 changes: 10 additions & 2 deletions apps/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"build": "tsc --noEmit && vite build",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@base-ui/react": "^1.4.1",
Expand All @@ -29,12 +31,18 @@
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/ui": "^4.1.6",
"jsdom": "^29.1.1",
"tailwindcss": "^4.2.2",
"typescript": "~6.0.2",
"vite": "^8.0.4"
"vite": "^8.0.4",
"vitest": "^4.1.6"
}
}
99 changes: 99 additions & 0 deletions apps/console/src/components/AdditionalPropertiesField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, it, expect } from "vitest"
import { render, screen } from "@testing-library/react"
import { SchemaForm } from "./SchemaForm.tsx"
import { IMMUTABLE_HELP_TEXT } from "../lib/immutable-paths.ts"

const schemaWithMap = JSON.stringify({
type: "object",
properties: {
labels: {
type: "object",
additionalProperties: { type: "string" },
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "labels are immutable" },
],
},
},
})

const noop = () => {}

describe("AdditionalPropertiesField immutable", () => {
it("renders Add/Remove controls when not immutable", () => {
render(
<SchemaForm
openAPISchema={schemaWithMap}
formData={{ labels: { env: "prod" } }}
onChange={noop}
/>,
)
expect(screen.getByPlaceholderText("Enter key name...")).toBeInTheDocument()
expect(screen.getByRole("button", { name: /add/i })).toBeInTheDocument()
expect(screen.getByRole("button", { name: /remove/i })).toBeInTheDocument()
})

it("treats *-not-last on additionalProperties as whole-map disabled in the UI even though overlay is per-entry", () => {
// Per-value-immutable schema (additionalProperties carries a nested
// CEL rule). The UI deliberately freezes the whole map for simplicity;
// the overlay enforces per-entry semantics. This asymmetry is a known
// trade-off: a YAML-editor bypass that adds a new key will be accepted
// by the overlay, while the form denies the addition. Documented here
// so it isn't mistaken for a UI bug.
const nestedImmutableSchema = JSON.stringify({
type: "object",
properties: {
labels: {
type: "object",
additionalProperties: {
type: "object",
properties: {
value: {
type: "string",
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "value is immutable" },
],
},
},
},
},
},
})
render(
<SchemaForm
openAPISchema={nestedImmutableSchema}
formData={{ labels: { env: { value: "prod" } } }}
onChange={noop}
immutableMode="enforce"
/>,
)
expect(
screen.queryByPlaceholderText("Enter key name..."),
).not.toBeInTheDocument()
expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})

it("hides Add/Remove and disables inner fields when the map is immutable in enforce mode", () => {
render(
<SchemaForm
openAPISchema={schemaWithMap}
formData={{ labels: { env: "prod" } }}
onChange={noop}
immutableMode="enforce"
/>,
)
expect(
screen.queryByPlaceholderText("Enter key name..."),
).not.toBeInTheDocument()
expect(
screen.queryByRole("button", { name: /add/i }),
).not.toBeInTheDocument()
expect(
screen.queryByRole("button", { name: /remove/i }),
).not.toBeInTheDocument()

const innerInput = screen.getByDisplayValue("prod") as HTMLInputElement
expect(innerInput).toBeDisabled()

expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})
})
104 changes: 104 additions & 0 deletions apps/console/src/components/SchemaForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, it, expect } from "vitest"
import { render, screen } from "@testing-library/react"
import { SchemaForm } from "./SchemaForm.tsx"
import { IMMUTABLE_HELP_TEXT } from "../lib/immutable-paths.ts"

const schema = JSON.stringify({
type: "object",
properties: {
version: {
type: "string",
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "version is immutable" },
],
},
description: {
type: "string",
},
},
})

const noop = () => {}

describe("SchemaForm immutableMode", () => {
it("renders fields editable and without help text when immutableMode is omitted", () => {
render(
<SchemaForm
openAPISchema={schema}
formData={{ version: "1.0", description: "hi" }}
onChange={noop}
/>,
)
const versionInput = screen.getByLabelText("version") as HTMLInputElement
expect(versionInput).not.toBeDisabled()
expect(screen.queryByText(IMMUTABLE_HELP_TEXT)).not.toBeInTheDocument()
})

it("greys out immutable fields and shows help text when immutableMode is enforce", () => {
render(
<SchemaForm
openAPISchema={schema}
formData={{ version: "1.0", description: "hi" }}
onChange={noop}
immutableMode="enforce"
/>,
)
const versionInput = screen.getByLabelText("version") as HTMLInputElement
expect(versionInput).toBeDisabled()
const descriptionInput = screen.getByLabelText(
"description",
) as HTMLInputElement
expect(descriptionInput).not.toBeDisabled()
expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})

it("treats immutableMode='off' the same as omitting the prop", () => {
render(
<SchemaForm
openAPISchema={schema}
formData={{ version: "1.0", description: "hi" }}
onChange={noop}
immutableMode="off"
/>,
)
const versionInput = screen.getByLabelText("version") as HTMLInputElement
expect(versionInput).not.toBeDisabled()
expect(screen.queryByText(IMMUTABLE_HELP_TEXT)).not.toBeInTheDocument()
})

it("greys out immutable nested fields inside array items", () => {
const arraySchema = JSON.stringify({
type: "object",
properties: {
volumes: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
"x-kubernetes-validations": [
{ rule: "self == oldSelf", message: "name immutable" },
],
},
size: { type: "string" },
},
},
},
},
})
render(
<SchemaForm
openAPISchema={arraySchema}
formData={{ volumes: [{ name: "disk1", size: "10Gi" }] }}
onChange={noop}
immutableMode="enforce"
/>,
)
const nameInput = screen.getByDisplayValue("disk1") as HTMLInputElement
const sizeInput = screen.getByDisplayValue("10Gi") as HTMLInputElement
expect(nameInput).toBeDisabled()
expect(sizeInput).not.toBeDisabled()
expect(screen.getByText(IMMUTABLE_HELP_TEXT)).toBeInTheDocument()
})
})
Loading