@zeix/le-truc 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ai-context.md +234 -0
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/.editorconfig +12 -0
- package/.github/copilot-instructions.md +62 -0
- package/.github/workflows/codeql.yml +108 -0
- package/.github/workflows/static.yml +43 -0
- package/.prettierrc +17 -0
- package/CLAUDE.md +215 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +160 -0
- package/LICENSE +21 -0
- package/README.md +474 -0
- package/biome.json +295 -0
- package/bun.lock +239 -0
- package/docs/about.html +105 -0
- package/docs/assets/main.css +1 -0
- package/docs/assets/main.js +10 -0
- package/docs/assets/main.js.map +66 -0
- package/docs/components.html +293 -0
- package/docs/data-flow.html +308 -0
- package/docs/examples/basic-button.html +367 -0
- package/docs/examples/basic-counter.html +188 -0
- package/docs/examples/basic-hello.html +138 -0
- package/docs/examples/basic-number.html +271 -0
- package/docs/examples/basic-pluralize.html +214 -0
- package/docs/examples/card-callout.html +152 -0
- package/docs/examples/card-mediaqueries.html +138 -0
- package/docs/examples/context-media.html +198 -0
- package/docs/examples/empty.html +37 -0
- package/docs/examples/form-checkbox.html +233 -0
- package/docs/examples/form-combobox.html +420 -0
- package/docs/examples/form-listbox.html +434 -0
- package/docs/examples/form-radiogroup.html +296 -0
- package/docs/examples/form-spinbutton.html +402 -0
- package/docs/examples/form-textbox.html +361 -0
- package/docs/examples/layout.html +67 -0
- package/docs/examples/module-carousel.html +552 -0
- package/docs/examples/module-catalog.html +241 -0
- package/docs/examples/module-codeblock.html +270 -0
- package/docs/examples/module-dialog.html +343 -0
- package/docs/examples/module-lazyload.html +289 -0
- package/docs/examples/module-list.html +197 -0
- package/docs/examples/module-pagination.html +283 -0
- package/docs/examples/module-scrollarea.html +447 -0
- package/docs/examples/module-tabgroup.html +526 -0
- package/docs/examples/module-todo.html +367 -0
- package/docs/examples/module-with-type.html +63 -0
- package/docs/examples/nested-components.html +88 -0
- package/docs/examples/recursive.html +56 -0
- package/docs/examples/simple-text.html +39 -0
- package/docs/examples/snippet.html +93 -0
- package/docs/examples/with-styles.html +75 -0
- package/docs/getting-started.html +143 -0
- package/docs/index.html +112 -0
- package/docs/sitemap.xml +28 -0
- package/docs/styling.html +160 -0
- package/docs/sw.js +112 -0
- package/docs-src/api/README.md +478 -0
- package/docs-src/api/_media/LICENSE +21 -0
- package/docs-src/api/classes/CircularDependencyError.md +299 -0
- package/docs-src/api/classes/CircularMutationError.md +301 -0
- package/docs-src/api/classes/ContextRequestEvent.md +590 -0
- package/docs-src/api/classes/DependencyTimeoutError.md +301 -0
- package/docs-src/api/classes/InvalidCallbackError.md +303 -0
- package/docs-src/api/classes/InvalidComponentNameError.md +295 -0
- package/docs-src/api/classes/InvalidCustomElementError.md +301 -0
- package/docs-src/api/classes/InvalidEffectsError.md +301 -0
- package/docs-src/api/classes/InvalidPropertyNameError.md +307 -0
- package/docs-src/api/classes/InvalidReactivesError.md +307 -0
- package/docs-src/api/classes/InvalidSignalValueError.md +303 -0
- package/docs-src/api/classes/MissingElementError.md +307 -0
- package/docs-src/api/classes/NullishSignalValueError.md +299 -0
- package/docs-src/api/classes/StoreKeyExistsError.md +303 -0
- package/docs-src/api/classes/StoreKeyRangeError.md +299 -0
- package/docs-src/api/classes/StoreKeyReadonlyError.md +303 -0
- package/docs-src/api/functions/asBoolean.md +21 -0
- package/docs-src/api/functions/asEnum.md +31 -0
- package/docs-src/api/functions/asInteger.md +39 -0
- package/docs-src/api/functions/asJSON.md +49 -0
- package/docs-src/api/functions/asNumber.md +37 -0
- package/docs-src/api/functions/asString.md +37 -0
- package/docs-src/api/functions/createCollection.md +83 -0
- package/docs-src/api/functions/createSensor.md +71 -0
- package/docs-src/api/functions/dangerouslySetInnerHTML.md +48 -0
- package/docs-src/api/functions/defineComponent.md +65 -0
- package/docs-src/api/functions/isCollection.md +37 -0
- package/docs-src/api/functions/isParser.md +41 -0
- package/docs-src/api/functions/match.md +47 -0
- package/docs-src/api/functions/on.md +58 -0
- package/docs-src/api/functions/pass.md +53 -0
- package/docs-src/api/functions/provideContexts.md +47 -0
- package/docs-src/api/functions/read.md +47 -0
- package/docs-src/api/functions/requestContext.md +51 -0
- package/docs-src/api/functions/resolve.md +40 -0
- package/docs-src/api/functions/runEffects.md +51 -0
- package/docs-src/api/functions/runElementEffects.md +57 -0
- package/docs-src/api/functions/schedule.md +33 -0
- package/docs-src/api/functions/setAttribute.md +48 -0
- package/docs-src/api/functions/setProperty.md +52 -0
- package/docs-src/api/functions/setStyle.md +48 -0
- package/docs-src/api/functions/setText.md +42 -0
- package/docs-src/api/functions/show.md +42 -0
- package/docs-src/api/functions/toSignal.md +37 -0
- package/docs-src/api/functions/toggleAttribute.md +48 -0
- package/docs-src/api/functions/toggleClass.md +48 -0
- package/docs-src/api/functions/updateElement.md +53 -0
- package/docs-src/api/globals.md +131 -0
- package/docs-src/api/type-aliases/Cleanup.md +27 -0
- package/docs-src/api/type-aliases/Collection.md +91 -0
- package/docs-src/api/type-aliases/CollectionListener.md +27 -0
- package/docs-src/api/type-aliases/Component.md +17 -0
- package/docs-src/api/type-aliases/ComponentProp.md +11 -0
- package/docs-src/api/type-aliases/ComponentProps.md +11 -0
- package/docs-src/api/type-aliases/ComponentSetup.md +31 -0
- package/docs-src/api/type-aliases/ComponentUI.md +27 -0
- package/docs-src/api/type-aliases/Computed.md +49 -0
- package/docs-src/api/type-aliases/ComputedCallback.md +29 -0
- package/docs-src/api/type-aliases/Context.md +33 -0
- package/docs-src/api/type-aliases/ContextType.md +19 -0
- package/docs-src/api/type-aliases/DangerouslySetInnerHTMLOptions.md +27 -0
- package/docs-src/api/type-aliases/DiffResult.md +61 -0
- package/docs-src/api/type-aliases/Effect.md +35 -0
- package/docs-src/api/type-aliases/EffectCallback.md +23 -0
- package/docs-src/api/type-aliases/Effects.md +21 -0
- package/docs-src/api/type-aliases/ElementEffects.md +21 -0
- package/docs-src/api/type-aliases/ElementFromKey.md +21 -0
- package/docs-src/api/type-aliases/ElementQueries.md +27 -0
- package/docs-src/api/type-aliases/ElementUpdater.md +131 -0
- package/docs-src/api/type-aliases/EventHandler.md +31 -0
- package/docs-src/api/type-aliases/EventType.md +17 -0
- package/docs-src/api/type-aliases/Fallback.md +21 -0
- package/docs-src/api/type-aliases/Initializers.md +21 -0
- package/docs-src/api/type-aliases/LooseReader.md +31 -0
- package/docs-src/api/type-aliases/MatchHandlers.md +77 -0
- package/docs-src/api/type-aliases/MaybeCleanup.md +23 -0
- package/docs-src/api/type-aliases/MaybeSignal.md +17 -0
- package/docs-src/api/type-aliases/Parser.md +39 -0
- package/docs-src/api/type-aliases/ParserOrFallback.md +21 -0
- package/docs-src/api/type-aliases/PassedProp.md +25 -0
- package/docs-src/api/type-aliases/PassedProps.md +21 -0
- package/docs-src/api/type-aliases/Reactive.md +25 -0
- package/docs-src/api/type-aliases/Reader.md +31 -0
- package/docs-src/api/type-aliases/ReservedWords.md +11 -0
- package/docs-src/api/type-aliases/ResolveResult.md +29 -0
- package/docs-src/api/type-aliases/SensorEvents.md +25 -0
- package/docs-src/api/type-aliases/Signal.md +41 -0
- package/docs-src/api/type-aliases/State.md +85 -0
- package/docs-src/api/type-aliases/Store.md +29 -0
- package/docs-src/api/type-aliases/UI.md +11 -0
- package/docs-src/api/type-aliases/UnknownContext.md +13 -0
- package/docs-src/api/variables/CONTEXT_REQUEST.md +11 -0
- package/docs-src/api/variables/UNSET.md +23 -0
- package/docs-src/api/variables/batch.md +25 -0
- package/docs-src/api/variables/createComputed.md +41 -0
- package/docs-src/api/variables/createEffect.md +35 -0
- package/docs-src/api/variables/createState.md +37 -0
- package/docs-src/api/variables/createStore.md +42 -0
- package/docs-src/api/variables/diff.md +43 -0
- package/docs-src/api/variables/isAbortError.md +33 -0
- package/docs-src/api/variables/isAsyncFunction.md +39 -0
- package/docs-src/api/variables/isComputed.md +37 -0
- package/docs-src/api/variables/isEqual.md +49 -0
- package/docs-src/api/variables/isFunction.md +39 -0
- package/docs-src/api/variables/isMutableSignal.md +37 -0
- package/docs-src/api/variables/isNumber.md +33 -0
- package/docs-src/api/variables/isRecord.md +39 -0
- package/docs-src/api/variables/isRecordOrArray.md +39 -0
- package/docs-src/api/variables/isSignal.md +37 -0
- package/docs-src/api/variables/isState.md +37 -0
- package/docs-src/api/variables/isStore.md +37 -0
- package/docs-src/api/variables/isString.md +33 -0
- package/docs-src/api/variables/isSymbol.md +33 -0
- package/docs-src/api/variables/toError.md +33 -0
- package/docs-src/api/variables/valueString.md +33 -0
- package/docs-src/includes/menu.html +44 -0
- package/docs-src/pages/about.md +89 -0
- package/docs-src/pages/components.md +437 -0
- package/docs-src/pages/data-flow.md +449 -0
- package/docs-src/pages/getting-started.md +170 -0
- package/docs-src/pages/index.md +98 -0
- package/docs-src/pages/styling.md +165 -0
- package/eslint.config.js +64 -0
- package/examples/_common/clear.ts +49 -0
- package/examples/_common/fetch.ts +160 -0
- package/examples/_common/focus.ts +45 -0
- package/examples/_common/highlight.ts +5 -0
- package/examples/_global.css +463 -0
- package/examples/basic-button/basic-button.css +176 -0
- package/examples/basic-button/basic-button.html +46 -0
- package/examples/basic-button/basic-button.spec.ts +160 -0
- package/examples/basic-button/basic-button.ts +45 -0
- package/examples/basic-button/copyToClipboard.ts +37 -0
- package/examples/basic-counter/basic-counter.css +21 -0
- package/examples/basic-counter/basic-counter.html +24 -0
- package/examples/basic-counter/basic-counter.spec.ts +85 -0
- package/examples/basic-counter/basic-counter.ts +43 -0
- package/examples/basic-hello/basic-hello.html +34 -0
- package/examples/basic-hello/basic-hello.spec.ts +110 -0
- package/examples/basic-hello/basic-hello.ts +36 -0
- package/examples/basic-number/basic-number.html +79 -0
- package/examples/basic-number/basic-number.spec.ts +175 -0
- package/examples/basic-number/basic-number.ts +124 -0
- package/examples/basic-pluralize/basic-pluralize.html +64 -0
- package/examples/basic-pluralize/basic-pluralize.spec.ts +258 -0
- package/examples/basic-pluralize/basic-pluralize.ts +82 -0
- package/examples/card-callout/card-callout.css +79 -0
- package/examples/card-callout/card-callout.html +5 -0
- package/examples/card-mediaqueries/card-mediaqueries.html +29 -0
- package/examples/card-mediaqueries/card-mediaqueries.spec.ts +300 -0
- package/examples/card-mediaqueries/card-mediaqueries.ts +41 -0
- package/examples/context-media/context-media.html +3 -0
- package/examples/context-media/context-media.ts +127 -0
- package/examples/form-checkbox/form-checkbox.css +70 -0
- package/examples/form-checkbox/form-checkbox.html +13 -0
- package/examples/form-checkbox/form-checkbox.spec.ts +357 -0
- package/examples/form-checkbox/form-checkbox.ts +50 -0
- package/examples/form-checkbox/vanilla-checkbox.ts +101 -0
- package/examples/form-combobox/form-combobox.css +118 -0
- package/examples/form-combobox/form-combobox.html +74 -0
- package/examples/form-combobox/form-combobox.spec.ts +977 -0
- package/examples/form-combobox/form-combobox.ts +128 -0
- package/examples/form-listbox/form-listbox.css +71 -0
- package/examples/form-listbox/form-listbox.html +67 -0
- package/examples/form-listbox/form-listbox.spec.ts +1050 -0
- package/examples/form-listbox/form-listbox.ts +196 -0
- package/examples/form-listbox/mocks/timezones.json +495 -0
- package/examples/form-radiogroup/form-radiogroup.css +87 -0
- package/examples/form-radiogroup/form-radiogroup.html +51 -0
- package/examples/form-radiogroup/form-radiogroup.spec.ts +515 -0
- package/examples/form-radiogroup/form-radiogroup.ts +58 -0
- package/examples/form-spinbutton/form-spinbutton.css +95 -0
- package/examples/form-spinbutton/form-spinbutton.html +96 -0
- package/examples/form-spinbutton/form-spinbutton.spec.ts +688 -0
- package/examples/form-spinbutton/form-spinbutton.ts +111 -0
- package/examples/form-textbox/form-textbox.css +104 -0
- package/examples/form-textbox/form-textbox.html +53 -0
- package/examples/form-textbox/form-textbox.spec.ts +542 -0
- package/examples/form-textbox/form-textbox.ts +104 -0
- package/examples/main.css +22 -0
- package/examples/main.ts +23 -0
- package/examples/module-carousel/module-carousel.css +113 -0
- package/examples/module-carousel/module-carousel.html +208 -0
- package/examples/module-carousel/module-carousel.spec.ts +523 -0
- package/examples/module-carousel/module-carousel.ts +131 -0
- package/examples/module-catalog/module-catalog.css +22 -0
- package/examples/module-catalog/module-catalog.html +82 -0
- package/examples/module-catalog/module-catalog.spec.ts +396 -0
- package/examples/module-catalog/module-catalog.ts +37 -0
- package/examples/module-codeblock/module-codeblock.css +95 -0
- package/examples/module-codeblock/module-codeblock.html +28 -0
- package/examples/module-codeblock/module-codeblock.ts +47 -0
- package/examples/module-demo/module-demo.css +13 -0
- package/examples/module-dialog/module-dialog.css +96 -0
- package/examples/module-dialog/module-dialog.html +66 -0
- package/examples/module-dialog/module-dialog.spec.ts +557 -0
- package/examples/module-dialog/module-dialog.ts +81 -0
- package/examples/module-lazyload/mocks/empty.html +1 -0
- package/examples/module-lazyload/mocks/module-with-type.html +27 -0
- package/examples/module-lazyload/mocks/nested-components.html +52 -0
- package/examples/module-lazyload/mocks/recursive.html +20 -0
- package/examples/module-lazyload/mocks/simple-text.html +3 -0
- package/examples/module-lazyload/mocks/snippet.html +57 -0
- package/examples/module-lazyload/mocks/with-styles.html +39 -0
- package/examples/module-lazyload/module-lazyload.html +132 -0
- package/examples/module-lazyload/module-lazyload.spec.ts +734 -0
- package/examples/module-lazyload/module-lazyload.ts +89 -0
- package/examples/module-list/module-list.html +30 -0
- package/examples/module-list/module-list.spec.ts +592 -0
- package/examples/module-list/module-list.ts +99 -0
- package/examples/module-pagination/module-pagination.css +79 -0
- package/examples/module-pagination/module-pagination.html +16 -0
- package/examples/module-pagination/module-pagination.spec.ts +701 -0
- package/examples/module-pagination/module-pagination.ts +88 -0
- package/examples/module-scrollarea/module-scrollarea.css +77 -0
- package/examples/module-scrollarea/module-scrollarea.html +189 -0
- package/examples/module-scrollarea/module-scrollarea.spec.ts +445 -0
- package/examples/module-scrollarea/module-scrollarea.ts +81 -0
- package/examples/module-tabgroup/module-tabgroup.css +55 -0
- package/examples/module-tabgroup/module-tabgroup.html +269 -0
- package/examples/module-tabgroup/module-tabgroup.spec.ts +631 -0
- package/examples/module-tabgroup/module-tabgroup.ts +102 -0
- package/examples/module-toc/module-toc.css +34 -0
- package/examples/module-todo/module-todo.css +84 -0
- package/examples/module-todo/module-todo.html +92 -0
- package/examples/module-todo/module-todo.spec.ts +528 -0
- package/examples/module-todo/module-todo.ts +91 -0
- package/examples/section-hero/section-hero.css +37 -0
- package/examples/section-menu/section-menu.css +81 -0
- package/examples/server.ts +95 -0
- package/examples/test-setup.md +314 -0
- package/index.dev.js +1688 -0
- package/index.dev.ts +127 -0
- package/index.js +3 -0
- package/index.js.map +42 -0
- package/index.ts +127 -0
- package/package.json +64 -0
- package/playwright.config.ts +31 -0
- package/server/BUILD_SYSTEM.md +428 -0
- package/server/SERVER.md +286 -0
- package/server/build.ts +91 -0
- package/server/config.ts +130 -0
- package/server/effects/api.ts +28 -0
- package/server/effects/css.ts +31 -0
- package/server/effects/examples.ts +109 -0
- package/server/effects/js.ts +32 -0
- package/server/effects/menu.ts +34 -0
- package/server/effects/pages.ts +178 -0
- package/server/effects/service-worker.ts +57 -0
- package/server/effects/sitemap.ts +27 -0
- package/server/file-signals.ts +361 -0
- package/server/file-watcher.ts +77 -0
- package/server/io.ts +174 -0
- package/server/layout-engine.ts +470 -0
- package/server/layout-utils.ts +615 -0
- package/server/layouts/api.html +76 -0
- package/server/layouts/base.html +37 -0
- package/server/layouts/blog.html +115 -0
- package/server/layouts/example.html +104 -0
- package/server/layouts/overview.html +165 -0
- package/server/layouts/page.html +36 -0
- package/server/layouts/test.html +24 -0
- package/server/markdoc-helpers.ts +217 -0
- package/server/markdoc.config.ts +29 -0
- package/server/schema/callout.markdoc.ts +17 -0
- package/server/schema/carousel.markdoc.ts +118 -0
- package/server/schema/demo.markdoc.ts +74 -0
- package/server/schema/fence.markdoc.ts +84 -0
- package/server/schema/heading.markdoc.ts +23 -0
- package/server/schema/hero.markdoc.ts +59 -0
- package/server/schema/section.markdoc.ts +10 -0
- package/server/schema/slide.markdoc.ts +17 -0
- package/server/schema/source.markdoc.ts +53 -0
- package/server/schema/tabgroup.markdoc.ts +102 -0
- package/server/serve.ts +635 -0
- package/server/templates/README.md +352 -0
- package/server/templates/constants.ts +236 -0
- package/server/templates/fragments.ts +159 -0
- package/server/templates/hmr.ts +269 -0
- package/server/templates/menu.ts +33 -0
- package/server/templates/performance-hints.ts +94 -0
- package/server/templates/service-worker.ts +403 -0
- package/server/templates/sitemap.ts +57 -0
- package/server/templates/toc.ts +41 -0
- package/server/templates/utils.ts +378 -0
- package/src/component.ts +215 -0
- package/src/context.ts +156 -0
- package/src/effects/attribute.ts +82 -0
- package/src/effects/class.ts +28 -0
- package/src/effects/event.ts +67 -0
- package/src/effects/html.ts +60 -0
- package/src/effects/method.ts +57 -0
- package/src/effects/pass.ts +103 -0
- package/src/effects/property.ts +57 -0
- package/src/effects/style.ts +34 -0
- package/src/effects/text.ts +28 -0
- package/src/effects.ts +412 -0
- package/src/errors.ts +160 -0
- package/src/parsers/boolean.ts +14 -0
- package/src/parsers/json.ts +33 -0
- package/src/parsers/number.ts +55 -0
- package/src/parsers/string.ts +32 -0
- package/src/parsers.ts +90 -0
- package/src/scheduler.ts +47 -0
- package/src/signals/collection.ts +253 -0
- package/src/signals/sensor.ts +131 -0
- package/src/ui.ts +236 -0
- package/src/util.ts +187 -0
- package/tsconfig.json +34 -0
- package/types/examples/basic-button/basic-button.d.ts +16 -0
- package/types/examples/basic-hello/basic-hello.d.ts +18 -0
- package/types/index.d.ts +27 -0
- package/types/index.dev.d.ts +27 -0
- package/types/src/collection.d.ts +27 -0
- package/types/src/component.d.ts +32 -0
- package/types/src/context.d.ts +85 -0
- package/types/src/effects/attribute.d.ts +23 -0
- package/types/src/effects/callMethod.d.ts +23 -0
- package/types/src/effects/class.d.ts +13 -0
- package/types/src/effects/dangerouslySetInnerHTML.d.ts +18 -0
- package/types/src/effects/event.d.ts +18 -0
- package/types/src/effects/html.d.ts +17 -0
- package/types/src/effects/method.d.ts +22 -0
- package/types/src/effects/pass.d.ts +18 -0
- package/types/src/effects/property.d.ts +22 -0
- package/types/src/effects/setAttribute.d.ts +24 -0
- package/types/src/effects/setProperty.d.ts +23 -0
- package/types/src/effects/setStyle.d.ts +14 -0
- package/types/src/effects/setText.d.ts +13 -0
- package/types/src/effects/style.d.ts +13 -0
- package/types/src/effects/text.d.ts +12 -0
- package/types/src/effects/toggleClass.d.ts +14 -0
- package/types/src/effects.d.ts +153 -0
- package/types/src/errors.d.ts +99 -0
- package/types/src/events.d.ts +27 -0
- package/types/src/extractors.d.ts +23 -0
- package/types/src/parsers/boolean.d.ts +10 -0
- package/types/src/parsers/json.d.ts +13 -0
- package/types/src/parsers/number.d.ts +21 -0
- package/types/src/parsers/string.d.ts +19 -0
- package/types/src/parsers.d.ts +41 -0
- package/types/src/scheduler.d.ts +11 -0
- package/types/src/sensor.d.ts +27 -0
- package/types/src/signals/collection.d.ts +32 -0
- package/types/src/signals/sensor.d.ts +27 -0
- package/types/src/ui.d.ts +37 -0
- package/types/src/util.d.ts +65 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test Suite: form-listbox Component
|
|
5
|
+
*
|
|
6
|
+
* Comprehensive tests for the Le Truc form-listbox component, which provides
|
|
7
|
+
* a filterable listbox with keyboard navigation and remote data loading.
|
|
8
|
+
*
|
|
9
|
+
* Key Features Tested:
|
|
10
|
+
* - ✅ Basic rendering and initialization
|
|
11
|
+
* - ✅ Remote JSON data loading (both flat arrays and grouped objects)
|
|
12
|
+
* - ✅ Loading states and error handling
|
|
13
|
+
* - ✅ Option selection and value management
|
|
14
|
+
* - ✅ Keyboard navigation (arrow keys, Enter, Escape)
|
|
15
|
+
* - ✅ Filtering functionality
|
|
16
|
+
* - ✅ Focus management and accessibility
|
|
17
|
+
* - ✅ Event emission on value changes
|
|
18
|
+
* - ✅ Dynamic src and property updates
|
|
19
|
+
* - ✅ Form integration and data submission
|
|
20
|
+
* - ✅ Edge cases and performance scenarios
|
|
21
|
+
*
|
|
22
|
+
* Architecture Notes:
|
|
23
|
+
* - Uses `asString` parser for src URL validation
|
|
24
|
+
* - Implements `fetchWithCache` for HTTP request caching
|
|
25
|
+
* - Uses Collections for reactive option tracking
|
|
26
|
+
* - Manages focus via `manageFocus` utility
|
|
27
|
+
* - Renders content via `dangerouslySetInnerHTML`
|
|
28
|
+
* - Emits custom 'form-listbox.change' events
|
|
29
|
+
*
|
|
30
|
+
* Test Coverage: 80+ comprehensive test cases covering all major functionality,
|
|
31
|
+
* error states, accessibility features, and edge cases. Tests validate both
|
|
32
|
+
* user interactions and programmatic property changes following Le Truc's
|
|
33
|
+
* reactive property model.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
test.describe('form-listbox component', () => {
|
|
37
|
+
test.beforeEach(async ({ page }) => {
|
|
38
|
+
page.on('console', msg => {
|
|
39
|
+
console.log(`[browser] ${msg.type()}: ${msg.text()}`)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
await page.goto('http://localhost:3000/test/form-listbox.html')
|
|
43
|
+
await page.waitForSelector('form-listbox')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test.describe('Basic Rendering and Initialization', () => {
|
|
47
|
+
test('loads and displays timezone data successfully', async ({ page }) => {
|
|
48
|
+
const listbox = page.locator('form-listbox').first()
|
|
49
|
+
const loading = listbox.locator('.loading')
|
|
50
|
+
const error = listbox.locator('.error')
|
|
51
|
+
const callout = listbox.locator('card-callout')
|
|
52
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
53
|
+
|
|
54
|
+
// Wait for content to load
|
|
55
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
56
|
+
|
|
57
|
+
// After loading, should hide loading and error states
|
|
58
|
+
await expect(loading).toBeHidden()
|
|
59
|
+
await expect(error).toBeHidden()
|
|
60
|
+
await expect(callout).toBeHidden()
|
|
61
|
+
|
|
62
|
+
// Should have loaded timezone options
|
|
63
|
+
const options = listbox.locator('button[role="option"]')
|
|
64
|
+
const optionCount = await options.count()
|
|
65
|
+
expect(optionCount).toBeGreaterThan(100) // Should have many timezone options
|
|
66
|
+
|
|
67
|
+
// Should have grouped content
|
|
68
|
+
const groups = listbox.locator('[role="group"]')
|
|
69
|
+
const groupCount = await groups.count()
|
|
70
|
+
expect(groupCount).toBeGreaterThan(5) // Should have continent groups
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('displays grouped timezone data with proper structure', async ({
|
|
74
|
+
page,
|
|
75
|
+
}) => {
|
|
76
|
+
const listbox = page.locator('form-listbox').first()
|
|
77
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
78
|
+
|
|
79
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
80
|
+
|
|
81
|
+
// Check for Africa group
|
|
82
|
+
const africaGroup = listbox.locator('[role="group"]').first()
|
|
83
|
+
await expect(africaGroup).toBeVisible()
|
|
84
|
+
|
|
85
|
+
const africaLabel = africaGroup.locator('[role="presentation"]').first()
|
|
86
|
+
await expect(africaLabel).toContainText('Africa')
|
|
87
|
+
|
|
88
|
+
// Check for options within Africa group
|
|
89
|
+
const africaOptions = africaGroup.locator('button[role="option"]')
|
|
90
|
+
const africaCount = await africaOptions.count()
|
|
91
|
+
expect(africaCount).toBeGreaterThan(10)
|
|
92
|
+
|
|
93
|
+
// Verify specific African cities
|
|
94
|
+
await expect(africaOptions).toContainText(['Abidjan', 'Cairo', 'Lagos'])
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test.describe('Error Handling', () => {
|
|
99
|
+
test('shows error for invalid URL', async ({ page }) => {
|
|
100
|
+
// Add a listbox with invalid src (using an actual invalid URL format)
|
|
101
|
+
await page.evaluate(() => {
|
|
102
|
+
const listbox = document.createElement('form-listbox')
|
|
103
|
+
listbox.setAttribute('src', 'ftp://invalid-protocol.example')
|
|
104
|
+
listbox.innerHTML = `
|
|
105
|
+
<input type="hidden" name="timezone" />
|
|
106
|
+
<card-callout>
|
|
107
|
+
<p class="loading" role="status">Loading...</p>
|
|
108
|
+
<p class="error" role="alert" aria-live="assertive" hidden></p>
|
|
109
|
+
</card-callout>
|
|
110
|
+
<module-scrollarea orientation="vertical">
|
|
111
|
+
<div role="listbox" aria-label="Test" hidden></div>
|
|
112
|
+
</module-scrollarea>
|
|
113
|
+
`
|
|
114
|
+
document.body.appendChild(listbox)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const invalidListbox = page.locator('form-listbox').last()
|
|
118
|
+
const loading = invalidListbox.locator('.loading')
|
|
119
|
+
const error = invalidListbox.locator('.error')
|
|
120
|
+
const callout = invalidListbox.locator('card-callout')
|
|
121
|
+
|
|
122
|
+
await expect(callout).toBeVisible({ timeout: 1000 })
|
|
123
|
+
await expect(error).toBeVisible()
|
|
124
|
+
await expect(loading).toBeHidden()
|
|
125
|
+
await expect(callout).toHaveClass(/danger/)
|
|
126
|
+
|
|
127
|
+
const errorText = await error.textContent()
|
|
128
|
+
expect(errorText).toContain('Invalid URL')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('shows error for 404 not found', async ({ page }) => {
|
|
132
|
+
await page.evaluate(() => {
|
|
133
|
+
const listbox = document.createElement('form-listbox')
|
|
134
|
+
listbox.setAttribute('src', '/nonexistent-file.json')
|
|
135
|
+
listbox.innerHTML = `
|
|
136
|
+
<input type="hidden" name="timezone" />
|
|
137
|
+
<card-callout>
|
|
138
|
+
<p class="loading" role="status">Loading...</p>
|
|
139
|
+
<p class="error" role="alert" aria-live="assertive" hidden></p>
|
|
140
|
+
</card-callout>
|
|
141
|
+
<module-scrollarea orientation="vertical">
|
|
142
|
+
<div role="listbox" aria-label="Test" hidden></div>
|
|
143
|
+
</module-scrollarea>
|
|
144
|
+
`
|
|
145
|
+
document.body.appendChild(listbox)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const notFoundListbox = page.locator('form-listbox').last()
|
|
149
|
+
const error = notFoundListbox.locator('.error')
|
|
150
|
+
const callout = notFoundListbox.locator('card-callout')
|
|
151
|
+
|
|
152
|
+
await expect(error).toBeVisible({ timeout: 1000 })
|
|
153
|
+
await expect(callout).toHaveClass(/danger/)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('handles component without src attribute correctly', async ({
|
|
157
|
+
page,
|
|
158
|
+
}) => {
|
|
159
|
+
await page.evaluate(() => {
|
|
160
|
+
const listbox = document.createElement('form-listbox')
|
|
161
|
+
listbox.innerHTML = `
|
|
162
|
+
<input type="hidden" name="timezone" />
|
|
163
|
+
<div role="listbox" aria-label="Test">
|
|
164
|
+
<button type="button" role="option" tabindex="-1" value="test">Test Option</button>
|
|
165
|
+
</div>
|
|
166
|
+
`
|
|
167
|
+
document.body.appendChild(listbox)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const noSrcListbox = page.locator('form-listbox').last()
|
|
171
|
+
const listboxElement = noSrcListbox.locator('[role="listbox"]')
|
|
172
|
+
const options = listboxElement.locator('button[role="option"]')
|
|
173
|
+
|
|
174
|
+
// Should show listbox and options when no src but inline HTML exists
|
|
175
|
+
await expect(listboxElement).toBeVisible()
|
|
176
|
+
await expect(options).toHaveCount(1)
|
|
177
|
+
|
|
178
|
+
// Should not show any error callout
|
|
179
|
+
const callout = noSrcListbox.locator('card-callout')
|
|
180
|
+
if ((await callout.count()) > 0) {
|
|
181
|
+
await expect(callout).toBeHidden()
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test.describe('Option Selection and Value Management', () => {
|
|
187
|
+
test('selects option when clicked', async ({ page }) => {
|
|
188
|
+
const listbox = page.locator('form-listbox').first()
|
|
189
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
190
|
+
|
|
191
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
192
|
+
|
|
193
|
+
// Find and click an option
|
|
194
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
195
|
+
await expect(firstOption).toBeVisible()
|
|
196
|
+
|
|
197
|
+
const optionValue = await firstOption.getAttribute('value')
|
|
198
|
+
|
|
199
|
+
await firstOption.click()
|
|
200
|
+
|
|
201
|
+
// Verify selection
|
|
202
|
+
await expect(firstOption).toHaveAttribute('aria-selected', 'true')
|
|
203
|
+
await expect(firstOption).toHaveAttribute('tabindex', '0')
|
|
204
|
+
|
|
205
|
+
// Check component value property
|
|
206
|
+
const componentValue = await page.evaluate(() => {
|
|
207
|
+
const element = document.querySelector('form-listbox') as any
|
|
208
|
+
return element.value
|
|
209
|
+
})
|
|
210
|
+
expect(componentValue).toBe(optionValue)
|
|
211
|
+
|
|
212
|
+
// Check value attribute on host element
|
|
213
|
+
await expect(listbox).toHaveAttribute('value', optionValue || '')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('updates selection when value property is changed programmatically', async ({
|
|
217
|
+
page,
|
|
218
|
+
}) => {
|
|
219
|
+
const listbox = page.locator('form-listbox').first()
|
|
220
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
221
|
+
timeout: 1000,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Get a specific option value
|
|
225
|
+
const targetOption = listbox.locator('button[role="option"]').nth(5)
|
|
226
|
+
const targetValue = await targetOption.getAttribute('value')
|
|
227
|
+
|
|
228
|
+
// Set value programmatically
|
|
229
|
+
await page.evaluate(value => {
|
|
230
|
+
const element = document.querySelector('form-listbox') as any
|
|
231
|
+
element.value = value
|
|
232
|
+
}, targetValue)
|
|
233
|
+
|
|
234
|
+
// Verify the option is now selected
|
|
235
|
+
await expect(targetOption).toHaveAttribute('aria-selected', 'true')
|
|
236
|
+
await expect(targetOption).toHaveAttribute('tabindex', '0')
|
|
237
|
+
await expect(listbox).toHaveAttribute('value', targetValue || '')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('deselects previous option when new option is selected', async ({
|
|
241
|
+
page,
|
|
242
|
+
}) => {
|
|
243
|
+
const listbox = page.locator('form-listbox').first()
|
|
244
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
245
|
+
timeout: 1000,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
249
|
+
const secondOption = listbox.locator('button[role="option"]').nth(1)
|
|
250
|
+
|
|
251
|
+
// Select first option
|
|
252
|
+
await firstOption.click()
|
|
253
|
+
await expect(firstOption).toHaveAttribute('aria-selected', 'true')
|
|
254
|
+
await expect(firstOption).toHaveAttribute('tabindex', '0')
|
|
255
|
+
|
|
256
|
+
// Select second option
|
|
257
|
+
await secondOption.click()
|
|
258
|
+
await expect(secondOption).toHaveAttribute('aria-selected', 'true')
|
|
259
|
+
await expect(secondOption).toHaveAttribute('tabindex', '0')
|
|
260
|
+
|
|
261
|
+
// First option should be deselected
|
|
262
|
+
await expect(firstOption).toHaveAttribute('aria-selected', 'false')
|
|
263
|
+
await expect(firstOption).toHaveAttribute('tabindex', '-1')
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test.describe('Keyboard Navigation', () => {
|
|
268
|
+
test('focuses first option when listbox receives focus', async ({
|
|
269
|
+
page,
|
|
270
|
+
}) => {
|
|
271
|
+
const listbox = page.locator('form-listbox').first()
|
|
272
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
273
|
+
|
|
274
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
275
|
+
|
|
276
|
+
// First select an option to set up initial focus management
|
|
277
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
278
|
+
await firstOption.click()
|
|
279
|
+
|
|
280
|
+
// Now test focus behavior
|
|
281
|
+
await listboxElement.focus()
|
|
282
|
+
await expect(firstOption).toBeFocused()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('navigates options with arrow keys', async ({ page }) => {
|
|
286
|
+
const listbox = page.locator('form-listbox').first()
|
|
287
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
288
|
+
|
|
289
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
290
|
+
|
|
291
|
+
// First select an option to establish focus management
|
|
292
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
293
|
+
const secondOption = listbox.locator('button[role="option"]').nth(1)
|
|
294
|
+
|
|
295
|
+
await firstOption.click()
|
|
296
|
+
await listboxElement.focus()
|
|
297
|
+
await expect(firstOption).toBeFocused()
|
|
298
|
+
|
|
299
|
+
// Navigate down
|
|
300
|
+
await page.keyboard.press('ArrowDown')
|
|
301
|
+
await expect(secondOption).toBeFocused()
|
|
302
|
+
|
|
303
|
+
// Navigate back up
|
|
304
|
+
await page.keyboard.press('ArrowUp')
|
|
305
|
+
await expect(firstOption).toBeFocused()
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('selects focused option with Enter key', async ({ page }) => {
|
|
309
|
+
const listbox = page.locator('form-listbox').first()
|
|
310
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
311
|
+
|
|
312
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
313
|
+
|
|
314
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
315
|
+
const firstValue = await firstOption.getAttribute('value')
|
|
316
|
+
|
|
317
|
+
// First click to establish focus, then clear selection
|
|
318
|
+
await firstOption.click()
|
|
319
|
+
await page.evaluate(() => {
|
|
320
|
+
const element = document.querySelector('form-listbox') as any
|
|
321
|
+
element.value = ''
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
await listboxElement.focus()
|
|
325
|
+
await expect(firstOption).toBeFocused()
|
|
326
|
+
|
|
327
|
+
// Press Enter to select
|
|
328
|
+
await page.keyboard.press('Enter')
|
|
329
|
+
|
|
330
|
+
await expect(firstOption).toHaveAttribute('aria-selected', 'true')
|
|
331
|
+
await expect(listbox).toHaveAttribute('value', firstValue || '')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('wraps navigation at boundaries', async ({ page }) => {
|
|
335
|
+
const listbox = page.locator('form-listbox').first()
|
|
336
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
337
|
+
|
|
338
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
339
|
+
|
|
340
|
+
// First establish focus on first option
|
|
341
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
342
|
+
await firstOption.click()
|
|
343
|
+
await listboxElement.focus()
|
|
344
|
+
|
|
345
|
+
// Navigate up from first option should wrap to last
|
|
346
|
+
await page.keyboard.press('ArrowUp')
|
|
347
|
+
|
|
348
|
+
const lastOptionValue = await page.evaluate(() => {
|
|
349
|
+
const listbox = document.querySelector('form-listbox')
|
|
350
|
+
const options = Array.from(
|
|
351
|
+
listbox?.querySelectorAll('button[role="option"]:not([hidden])')
|
|
352
|
+
|| [],
|
|
353
|
+
)
|
|
354
|
+
return (options[options.length - 1] as HTMLElement)?.getAttribute(
|
|
355
|
+
'value',
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const focusedValue = await page.evaluate(() =>
|
|
360
|
+
(document.activeElement as HTMLElement)?.getAttribute('value'),
|
|
361
|
+
)
|
|
362
|
+
expect(focusedValue).toBe(lastOptionValue)
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test.describe('Filtering Functionality', () => {
|
|
367
|
+
test('filters options based on filter property', async ({ page }) => {
|
|
368
|
+
const listbox = page.locator('form-listbox').first()
|
|
369
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
370
|
+
timeout: 1000,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const allOptions = listbox.locator('button[role="option"]')
|
|
374
|
+
const initialCount = await allOptions.count()
|
|
375
|
+
|
|
376
|
+
// Set filter to show options containing 'af' (should match Africa cities)
|
|
377
|
+
await page.evaluate(() => {
|
|
378
|
+
const element = document.querySelector('form-listbox') as any
|
|
379
|
+
element.filter = 'af'
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const visibleOptions = listbox.locator(
|
|
383
|
+
'button[role="option"]:not([hidden])',
|
|
384
|
+
)
|
|
385
|
+
const visibleCount = await visibleOptions.count()
|
|
386
|
+
|
|
387
|
+
expect(visibleCount).toBeLessThan(initialCount)
|
|
388
|
+
expect(visibleCount).toBeGreaterThan(0)
|
|
389
|
+
|
|
390
|
+
// Verify visible options contain "af" in their text
|
|
391
|
+
for (let i = 0; i < Math.min(visibleCount, 5); i++) {
|
|
392
|
+
const optionText = await visibleOptions.nth(i).textContent()
|
|
393
|
+
expect(optionText?.toLowerCase()).toContain('af')
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('highlights matching text when filter is applied', async ({
|
|
398
|
+
page,
|
|
399
|
+
}) => {
|
|
400
|
+
const listbox = page.locator('form-listbox').first()
|
|
401
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
402
|
+
timeout: 1000,
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// Set filter
|
|
406
|
+
await page.evaluate(() => {
|
|
407
|
+
const element = document.querySelector('form-listbox') as any
|
|
408
|
+
element.filter = 'new'
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const visibleOptions = listbox.locator(
|
|
412
|
+
'button[role="option"]:not([hidden])',
|
|
413
|
+
)
|
|
414
|
+
const visibleCount = await visibleOptions.count()
|
|
415
|
+
|
|
416
|
+
if (visibleCount > 0) {
|
|
417
|
+
const firstVisible = visibleOptions.first()
|
|
418
|
+
const highlightedMark = firstVisible.locator('mark')
|
|
419
|
+
|
|
420
|
+
// Should have highlighted text
|
|
421
|
+
await expect(highlightedMark).toBeVisible()
|
|
422
|
+
const highlightedText = await highlightedMark.textContent()
|
|
423
|
+
expect(highlightedText?.toLowerCase()).toBe('new')
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('shows no options when filter matches nothing', async ({ page }) => {
|
|
428
|
+
const listbox = page.locator('form-listbox').first()
|
|
429
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
430
|
+
timeout: 1000,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// Set filter that won't match anything
|
|
434
|
+
await page.evaluate(() => {
|
|
435
|
+
const element = document.querySelector('form-listbox') as any
|
|
436
|
+
element.filter = 'xyzzyx-nonexistent'
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
const visibleOptions = listbox.locator(
|
|
440
|
+
'button[role="option"]:not([hidden])',
|
|
441
|
+
)
|
|
442
|
+
const visibleCount = await visibleOptions.count()
|
|
443
|
+
|
|
444
|
+
expect(visibleCount).toBe(0)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('clears filter and shows all options', async ({ page }) => {
|
|
448
|
+
const listbox = page.locator('form-listbox').first()
|
|
449
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
450
|
+
timeout: 1000,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const allOptions = listbox.locator('button[role="option"]')
|
|
454
|
+
const initialCount = await allOptions.count()
|
|
455
|
+
|
|
456
|
+
// Apply filter
|
|
457
|
+
await page.evaluate(() => {
|
|
458
|
+
const element = document.querySelector('form-listbox') as any
|
|
459
|
+
element.filter = 'africa'
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const filteredCount = await listbox
|
|
463
|
+
.locator('button[role="option"]:not([hidden])')
|
|
464
|
+
.count()
|
|
465
|
+
expect(filteredCount).toBeLessThan(initialCount)
|
|
466
|
+
|
|
467
|
+
// Clear filter
|
|
468
|
+
await page.evaluate(() => {
|
|
469
|
+
const element = document.querySelector('form-listbox') as any
|
|
470
|
+
element.filter = ''
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
const clearedCount = await listbox
|
|
474
|
+
.locator('button[role="option"]:not([hidden])')
|
|
475
|
+
.count()
|
|
476
|
+
expect(clearedCount).toBe(initialCount)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test.describe('Event Handling', () => {
|
|
481
|
+
test('emits change event when value changes', async ({ page }) => {
|
|
482
|
+
const listbox = page.locator('form-listbox').first()
|
|
483
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
484
|
+
timeout: 1000,
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Set up event listener
|
|
488
|
+
await page.evaluate(() => {
|
|
489
|
+
;(window as any).changeEvents = []
|
|
490
|
+
const element = document.querySelector('form-listbox')
|
|
491
|
+
element?.addEventListener('change', event => {
|
|
492
|
+
;(window as any).changeEvents.push(
|
|
493
|
+
(event.target as HTMLInputElement).value,
|
|
494
|
+
)
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
499
|
+
const optionValue = await firstOption.getAttribute('value')
|
|
500
|
+
|
|
501
|
+
await firstOption.click()
|
|
502
|
+
|
|
503
|
+
// Check that event was fired
|
|
504
|
+
const changeEvents = await page.evaluate(
|
|
505
|
+
() => (window as any).changeEvents,
|
|
506
|
+
)
|
|
507
|
+
expect(changeEvents).toHaveLength(1)
|
|
508
|
+
expect(changeEvents[0]).toBe(optionValue)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('does not emit duplicate events for same value', async ({ page }) => {
|
|
512
|
+
const listbox = page.locator('form-listbox').first()
|
|
513
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
514
|
+
timeout: 1000,
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
// Set up event listener
|
|
518
|
+
await page.evaluate(() => {
|
|
519
|
+
;(window as any).changeEvents = []
|
|
520
|
+
const element = document.querySelector('form-listbox')
|
|
521
|
+
element?.addEventListener('change', event => {
|
|
522
|
+
;(window as any).changeEvents.push(
|
|
523
|
+
(event.target as HTMLInputElement).value,
|
|
524
|
+
)
|
|
525
|
+
})
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
529
|
+
|
|
530
|
+
// Click same option twice
|
|
531
|
+
await firstOption.click()
|
|
532
|
+
await firstOption.click()
|
|
533
|
+
|
|
534
|
+
// Should only have one event
|
|
535
|
+
const changeEvents = await page.evaluate(
|
|
536
|
+
() => (window as any).changeEvents,
|
|
537
|
+
)
|
|
538
|
+
expect(changeEvents).toHaveLength(1)
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test.describe('Dynamic Behavior', () => {
|
|
543
|
+
test('updates content when src property changes', async ({ page }) => {
|
|
544
|
+
// Create a simple JSON endpoint response simulation
|
|
545
|
+
await page.route('**/simple-options.json', route => {
|
|
546
|
+
route.fulfill({
|
|
547
|
+
status: 200,
|
|
548
|
+
contentType: 'application/json',
|
|
549
|
+
body: JSON.stringify([
|
|
550
|
+
{ value: 'opt1', label: 'Option 1' },
|
|
551
|
+
{ value: 'opt2', label: 'Option 2' },
|
|
552
|
+
{ value: 'opt3', label: 'Option 3' },
|
|
553
|
+
]),
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const listbox = page.locator('form-listbox').first()
|
|
558
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
559
|
+
timeout: 1000,
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
const initialOptions = listbox.locator('button[role="option"]')
|
|
563
|
+
const initialCount = await initialOptions.count()
|
|
564
|
+
|
|
565
|
+
// Change src to simple options
|
|
566
|
+
await page.evaluate(() => {
|
|
567
|
+
const element = document.querySelector('form-listbox') as any
|
|
568
|
+
element.src = '/simple-options.json'
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// Wait for new content to load
|
|
572
|
+
await page.waitForTimeout(50)
|
|
573
|
+
|
|
574
|
+
const newOptions = listbox.locator('button[role="option"]')
|
|
575
|
+
const newCount = await newOptions.count()
|
|
576
|
+
|
|
577
|
+
expect(newCount).toBe(3)
|
|
578
|
+
expect(newCount).not.toBe(initialCount)
|
|
579
|
+
|
|
580
|
+
await expect(newOptions).toContainText([
|
|
581
|
+
'Option 1',
|
|
582
|
+
'Option 2',
|
|
583
|
+
'Option 3',
|
|
584
|
+
])
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
test('handles src property validation', async ({ page }) => {
|
|
588
|
+
const listbox = page.locator('form-listbox').first()
|
|
589
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
590
|
+
timeout: 1000,
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
// Set invalid src
|
|
594
|
+
await page.evaluate(() => {
|
|
595
|
+
const element = document.querySelector('form-listbox') as any
|
|
596
|
+
element.src = 'invalid-url'
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
const error = listbox.locator('.error')
|
|
600
|
+
const callout = listbox.locator('card-callout')
|
|
601
|
+
|
|
602
|
+
await expect(error).toBeVisible({ timeout: 1000 })
|
|
603
|
+
await expect(callout).toHaveClass(/danger/)
|
|
604
|
+
})
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
test.describe('Accessibility Features', () => {
|
|
608
|
+
test('maintains proper ARIA attributes', async ({ page }) => {
|
|
609
|
+
const listbox = page.locator('form-listbox').first()
|
|
610
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
611
|
+
|
|
612
|
+
await expect(listboxElement).toBeVisible({ timeout: 1000 })
|
|
613
|
+
|
|
614
|
+
// Verify listbox role and aria-label
|
|
615
|
+
await expect(listboxElement).toHaveAttribute('role', 'listbox')
|
|
616
|
+
await expect(listboxElement).toHaveAttribute('aria-label')
|
|
617
|
+
|
|
618
|
+
// Verify option roles and aria-selected attributes
|
|
619
|
+
const options = listbox.locator('button[role="option"]')
|
|
620
|
+
const firstOption = options.first()
|
|
621
|
+
|
|
622
|
+
await expect(firstOption).toHaveAttribute('role', 'option')
|
|
623
|
+
await expect(firstOption).toHaveAttribute('aria-selected', 'false')
|
|
624
|
+
|
|
625
|
+
await firstOption.click()
|
|
626
|
+
await expect(firstOption).toHaveAttribute('aria-selected', 'true')
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
test('manages tabindex correctly for keyboard navigation', async ({
|
|
630
|
+
page,
|
|
631
|
+
}) => {
|
|
632
|
+
const listbox = page.locator('form-listbox').first()
|
|
633
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
634
|
+
timeout: 1000,
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
const options = listbox.locator('button[role="option"]')
|
|
638
|
+
const firstOption = options.first()
|
|
639
|
+
const secondOption = options.nth(1)
|
|
640
|
+
|
|
641
|
+
// Initially no option should have tabindex="0"
|
|
642
|
+
await expect(firstOption).toHaveAttribute('tabindex', '-1')
|
|
643
|
+
await expect(secondOption).toHaveAttribute('tabindex', '-1')
|
|
644
|
+
|
|
645
|
+
// Select first option
|
|
646
|
+
await firstOption.click()
|
|
647
|
+
|
|
648
|
+
// Selected option should have tabindex="0"
|
|
649
|
+
await expect(firstOption).toHaveAttribute('tabindex', '0')
|
|
650
|
+
await expect(secondOption).toHaveAttribute('tabindex', '-1')
|
|
651
|
+
|
|
652
|
+
// Select second option
|
|
653
|
+
await secondOption.click()
|
|
654
|
+
|
|
655
|
+
// Tabindex should move to second option
|
|
656
|
+
await expect(firstOption).toHaveAttribute('tabindex', '-1')
|
|
657
|
+
await expect(secondOption).toHaveAttribute('tabindex', '0')
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
test('provides proper live region updates', async ({ page }) => {
|
|
661
|
+
const listbox = page.locator('form-listbox').first()
|
|
662
|
+
|
|
663
|
+
// Check error live region
|
|
664
|
+
const error = listbox.locator('.error')
|
|
665
|
+
await expect(error).toHaveAttribute('role', 'alert')
|
|
666
|
+
await expect(error).toHaveAttribute('aria-live', 'assertive')
|
|
667
|
+
|
|
668
|
+
// Check loading status
|
|
669
|
+
const loading = listbox.locator('.loading')
|
|
670
|
+
await expect(loading).toHaveAttribute('role', 'status')
|
|
671
|
+
})
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
test.describe('Form Integration', () => {
|
|
675
|
+
test('works with FormData and form submission', async ({ page }) => {
|
|
676
|
+
const listbox = page.locator('form-listbox').first()
|
|
677
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
678
|
+
timeout: 1000,
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
// Form is already wrapped in HTML, no need to create one
|
|
682
|
+
|
|
683
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
684
|
+
const optionValue = await firstOption.getAttribute('value')
|
|
685
|
+
|
|
686
|
+
await firstOption.click()
|
|
687
|
+
|
|
688
|
+
// Verify hidden input has the value
|
|
689
|
+
const hiddenInputValue = await page.evaluate(() => {
|
|
690
|
+
const hiddenInput = document.querySelector(
|
|
691
|
+
'form-listbox input[type="hidden"]',
|
|
692
|
+
) as HTMLInputElement
|
|
693
|
+
return hiddenInput?.value
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
expect(hiddenInputValue).toBe(optionValue)
|
|
697
|
+
|
|
698
|
+
// Test form data
|
|
699
|
+
const formData = await page.evaluate(() => {
|
|
700
|
+
const form = document.querySelector('form')
|
|
701
|
+
if (!form) return { error: 'No form found' }
|
|
702
|
+
const data = new FormData(form)
|
|
703
|
+
const entries = Object.fromEntries(data.entries())
|
|
704
|
+
return entries
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
// Debug form data if test fails
|
|
708
|
+
if (!formData.timezone) {
|
|
709
|
+
console.log('Form data:', formData)
|
|
710
|
+
console.log('Expected timezone value:', optionValue)
|
|
711
|
+
console.log('Hidden input value:', hiddenInputValue)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
expect(formData).toEqual({ timezone: optionValue })
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
test.describe('Component Properties', () => {
|
|
719
|
+
test('value property reflects selected option', async ({ page }) => {
|
|
720
|
+
const listbox = page.locator('form-listbox').first()
|
|
721
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
722
|
+
timeout: 1000,
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
// Initially no value
|
|
726
|
+
let value = await page.evaluate(() => {
|
|
727
|
+
const element = document.querySelector('form-listbox') as any
|
|
728
|
+
return element.value
|
|
729
|
+
})
|
|
730
|
+
expect(value).toBe('')
|
|
731
|
+
|
|
732
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
733
|
+
const optionValue = await firstOption.getAttribute('value')
|
|
734
|
+
|
|
735
|
+
await firstOption.click()
|
|
736
|
+
|
|
737
|
+
// Value should update
|
|
738
|
+
value = await page.evaluate(() => {
|
|
739
|
+
const element = document.querySelector('form-listbox') as any
|
|
740
|
+
return element.value
|
|
741
|
+
})
|
|
742
|
+
expect(value).toBe(optionValue)
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
test('filter property controls option visibility', async ({ page }) => {
|
|
746
|
+
const listbox = page.locator('form-listbox').first()
|
|
747
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
748
|
+
timeout: 1000,
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
// Test getting filter property
|
|
752
|
+
let filter = await page.evaluate(() => {
|
|
753
|
+
const element = document.querySelector('form-listbox') as any
|
|
754
|
+
return element.filter
|
|
755
|
+
})
|
|
756
|
+
expect(filter).toBe('')
|
|
757
|
+
|
|
758
|
+
// Set filter property
|
|
759
|
+
await page.evaluate(() => {
|
|
760
|
+
const element = document.querySelector('form-listbox') as any
|
|
761
|
+
element.filter = 'america'
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
filter = await page.evaluate(() => {
|
|
765
|
+
const element = document.querySelector('form-listbox') as any
|
|
766
|
+
return element.filter
|
|
767
|
+
})
|
|
768
|
+
expect(filter).toBe('america')
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
test('src property controls data source', async ({ page }) => {
|
|
772
|
+
const src = await page.evaluate(() => {
|
|
773
|
+
const element = document.querySelector('form-listbox') as any
|
|
774
|
+
return element.src
|
|
775
|
+
})
|
|
776
|
+
expect(src).toBe('/form-listbox/mocks/timezones.json')
|
|
777
|
+
|
|
778
|
+
// Change src property
|
|
779
|
+
await page.evaluate(() => {
|
|
780
|
+
const element = document.querySelector('form-listbox') as any
|
|
781
|
+
element.src = '/different-url.json'
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
const newSrc = await page.evaluate(() => {
|
|
785
|
+
const element = document.querySelector('form-listbox') as any
|
|
786
|
+
return element.src
|
|
787
|
+
})
|
|
788
|
+
expect(newSrc).toBe('/different-url.json')
|
|
789
|
+
})
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
test.describe('Edge Cases and Performance', () => {
|
|
793
|
+
test('handles rapid property changes gracefully', async ({ page }) => {
|
|
794
|
+
const listbox = page.locator('form-listbox').first()
|
|
795
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
796
|
+
timeout: 1000,
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
// Test that rapid filter changes don't break the component
|
|
800
|
+
await page.evaluate(() => {
|
|
801
|
+
const element = document.querySelector('form-listbox') as any
|
|
802
|
+
// Apply filters that should all find results
|
|
803
|
+
element.filter = 'a'
|
|
804
|
+
element.filter = 'af'
|
|
805
|
+
element.filter = 'afr'
|
|
806
|
+
element.filter = 'a' // back to broader filter
|
|
807
|
+
element.filter = '' // clear filter
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
// Should show all options when filter is cleared
|
|
811
|
+
const visibleOptions = listbox.locator(
|
|
812
|
+
'button[role="option"]:not([hidden])',
|
|
813
|
+
)
|
|
814
|
+
const visibleCount = await visibleOptions.count()
|
|
815
|
+
const totalCount = await listbox.locator('button[role="option"]').count()
|
|
816
|
+
|
|
817
|
+
expect(visibleCount).toBe(totalCount)
|
|
818
|
+
expect(visibleCount).toBeGreaterThan(100)
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
test('handles empty groups correctly', async ({ page }) => {
|
|
822
|
+
const listbox = page.locator('form-listbox').first()
|
|
823
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
824
|
+
timeout: 1000,
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
// Apply filter that should make some groups empty
|
|
828
|
+
await page.evaluate(() => {
|
|
829
|
+
const element = document.querySelector('form-listbox') as any
|
|
830
|
+
element.filter = 'Zurich' // Only one option
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
// Groups with no visible options should be hidden via CSS
|
|
834
|
+
// Check that some groups are effectively hidden (either via CSS or JS)
|
|
835
|
+
const allGroups = listbox.locator('[role="group"]')
|
|
836
|
+
const totalGroups = await allGroups.count()
|
|
837
|
+
|
|
838
|
+
// Count groups that have visible options
|
|
839
|
+
let groupsWithVisibleOptions = 0
|
|
840
|
+
for (let i = 0; i < totalGroups; i++) {
|
|
841
|
+
const group = allGroups.nth(i)
|
|
842
|
+
const visibleOptionsInGroup = group.locator(
|
|
843
|
+
'button[role="option"]:not([hidden])',
|
|
844
|
+
)
|
|
845
|
+
const count = await visibleOptionsInGroup.count()
|
|
846
|
+
if (count > 0) {
|
|
847
|
+
groupsWithVisibleOptions++
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
expect(groupsWithVisibleOptions).toBeLessThan(totalGroups)
|
|
852
|
+
expect(groupsWithVisibleOptions).toBeGreaterThan(0)
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
test('maintains performance with large datasets', async ({ page }) => {
|
|
856
|
+
const listbox = page.locator('form-listbox').first()
|
|
857
|
+
await expect(listbox.locator('[role="listbox"]')).toBeVisible({
|
|
858
|
+
timeout: 1000,
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
const startTime = Date.now()
|
|
862
|
+
|
|
863
|
+
// Apply and clear filter rapidly
|
|
864
|
+
await page.evaluate(() => {
|
|
865
|
+
const element = document.querySelector('form-listbox') as any
|
|
866
|
+
for (let i = 0; i < 10; i++) {
|
|
867
|
+
element.filter = 'test'
|
|
868
|
+
element.filter = ''
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const endTime = Date.now()
|
|
873
|
+
const duration = endTime - startTime
|
|
874
|
+
|
|
875
|
+
// Should complete quickly even with large dataset
|
|
876
|
+
expect(duration).toBeLessThan(2000)
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
test('handles component removal during loading', async ({ page }) => {
|
|
880
|
+
// Add a new listbox that will be removed quickly
|
|
881
|
+
await page.evaluate(() => {
|
|
882
|
+
const listbox = document.createElement('form-listbox')
|
|
883
|
+
listbox.setAttribute('src', '/form-listbox/timezones.json')
|
|
884
|
+
listbox.innerHTML = `
|
|
885
|
+
<input type="hidden" name="timezone" />
|
|
886
|
+
<card-callout>
|
|
887
|
+
<p class="loading" role="status">Loading...</p>
|
|
888
|
+
<p class="error" role="alert" aria-live="assertive" hidden></p>
|
|
889
|
+
</card-callout>
|
|
890
|
+
<module-scrollarea orientation="vertical">
|
|
891
|
+
<div role="listbox" aria-label="Test" hidden></div>
|
|
892
|
+
</module-scrollarea>
|
|
893
|
+
`
|
|
894
|
+
document.body.appendChild(listbox)
|
|
895
|
+
|
|
896
|
+
// Remove it quickly
|
|
897
|
+
setTimeout(() => listbox.remove(), 50)
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
// Wait to ensure no errors occur
|
|
901
|
+
await page.waitForTimeout(50)
|
|
902
|
+
|
|
903
|
+
// Test should complete without throwing errors
|
|
904
|
+
expect(true).toBe(true)
|
|
905
|
+
})
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
test.describe('Declarative Inline HTML', () => {
|
|
909
|
+
test('renders inline options without src attribute', async ({ page }) => {
|
|
910
|
+
const listbox = page.locator('form-listbox#colors')
|
|
911
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
912
|
+
const options = listboxElement.locator('button[role="option"]')
|
|
913
|
+
|
|
914
|
+
// Should show listbox immediately (no loading)
|
|
915
|
+
await expect(listboxElement).toBeVisible()
|
|
916
|
+
|
|
917
|
+
// Should have all inline options
|
|
918
|
+
await expect(options).toHaveCount(5)
|
|
919
|
+
|
|
920
|
+
// Should have correct option values and text
|
|
921
|
+
const firstOption = options.first()
|
|
922
|
+
await expect(firstOption).toHaveAttribute('value', 'red')
|
|
923
|
+
await expect(firstOption).toHaveText('Red')
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
test('handles option selection with inline HTML', async ({ page }) => {
|
|
927
|
+
const listbox = page.locator('form-listbox#colors')
|
|
928
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
929
|
+
const firstOption = listboxElement
|
|
930
|
+
.locator('button[role="option"]')
|
|
931
|
+
.first()
|
|
932
|
+
|
|
933
|
+
// Click first option
|
|
934
|
+
await firstOption.click()
|
|
935
|
+
|
|
936
|
+
// Should update component value
|
|
937
|
+
const componentValue = await listbox.getAttribute('value')
|
|
938
|
+
expect(componentValue).toBe('red')
|
|
939
|
+
|
|
940
|
+
// Should update hidden input
|
|
941
|
+
const hiddenInputValue = await listbox
|
|
942
|
+
.locator('input[type="hidden"]')
|
|
943
|
+
.inputValue()
|
|
944
|
+
expect(hiddenInputValue).toBe('red')
|
|
945
|
+
|
|
946
|
+
// Should mark option as selected
|
|
947
|
+
await expect(firstOption).toHaveAttribute('aria-selected', 'true')
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
test('supports filtering with inline options', async ({ page }) => {
|
|
951
|
+
const listbox = page.locator('form-listbox#colors')
|
|
952
|
+
const options = listbox.locator('button[role="option"]')
|
|
953
|
+
|
|
954
|
+
// All options visible initially
|
|
955
|
+
await expect(options).toHaveCount(5)
|
|
956
|
+
for (const option of await options.all()) {
|
|
957
|
+
await expect(option).toBeVisible()
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Set filter property
|
|
961
|
+
await listbox.evaluate((element: any) => {
|
|
962
|
+
element.filter = 'r'
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
// Should filter to options containing 'r'
|
|
966
|
+
const redOption = options.filter({ hasText: 'Red' })
|
|
967
|
+
const greenOption = options.filter({ hasText: 'Green' })
|
|
968
|
+
const yellowOption = options.filter({ hasText: 'Yellow' })
|
|
969
|
+
const blueOption = options.filter({ hasText: 'Blue' })
|
|
970
|
+
const purpleOption = options.filter({ hasText: 'Purple' })
|
|
971
|
+
|
|
972
|
+
await expect(redOption).toBeVisible() // Red contains 'r'
|
|
973
|
+
await expect(greenOption).toBeVisible() // Green contains 'r'
|
|
974
|
+
await expect(purpleOption).toBeVisible() // Purple contains 'r'
|
|
975
|
+
await expect(yellowOption).toBeHidden() // Yellow doesn't contain 'r'
|
|
976
|
+
await expect(blueOption).toBeHidden() // Blue doesn't contain 'r'
|
|
977
|
+
|
|
978
|
+
// Should highlight matching text
|
|
979
|
+
const greenOptionForHighlight = options.filter({ hasText: 'Green' })
|
|
980
|
+
const highlightedMark = greenOptionForHighlight.locator('mark')
|
|
981
|
+
await expect(highlightedMark).toBeVisible()
|
|
982
|
+
await expect(highlightedMark).toHaveText('r')
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
test('renders grouped inline options correctly', async ({ page }) => {
|
|
986
|
+
const listbox = page.locator('form-listbox#fruits')
|
|
987
|
+
const listboxElement = listbox.locator('[role="listbox"]')
|
|
988
|
+
const groups = listboxElement.locator('[role="group"]')
|
|
989
|
+
const options = listboxElement.locator('button[role="option"]')
|
|
990
|
+
|
|
991
|
+
// Should have both groups
|
|
992
|
+
await expect(groups).toHaveCount(2)
|
|
993
|
+
|
|
994
|
+
// Should have all options
|
|
995
|
+
await expect(options).toHaveCount(6)
|
|
996
|
+
|
|
997
|
+
// Should have proper group labels
|
|
998
|
+
const citrusLabel = listboxElement.locator('#fruits-citrus')
|
|
999
|
+
await expect(citrusLabel).toHaveText('Citrus')
|
|
1000
|
+
|
|
1001
|
+
const berriesLabel = listboxElement.locator('#fruits-berries')
|
|
1002
|
+
await expect(berriesLabel).toHaveText('Berries')
|
|
1003
|
+
|
|
1004
|
+
// Should maintain group structure for selection
|
|
1005
|
+
const orangeOption = options.filter({ hasText: 'Orange' })
|
|
1006
|
+
await orangeOption.click()
|
|
1007
|
+
|
|
1008
|
+
const componentValue = await listbox.getAttribute('value')
|
|
1009
|
+
expect(componentValue).toBe('orange')
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
test('works with form integration using inline options', async ({
|
|
1013
|
+
page,
|
|
1014
|
+
}) => {
|
|
1015
|
+
const listbox = page.locator('form-listbox#colors')
|
|
1016
|
+
const firstOption = listbox.locator('button[role="option"]').first()
|
|
1017
|
+
|
|
1018
|
+
// Select an option
|
|
1019
|
+
await firstOption.click()
|
|
1020
|
+
|
|
1021
|
+
// Check FormData
|
|
1022
|
+
const formData = await page.evaluate(() => {
|
|
1023
|
+
const form = document.querySelector(
|
|
1024
|
+
'form:nth-of-type(2)',
|
|
1025
|
+
) as HTMLFormElement
|
|
1026
|
+
if (!form) throw new Error('Form not found')
|
|
1027
|
+
const data = new FormData(form)
|
|
1028
|
+
const entries = Array.from(data.entries())
|
|
1029
|
+
return Object.fromEntries(entries)
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
expect(formData.color).toBe('red')
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
test('handles mixed content without src attribute', async ({ page }) => {
|
|
1036
|
+
const listbox = page.locator('form-listbox#colors')
|
|
1037
|
+
const options = listbox.locator('button[role="option"]')
|
|
1038
|
+
|
|
1039
|
+
// Should only count actual option buttons (using colors example)
|
|
1040
|
+
await expect(options).toHaveCount(5)
|
|
1041
|
+
|
|
1042
|
+
// Should still work for selection
|
|
1043
|
+
const firstOption = options.first()
|
|
1044
|
+
await firstOption.click()
|
|
1045
|
+
|
|
1046
|
+
const componentValue = await listbox.getAttribute('value')
|
|
1047
|
+
expect(componentValue).toBe('red')
|
|
1048
|
+
})
|
|
1049
|
+
})
|
|
1050
|
+
})
|