diff --git a/packages/system/dashboard/images/console/CLAUDE.md b/packages/system/dashboard/images/console/CLAUDE.md index b295bb236..2acf80504 100644 --- a/packages/system/dashboard/images/console/CLAUDE.md +++ b/packages/system/dashboard/images/console/CLAUDE.md @@ -100,6 +100,7 @@ schema). The pipeline in `apps/console/src/components/SchemaForm.tsx`: 3. A chain of `addXxxWidgets(schema, uiSchema)` walks the schema and binds widgets: - any field carrying the `x-cozystack-options` schema keyword → `DynamicOptionsWidget`, a runtime dropdown populated from the cluster's `Option` resource (`core.cozystack.io`) keyed by the keyword's `source`; `addDynamicOptionWidgets` (`lib/dynamic-options.ts`) recurses into `properties`, array `items` and `additionalProperties`, and replaces the former field-name-bound `StorageClassWidget` / `BackupClassWidget` / `VMDiskWidget`. + - object carrying `x-cozystack-no-enable-switch: true` → rendered by `CustomObjectFieldTemplate` as a fieldset saying it has no enable switch and that an `enabled` field in YAML does nothing. Declared in `values.yaml` as `## @x-cozystack-no-enable-switch true` on the `@field` line it belongs to, and generated through to `values.schema.json` and the ApplicationDefinition. Must be declared rather than inferred from the object's fields: an unrelated `valuesOverride`-only object would otherwise inherit the copy, and adding a second field would silently drop it. - object with `additionalProperties: ` → `AdditionalPropertiesField` - credential-shaped fields (`password`, `*token`, `*accessKey`, …) → `SensitiveStringWidget` — see `lib/sensitive-fields.ts` and its tests for diff --git a/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts b/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts index fdc3a264e..87ad2437e 100644 --- a/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts +++ b/packages/system/dashboard/images/console/apps/console/src/lib/focus-first-error.test.ts @@ -1,9 +1,14 @@ -import { describe, it, expect, beforeEach } from "vitest" +import { describe, it, expect, beforeEach, vi } from "vitest" import { focusFirstError } from "@/lib/focus-first-error.ts" describe("focusFirstError", () => { beforeEach(() => { document.body.innerHTML = "" + // jsdom has no layout, so scrollIntoView is absent. Stub it so the call can + // be asserted: focus() is passed preventScroll, which disables the + // browser's own scroll-on-focus, making this the only thing that brings + // the field into view. + HTMLElement.prototype.scrollIntoView = vi.fn() }) it("focuses the field carrying the generated id", () => { @@ -122,6 +127,26 @@ describe("focusFirstError", () => { expect(document.activeElement).toBe(field) }) + it("scrolls the field into view, not only focuses it", () => { + document.body.innerHTML = `` + const field = document.getElementById("root_name") as HTMLElement + + focusFirstError({ property: ".name" }) + + expect(field.scrollIntoView).toHaveBeenCalledWith({ block: "center" }) + }) + + it("escapes a quote in the generated id rather than breaking the selector", () => { + // Only the fallback interpolates the id into a quoted attribute selector, + // so the exact lookup has to miss for this to bite: an unescaped quote + // ends the selector string early and querySelector throws SyntaxError. + document.body.innerHTML = `` + const field = document.getElementById('root_a"b_child') + + expect(() => focusFirstError({ property: '.a"b' })).not.toThrow() + expect(document.activeElement).toBe(field) + }) + it("does nothing when the field is nowhere in the document", () => { document.body.innerHTML = `` diff --git a/packages/system/dashboard/images/console/apps/console/src/test-utils/render.tsx b/packages/system/dashboard/images/console/apps/console/src/test-utils/render.tsx index 496869cfc..04adc5d25 100644 --- a/packages/system/dashboard/images/console/apps/console/src/test-utils/render.tsx +++ b/packages/system/dashboard/images/console/apps/console/src/test-utils/render.tsx @@ -19,19 +19,8 @@ export function renderWithK8sProvider( ui: ReactElement, options: RenderWithK8sOptions, ): RenderResult & { queryClient: QueryClient } { - // Mirror the production defaults in `K8sProvider` that change what a - // component observes: `placeholderData` serves the previous query's data - // with status "success" when the key changes, so a test client without it - // never sees the window where `isLoading` is false but the data is stale. const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - gcTime: 0, - staleTime: 30_000, - placeholderData: (prev: unknown) => prev, - }, - }, + defaultOptions: { queries: { retry: false, gcTime: 0 } }, }) const result = render(