From 1c2a66df4561c994305a0c3ebedb16e4c2830699 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sun, 27 Sep 2026 17:53:57 +0200 Subject: [PATCH] test(10-05): bring every SPA module, composable and component under Vitest - 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui} covering states and a11y roles; every src module is imported by a test - typed fixture helper assigns each JSON fixture to its generated OpenAPI type - fix: iconFor ignores inherited object members such as "constructor" - fix: field controls import ./control instead of the registry (import cycle left a renderer unregistered depending on module load order) - fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption - fix: list announces a failed schema load even when the rows arrive after it - tailwind no longer scans admin/tests; boardwalk/dist rebuilt --- admin/src/app/icons.ts | 8 +- admin/src/components/form/control.ts | 69 +++++ .../components/form/fields/CheckboxField.vue | 2 +- .../components/form/fields/DropdownField.vue | 6 +- .../components/form/fields/NumberField.vue | 2 +- .../components/form/fields/RelationField.vue | 2 +- .../components/form/fields/SwitchField.vue | 2 +- .../src/components/form/fields/TextField.vue | 2 +- .../components/form/fields/TextareaField.vue | 2 +- .../form/fields/UnsupportedField.vue | 2 +- admin/src/components/form/registry.ts | 70 +---- .../components/relation/RelationManager.vue | 2 +- admin/src/components/shell/Breadcrumbs.vue | 2 +- admin/src/styles/main.css | 4 +- admin/src/views/ListView.vue | 16 +- admin/tests/app/client.test.ts | 161 ++++++++++++ admin/tests/app/controllerRoutes.test.ts | 39 +++ admin/tests/app/i18n.test.ts | 137 ++++++++++ admin/tests/app/icons.test.ts | 34 +++ admin/tests/app/listQuery.test.ts | 74 ++++++ admin/tests/app/router.test.ts | 95 +++++++ admin/tests/app/runtime.test.ts | 41 +++ admin/tests/app/theme.test.ts | 34 +++ admin/tests/app/winterUrl.test.ts | 53 ++++ admin/tests/fixtures/typed.ts | 74 ++++++ admin/tests/form/FormField.test.ts | 92 +++++++ admin/tests/form/FormGrid.test.ts | 52 ++++ admin/tests/form/FormTabs.test.ts | 61 +++++ admin/tests/form/FormView.test.ts | 149 +++++++++++ admin/tests/form/RelationField.test.ts | 243 ++++++++++++++++++ admin/tests/form/Settings.test.ts | 111 ++++++++ admin/tests/form/fields.test.ts | 215 ++++++++++++++++ admin/tests/form/formState.test.ts | 92 +++++++ admin/tests/form/registry.test.ts | 127 +++++++++ admin/tests/helpers.ts | 88 ++++++- admin/tests/list/CellValue.test.ts | 77 ++++++ admin/tests/list/DataTable.test.ts | 143 +++++++++++ admin/tests/list/FilterBar.test.ts | 121 +++++++++ admin/tests/list/ListToolbar.test.ts | 57 ++++ admin/tests/list/ListView.test.ts | 132 ++++++++++ admin/tests/list/Pagination.test.ts | 84 ++++++ admin/tests/relation/RelationManager.test.ts | 152 +++++++++++ .../relation/RelationPickerModal.test.ts | 160 ++++++++++++ admin/tests/shell/AppShell.test.ts | 79 ++++++ admin/tests/shell/Breadcrumbs.test.ts | 79 ++++++ admin/tests/shell/PluginRail.test.ts | 136 ++++++++++ admin/tests/shell/SectionFlyout.test.ts | 88 +++++++ admin/tests/shell/SectionPanel.test.ts | 58 +++++ admin/tests/shell/UserMenu.test.ts | 88 +++++++ admin/tests/state/useAuth.test.ts | 141 ++++++++++ admin/tests/state/useBreadcrumbs.test.ts | 12 + admin/tests/state/useNavigation.test.ts | 80 ++++++ admin/tests/state/useSettings.test.ts | 47 ++++ admin/tests/state/useSidebar.test.ts | 95 +++++++ admin/tests/state/useToasts.test.ts | 54 ++++ admin/tests/ui/ui.test.ts | 155 +++++++++++ admin/tests/views/App.test.ts | 54 ++++ admin/tests/views/LoginView.test.ts | 127 +++++++++ boardwalk/dist/assets/index-BQShvn7O.js | 3 - boardwalk/dist/assets/index-X1ye7ftl.js | 3 + boardwalk/dist/index.html | 2 +- 61 files changed, 4299 insertions(+), 91 deletions(-) create mode 100644 admin/src/components/form/control.ts create mode 100644 admin/tests/app/client.test.ts create mode 100644 admin/tests/app/controllerRoutes.test.ts create mode 100644 admin/tests/app/i18n.test.ts create mode 100644 admin/tests/app/icons.test.ts create mode 100644 admin/tests/app/listQuery.test.ts create mode 100644 admin/tests/app/router.test.ts create mode 100644 admin/tests/app/runtime.test.ts create mode 100644 admin/tests/app/theme.test.ts create mode 100644 admin/tests/app/winterUrl.test.ts create mode 100644 admin/tests/fixtures/typed.ts create mode 100644 admin/tests/form/FormField.test.ts create mode 100644 admin/tests/form/FormGrid.test.ts create mode 100644 admin/tests/form/FormTabs.test.ts create mode 100644 admin/tests/form/FormView.test.ts create mode 100644 admin/tests/form/RelationField.test.ts create mode 100644 admin/tests/form/Settings.test.ts create mode 100644 admin/tests/form/fields.test.ts create mode 100644 admin/tests/form/formState.test.ts create mode 100644 admin/tests/form/registry.test.ts create mode 100644 admin/tests/list/CellValue.test.ts create mode 100644 admin/tests/list/DataTable.test.ts create mode 100644 admin/tests/list/FilterBar.test.ts create mode 100644 admin/tests/list/ListToolbar.test.ts create mode 100644 admin/tests/list/ListView.test.ts create mode 100644 admin/tests/list/Pagination.test.ts create mode 100644 admin/tests/relation/RelationManager.test.ts create mode 100644 admin/tests/relation/RelationPickerModal.test.ts create mode 100644 admin/tests/shell/AppShell.test.ts create mode 100644 admin/tests/shell/Breadcrumbs.test.ts create mode 100644 admin/tests/shell/PluginRail.test.ts create mode 100644 admin/tests/shell/SectionFlyout.test.ts create mode 100644 admin/tests/shell/SectionPanel.test.ts create mode 100644 admin/tests/shell/UserMenu.test.ts create mode 100644 admin/tests/state/useAuth.test.ts create mode 100644 admin/tests/state/useBreadcrumbs.test.ts create mode 100644 admin/tests/state/useNavigation.test.ts create mode 100644 admin/tests/state/useSettings.test.ts create mode 100644 admin/tests/state/useSidebar.test.ts create mode 100644 admin/tests/state/useToasts.test.ts create mode 100644 admin/tests/ui/ui.test.ts create mode 100644 admin/tests/views/App.test.ts create mode 100644 admin/tests/views/LoginView.test.ts delete mode 100644 boardwalk/dist/assets/index-BQShvn7O.js create mode 100644 boardwalk/dist/assets/index-X1ye7ftl.js diff --git a/admin/src/app/icons.ts b/admin/src/app/icons.ts index b1d8255..ee884e5 100644 --- a/admin/src/app/icons.ts +++ b/admin/src/app/icons.ts @@ -102,10 +102,16 @@ export const winterIcons: Readonly> = { /** Neutral icon for names neither lucide map knows. */ export const fallbackIcon: Component = Square +/** An own entry of a map: names come from the server, so inherited object + * members such as "constructor" or "toString" never count as icons. */ +function own(map: Readonly>, key: string): T | undefined { + return Object.hasOwn(map, key) ? map[key] : undefined +} + export function iconFor(name: string | null | undefined): Component { if (!name) { return fallbackIcon } const key = name.trim() - return icons[key] ?? icons[winterIcons[key] ?? ''] ?? fallbackIcon + return own(icons, key) ?? own(icons, own(winterIcons, key) ?? '') ?? fallbackIcon } diff --git a/admin/src/components/form/control.ts b/admin/src/components/form/control.ts new file mode 100644 index 0000000..77ad8fe --- /dev/null +++ b/admin/src/components/form/control.ts @@ -0,0 +1,69 @@ +// Shared props and helpers of the field controls (D-05). Kept apart from the +// registry, which imports every control: a control importing the registry +// would close an import cycle. +import type { ControllerParams, FormField, RelationOption } from '../../api/types' + +/** Props every field control receives from FieldRenderer. */ +export interface FieldControlProps { + field: FormField + modelValue: unknown + /** id of the focusable control; the label points at it. */ + controlId: string + invalid?: boolean + /** Space-separated ids of the comment and error lines. */ + describedBy?: string + /** Display labels of the field's current relation values (meta.labels). */ + labels?: RelationOption[] + /** Controller whose option endpoints serve this form; null for settings. */ + source?: ControllerParams | null + /** Id of the record being edited; null on create and on settings pages. */ + recordId?: number | null +} + +// Winter `attributes:` are HTML attributes on the control. Only these are +// passed through; anything else (event handlers included) is dropped. +const allowedAttributes = new Set([ + 'readonly', + 'placeholder', + 'maxlength', + 'minlength', + 'min', + 'max', + 'step', + 'autocomplete', + 'spellcheck', + 'pattern', +]) + +export function controlAttributes(field: FormField): Record { + const out: Record = {} + for (const [name, value] of Object.entries(field.attributes ?? {})) { + const key = name.toLowerCase() + if (!allowedAttributes.has(key) || value === null || value === false) { + continue + } + out[key] = value + } + return out +} + +/** Shared control styling: 44px, radius 10, danger border when invalid. */ +export function controlClass(invalid: boolean | undefined): string { + return [ + 'w-full rounded-control border bg-surface px-3.5 text-text transition-colors duration-150 ease-out', + invalid ? 'border-danger' : 'border-border-strong', + ].join(' ') +} + +/** Truthiness of a stored toggle value (true, 1, "1", "true"). */ +export function toggleOn(value: unknown): boolean { + if (typeof value === 'string') { + return value !== '' && value !== '0' && value.toLowerCase() !== 'false' + } + return Boolean(value) +} + +/** A toggle's next value in the stored value's own type (1/0 or boolean). */ +export function toggleValue(current: unknown, on: boolean): boolean | number { + return typeof current === 'number' ? (on ? 1 : 0) : on +} diff --git a/admin/src/components/form/fields/CheckboxField.vue b/admin/src/components/form/fields/CheckboxField.vue index d8f3223..5d6ab16 100644 --- a/admin/src/components/form/fields/CheckboxField.vue +++ b/admin/src/components/form/fields/CheckboxField.vue @@ -1,6 +1,6 @@ +