From 98e579c8456581a0e4cb0feb6868d10613ec0097 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Fri, 28 Aug 2026 16:08:20 +0500 Subject: [PATCH] test(dashboard): pin the scroll and keep the test QueryClient at its defaults Deleting scrollIntoView left every focus test green, though it is the only thing that brings the field into view: focus is called with preventScroll, which turns off the browser's own scroll-on-focus. So the behaviour the fix exists for was unpinned. Stub scrollIntoView, which jsdom does not implement, and assert it. Same for the selector escaping, which only the fallback path reaches, so the test has to miss the exact id for an unescaped quote to break anything. Keep the test QueryClient at retry and gcTime only. No test needs the wider production defaults: the suite is green either way, while staleTime and placeholderData change what all 53 files observe. It belongs with the change that needs it. Record the new schema keyword in the console notes. Assisted-by: LLM Signed-off-by: Myasnikov Daniil --- .../system/dashboard/images/console/CLAUDE.md | 1 + .../console/src/lib/focus-first-error.test.ts | 27 ++++++++++++++++++- .../apps/console/src/test-utils/render.tsx | 13 +-------- 3 files changed, 28 insertions(+), 13 deletions(-) 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(