This is page 217 of 224. Use http://codebase.md/xmlui-org/xmlui?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ ├── hip-dogs-see.md
│ ├── open-dodos-fetch.md
│ ├── smooth-pears-kneel.md
│ ├── smooth-steaks-jog.md
│ ├── sour-queens-grab.md
│ ├── tasty-owls-win.md
│ └── wild-pots-glow.md
├── .eslintrc.cjs
├── .github
│ ├── build-checklist.png
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows
│ ├── deploy-blog-optimized.yml
│ ├── deploy-blog-swa.yml
│ ├── deploy-blog.yml
│ ├── deploy-docs-optimized.yml
│ ├── deploy-docs-swa.yml
│ ├── deploy-docs.yml
│ ├── prepare-versions.yml
│ ├── release-packages.yml
│ ├── run-all-tests-fast.yml
│ ├── run-all-tests.yml
│ └── run-smoke-tests.yml
├── .gitignore
├── .npmrc
├── .prettierrc.js
├── .vscode
│ ├── launch.json
│ └── settings.json
├── blog
│ ├── .gitignore
│ ├── .gitkeep
│ ├── CHANGELOG.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── blog
│ │ │ ├── images
│ │ │ │ ├── an-advanced-codefence.gif
│ │ │ │ ├── an-advanced-codefence.mp4
│ │ │ │ ├── blog-page-component.png
│ │ │ │ ├── blog-scrabble.png
│ │ │ │ ├── codefence-runner.png
│ │ │ │ ├── integrated-blog-search.png
│ │ │ │ ├── lorem-ipsum.png
│ │ │ │ ├── playground-checkbox-source.png
│ │ │ │ ├── playground.png
│ │ │ │ ├── use-xmlui-mcp-to-find-a-howto.png
│ │ │ │ └── xmlui-demo-gallery.png
│ │ │ ├── introducing-xmlui.md
│ │ │ ├── lorem-ipsum.md
│ │ │ ├── newest-post.md
│ │ │ ├── older-post.md
│ │ │ ├── xmlui-playground.md
│ │ │ └── xmlui-powered-blog.md
│ │ ├── mockServiceWorker.js
│ │ ├── resources
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ └── for-download
│ │ │ │ └── xmlui
│ │ │ │ └── xmlui-standalone.umd.js
│ │ │ ├── github.svg
│ │ │ ├── icons
│ │ │ │ ├── github.svg
│ │ │ │ └── rss.svg
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ ├── rss.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── staticwebapp.config.json
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── LinkButton.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ └── Separator.xmlui
│ │ ├── config.ts
│ │ └── Main.xmlui
│ └── tsconfig.json
├── CONTRIBUTING.md
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── ComponentRefLinks.txt
│ ├── content
│ │ ├── _meta.json
│ │ ├── components
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── APICall.md
│ │ │ ├── App.md
│ │ │ ├── AppHeader.md
│ │ │ ├── AppState.md
│ │ │ ├── AutoComplete.md
│ │ │ ├── Avatar.md
│ │ │ ├── Backdrop.md
│ │ │ ├── Badge.md
│ │ │ ├── BarChart.md
│ │ │ ├── Bookmark.md
│ │ │ ├── Breakout.md
│ │ │ ├── Button.md
│ │ │ ├── Card.md
│ │ │ ├── Carousel.md
│ │ │ ├── ChangeListener.md
│ │ │ ├── Checkbox.md
│ │ │ ├── CHStack.md
│ │ │ ├── ColorPicker.md
│ │ │ ├── Column.md
│ │ │ ├── ContentSeparator.md
│ │ │ ├── CVStack.md
│ │ │ ├── DataSource.md
│ │ │ ├── DateInput.md
│ │ │ ├── DatePicker.md
│ │ │ ├── DonutChart.md
│ │ │ ├── DropdownMenu.md
│ │ │ ├── EmojiSelector.md
│ │ │ ├── ExpandableItem.md
│ │ │ ├── FileInput.md
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FlowLayout.md
│ │ │ ├── Footer.md
│ │ │ ├── Form.md
│ │ │ ├── FormItem.md
│ │ │ ├── FormSection.md
│ │ │ ├── Fragment.md
│ │ │ ├── H1.md
│ │ │ ├── H2.md
│ │ │ ├── H3.md
│ │ │ ├── H4.md
│ │ │ ├── H5.md
│ │ │ ├── H6.md
│ │ │ ├── Heading.md
│ │ │ ├── HSplitter.md
│ │ │ ├── HStack.md
│ │ │ ├── Icon.md
│ │ │ ├── IFrame.md
│ │ │ ├── Image.md
│ │ │ ├── Items.md
│ │ │ ├── LabelList.md
│ │ │ ├── Legend.md
│ │ │ ├── LineChart.md
│ │ │ ├── Link.md
│ │ │ ├── List.md
│ │ │ ├── Logo.md
│ │ │ ├── Markdown.md
│ │ │ ├── MenuItem.md
│ │ │ ├── MenuSeparator.md
│ │ │ ├── ModalDialog.md
│ │ │ ├── NavGroup.md
│ │ │ ├── NavLink.md
│ │ │ ├── NavPanel.md
│ │ │ ├── NoResult.md
│ │ │ ├── NumberBox.md
│ │ │ ├── Option.md
│ │ │ ├── Page.md
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── Pages.md
│ │ │ ├── Pagination.md
│ │ │ ├── PasswordInput.md
│ │ │ ├── PieChart.md
│ │ │ ├── ProgressBar.md
│ │ │ ├── Queue.md
│ │ │ ├── RadioGroup.md
│ │ │ ├── RealTimeAdapter.md
│ │ │ ├── Redirect.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── Select.md
│ │ │ ├── Slider.md
│ │ │ ├── Slot.md
│ │ │ ├── SpaceFiller.md
│ │ │ ├── Spinner.md
│ │ │ ├── Splitter.md
│ │ │ ├── Stack.md
│ │ │ ├── StickyBox.md
│ │ │ ├── SubMenuItem.md
│ │ │ ├── Switch.md
│ │ │ ├── TabItem.md
│ │ │ ├── Table.md
│ │ │ ├── TableOfContents.md
│ │ │ ├── Tabs.md
│ │ │ ├── Text.md
│ │ │ ├── TextArea.md
│ │ │ ├── TextBox.md
│ │ │ ├── Theme.md
│ │ │ ├── TimeInput.md
│ │ │ ├── Timer.md
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneSwitch.md
│ │ │ ├── Tooltip.md
│ │ │ ├── Tree.md
│ │ │ ├── VSplitter.md
│ │ │ ├── VStack.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ ├── xmlui-spreadsheet
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Spreadsheet.md
│ │ │ └── xmlui-website-blocks
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Carousel.md
│ │ │ ├── HelloMd.md
│ │ │ ├── HeroSection.md
│ │ │ └── ScrollToTop.md
│ │ └── extensions
│ │ ├── _meta.json
│ │ ├── xmlui-animations
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ ├── Animation.md
│ │ │ ├── FadeAnimation.md
│ │ │ ├── FadeInAnimation.md
│ │ │ ├── FadeOutAnimation.md
│ │ │ ├── ScaleAnimation.md
│ │ │ └── SlideInAnimation.md
│ │ └── xmlui-website-blocks
│ │ ├── _meta.json
│ │ ├── _overview.md
│ │ ├── Carousel.md
│ │ ├── HelloMd.md
│ │ ├── HeroSection.md
│ │ └── ScrollToTop.md
│ ├── extensions.ts
│ ├── index.html
│ ├── index.ts
│ ├── package.json
│ ├── public
│ │ ├── feed.rss
│ │ ├── mockServiceWorker.js
│ │ ├── pages
│ │ │ ├── _meta.json
│ │ │ ├── app-structure.md
│ │ │ ├── build-editor-component.md
│ │ │ ├── build-hello-world-component.md
│ │ │ ├── components-intro.md
│ │ │ ├── context-variables.md
│ │ │ ├── forms.md
│ │ │ ├── globals.md
│ │ │ ├── glossary.md
│ │ │ ├── helper-tags.md
│ │ │ ├── hosted-deployment.md
│ │ │ ├── howto
│ │ │ │ ├── assign-a-complex-json-literal-to-a-component-variable.md
│ │ │ │ ├── chain-a-refetch.md
│ │ │ │ ├── control-cache-invalidation.md
│ │ │ │ ├── copy-billing-to-shipping.md
│ │ │ │ ├── debounce-user-input-for-api-calls.md
│ │ │ │ ├── debounce-with-changelistener.md
│ │ │ │ ├── debug-a-component.md
│ │ │ │ ├── delay-a-datasource-until-another-datasource-is-ready.md
│ │ │ │ ├── delegate-a-method.md
│ │ │ │ ├── do-custom-form-validation.md
│ │ │ │ ├── expose-a-method-from-a-component.md
│ │ │ │ ├── filter-and-transform-data-from-an-api.md
│ │ │ │ ├── group-items-in-list-by-a-property.md
│ │ │ │ ├── handle-background-operations.md
│ │ │ │ ├── hide-an-element-until-its-datasource-is-ready.md
│ │ │ │ ├── implement-an-authentication-gate.md
│ │ │ │ ├── make-a-set-of-equal-width-cards.md
│ │ │ │ ├── make-a-table-responsive.md
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ ├── use-modal-dialog-onclose.md
│ │ │ │ └── use-the-same-modaldialog-to-add-or-edit.md
│ │ │ ├── howto.md
│ │ │ ├── intro.md
│ │ │ ├── layout.md
│ │ │ ├── markup.md
│ │ │ ├── mcp.md
│ │ │ ├── modal-dialogs.md
│ │ │ ├── news-and-reviews.md
│ │ │ ├── reactive-intro.md
│ │ │ ├── refactoring.md
│ │ │ ├── routing-and-links.md
│ │ │ ├── samples
│ │ │ │ ├── color-palette.xmlui
│ │ │ │ ├── color-values.xmlui
│ │ │ │ ├── shadow-sizes.xmlui
│ │ │ │ ├── spacing-sizes.xmlui
│ │ │ │ ├── swatch.xmlui
│ │ │ │ ├── theme-gallery-brief.xmlui
│ │ │ │ └── theme-gallery.xmlui
│ │ │ ├── scoping.md
│ │ │ ├── scripting.md
│ │ │ ├── styles-and-themes
│ │ │ │ ├── common-units.md
│ │ │ │ ├── layout-props.md
│ │ │ │ ├── theme-variable-defaults.md
│ │ │ │ ├── theme-variables.md
│ │ │ │ └── themes.md
│ │ │ ├── template-properties.md
│ │ │ ├── test.md
│ │ │ ├── tutorial-01.md
│ │ │ ├── tutorial-02.md
│ │ │ ├── tutorial-03.md
│ │ │ ├── tutorial-04.md
│ │ │ ├── tutorial-05.md
│ │ │ ├── tutorial-06.md
│ │ │ ├── tutorial-07.md
│ │ │ ├── tutorial-08.md
│ │ │ ├── tutorial-09.md
│ │ │ ├── tutorial-10.md
│ │ │ ├── tutorial-11.md
│ │ │ ├── tutorial-12.md
│ │ │ ├── universal-properties.md
│ │ │ ├── user-defined-components.md
│ │ │ ├── vscode.md
│ │ │ ├── working-with-markdown.md
│ │ │ ├── working-with-text.md
│ │ │ ├── xmlui-animations
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── Animation.md
│ │ │ │ ├── FadeAnimation.md
│ │ │ │ ├── FadeInAnimation.md
│ │ │ │ ├── FadeOutAnimation.md
│ │ │ │ ├── ScaleAnimation.md
│ │ │ │ └── SlideInAnimation.md
│ │ │ ├── xmlui-charts
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── DonutChart.md
│ │ │ │ ├── LabelList.md
│ │ │ │ ├── Legend.md
│ │ │ │ ├── LineChart.md
│ │ │ │ └── PieChart.md
│ │ │ ├── xmlui-pdf
│ │ │ │ ├── _meta.json
│ │ │ │ ├── _overview.md
│ │ │ │ └── Pdf.md
│ │ │ └── xmlui-spreadsheet
│ │ │ ├── _meta.json
│ │ │ ├── _overview.md
│ │ │ └── Spreadsheet.md
│ │ ├── resources
│ │ │ ├── devdocs
│ │ │ │ ├── debug-proxy-object-2.png
│ │ │ │ ├── debug-proxy-object.png
│ │ │ │ ├── table_editor_01.png
│ │ │ │ ├── table_editor_02.png
│ │ │ │ ├── table_editor_03.png
│ │ │ │ ├── table_editor_04.png
│ │ │ │ ├── table_editor_05.png
│ │ │ │ ├── table_editor_06.png
│ │ │ │ ├── table_editor_07.png
│ │ │ │ ├── table_editor_08.png
│ │ │ │ ├── table_editor_09.png
│ │ │ │ ├── table_editor_10.png
│ │ │ │ ├── table_editor_11.png
│ │ │ │ ├── table-editor-01.png
│ │ │ │ ├── table-editor-02.png
│ │ │ │ ├── table-editor-03.png
│ │ │ │ ├── table-editor-04.png
│ │ │ │ ├── table-editor-06.png
│ │ │ │ ├── table-editor-07.png
│ │ │ │ ├── table-editor-08.png
│ │ │ │ ├── table-editor-09.png
│ │ │ │ └── xmlui-rendering-of-tiptap-markdown.png
│ │ │ ├── favicon.ico
│ │ │ ├── files
│ │ │ │ ├── clients.json
│ │ │ │ ├── daily-revenue.json
│ │ │ │ ├── dashboard-stats.json
│ │ │ │ ├── demo.xmlui
│ │ │ │ ├── demo.xmlui.xs
│ │ │ │ ├── downloads
│ │ │ │ │ └── downloads.json
│ │ │ │ ├── for-download
│ │ │ │ │ ├── index-with-api.html
│ │ │ │ │ ├── index.html
│ │ │ │ │ ├── mockApi.js
│ │ │ │ │ ├── start-darwin.sh
│ │ │ │ │ ├── start-linux.sh
│ │ │ │ │ ├── start.bat
│ │ │ │ │ └── xmlui
│ │ │ │ │ └── xmlui-standalone.umd.js
│ │ │ │ ├── getting-started
│ │ │ │ │ ├── cl-tutorial-final.zip
│ │ │ │ │ ├── cl-tutorial.zip
│ │ │ │ │ ├── cl-tutorial2.zip
│ │ │ │ │ ├── cl-tutorial3.zip
│ │ │ │ │ ├── cl-tutorial4.zip
│ │ │ │ │ ├── cl-tutorial5.zip
│ │ │ │ │ ├── cl-tutorial6.zip
│ │ │ │ │ ├── getting-started.zip
│ │ │ │ │ ├── hello-xmlui.zip
│ │ │ │ │ ├── xmlui-empty.zip
│ │ │ │ │ └── xmlui-starter.zip
│ │ │ │ ├── howto
│ │ │ │ │ └── component-icons
│ │ │ │ │ └── up-arrow.svg
│ │ │ │ ├── invoices.json
│ │ │ │ ├── monthly-status.json
│ │ │ │ ├── news-and-reviews.json
│ │ │ │ ├── products.json
│ │ │ │ ├── releases.json
│ │ │ │ ├── tutorials
│ │ │ │ │ ├── datasource
│ │ │ │ │ │ └── api.ts
│ │ │ │ │ └── p2do
│ │ │ │ │ ├── api.ts
│ │ │ │ │ └── todo-logo.svg
│ │ │ │ └── xmlui.json
│ │ │ ├── icons
│ │ │ │ ├── github.svg
│ │ │ │ └── rss.svg
│ │ │ ├── images
│ │ │ │ ├── apiaction-tutorial
│ │ │ │ │ ├── add-success.png
│ │ │ │ │ ├── apiaction-param.png
│ │ │ │ │ ├── change-completed.png
│ │ │ │ │ ├── change-in-progress.png
│ │ │ │ │ ├── confirm-delete.png
│ │ │ │ │ ├── data-error.png
│ │ │ │ │ ├── data-progress.png
│ │ │ │ │ ├── data-success.png
│ │ │ │ │ ├── display-1.png
│ │ │ │ │ ├── item-deleted.png
│ │ │ │ │ ├── item-updated.png
│ │ │ │ │ ├── missing-api-key.png
│ │ │ │ │ ├── new-item-added.png
│ │ │ │ │ └── test-message.png
│ │ │ │ ├── chat-api
│ │ │ │ │ └── domain-model.svg
│ │ │ │ ├── components
│ │ │ │ │ ├── image
│ │ │ │ │ │ └── breakfast.jpg
│ │ │ │ │ ├── markdown
│ │ │ │ │ │ └── colors.png
│ │ │ │ │ └── modal
│ │ │ │ │ ├── deep_link_dialog_1.jpg
│ │ │ │ │ └── deep_link_dialog_2.jpg
│ │ │ │ ├── create-apps
│ │ │ │ │ ├── collapsed-vertical.png
│ │ │ │ │ ├── using-forms-warning-dialog.png
│ │ │ │ │ └── using-forms.png
│ │ │ │ ├── datasource-tutorial
│ │ │ │ │ ├── data-with-header.png
│ │ │ │ │ ├── filtered-data.png
│ │ │ │ │ ├── filtered-items.png
│ │ │ │ │ ├── initial-page-items.png
│ │ │ │ │ ├── list-items.png
│ │ │ │ │ ├── next-page-items.png
│ │ │ │ │ ├── no-data.png
│ │ │ │ │ ├── pagination-1.jpg
│ │ │ │ │ ├── pagination-1.png
│ │ │ │ │ ├── polling-1.png
│ │ │ │ │ ├── refetch-data.png
│ │ │ │ │ ├── slow-loading.png
│ │ │ │ │ ├── test-message.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── unconventional-data.png
│ │ │ │ │ └── unfiltered-items.png
│ │ │ │ ├── flower.jpg
│ │ │ │ ├── get-started
│ │ │ │ │ ├── add-new-contact.png
│ │ │ │ │ ├── app-modified.png
│ │ │ │ │ ├── app-start.png
│ │ │ │ │ ├── app-with-boxes.png
│ │ │ │ │ ├── app-with-toast.png
│ │ │ │ │ ├── boilerplate-structure.png
│ │ │ │ │ ├── cl-initial.png
│ │ │ │ │ ├── cl-start.png
│ │ │ │ │ ├── contact-counts.png
│ │ │ │ │ ├── contact-dialog-title.png
│ │ │ │ │ ├── contact-dialog.png
│ │ │ │ │ ├── contact-menus.png
│ │ │ │ │ ├── contact-predicates.png
│ │ │ │ │ ├── context-menu.png
│ │ │ │ │ ├── dashboard-numbers.png
│ │ │ │ │ ├── default-contact-list.png
│ │ │ │ │ ├── delete-contact.png
│ │ │ │ │ ├── delete-task.png
│ │ │ │ │ ├── detailed-template.png
│ │ │ │ │ ├── edit-contact-details.png
│ │ │ │ │ ├── edited-contact-saved.png
│ │ │ │ │ ├── empty-sections.png
│ │ │ │ │ ├── filter-completed.png
│ │ │ │ │ ├── fullwidth-desktop.png
│ │ │ │ │ ├── fullwidth-mobile.png
│ │ │ │ │ ├── initial-table.png
│ │ │ │ │ ├── items-and-badges.png
│ │ │ │ │ ├── loading-message.png
│ │ │ │ │ ├── new-contact-button.png
│ │ │ │ │ ├── new-contact-saved.png
│ │ │ │ │ ├── no-empty-sections.png
│ │ │ │ │ ├── personal-todo-initial.png
│ │ │ │ │ ├── piechart.png
│ │ │ │ │ ├── review-today.png
│ │ │ │ │ ├── rudimentary-dashboard.png
│ │ │ │ │ ├── section-collapsed.png
│ │ │ │ │ ├── sectioned-items.png
│ │ │ │ │ ├── sections-ordered.png
│ │ │ │ │ ├── spacex-list-with-links.png
│ │ │ │ │ ├── spacex-list.png
│ │ │ │ │ ├── start-personal-todo-1.png
│ │ │ │ │ ├── submit-new-contact.png
│ │ │ │ │ ├── submit-new-task.png
│ │ │ │ │ ├── syntax-highlighting.png
│ │ │ │ │ ├── table-with-badge.png
│ │ │ │ │ ├── template-with-card.png
│ │ │ │ │ ├── test-emulated-api.png
│ │ │ │ │ ├── Thumbs.db
│ │ │ │ │ ├── todo-logo.png
│ │ │ │ │ └── xmlui-tools.png
│ │ │ │ ├── HelloApp.png
│ │ │ │ ├── HelloApp2.png
│ │ │ │ ├── logos
│ │ │ │ │ ├── xmlui1.svg
│ │ │ │ │ ├── xmlui2.svg
│ │ │ │ │ ├── xmlui3.svg
│ │ │ │ │ ├── xmlui4.svg
│ │ │ │ │ ├── xmlui5.svg
│ │ │ │ │ ├── xmlui6.svg
│ │ │ │ │ └── xmlui7.svg
│ │ │ │ ├── pdf
│ │ │ │ │ └── dummy-pdf.jpg
│ │ │ │ ├── rendering-engine
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ ├── Component.svg
│ │ │ │ │ ├── CompoundComponent.svg
│ │ │ │ │ ├── RootComponent.svg
│ │ │ │ │ └── tree-with-containers.svg
│ │ │ │ ├── reviewers-guide
│ │ │ │ │ ├── AppEngine-flow.svg
│ │ │ │ │ └── incbutton-in-action.png
│ │ │ │ ├── tools
│ │ │ │ │ └── boilerplate-structure.png
│ │ │ │ ├── try.svg
│ │ │ │ ├── tutorial
│ │ │ │ │ ├── app-chat-history.png
│ │ │ │ │ ├── app-content-placeholder.png
│ │ │ │ │ ├── app-header-and-content.png
│ │ │ │ │ ├── app-links-channel-selected.png
│ │ │ │ │ ├── app-links-click.png
│ │ │ │ │ ├── app-navigation.png
│ │ │ │ │ ├── finished-ex01.png
│ │ │ │ │ ├── finished-ex02.png
│ │ │ │ │ ├── hello.png
│ │ │ │ │ ├── splash-screen-advanced.png
│ │ │ │ │ ├── splash-screen-after-click.png
│ │ │ │ │ ├── splash-screen-centered.png
│ │ │ │ │ ├── splash-screen-events.png
│ │ │ │ │ ├── splash-screen-expression.png
│ │ │ │ │ ├── splash-screen-reuse-after.png
│ │ │ │ │ ├── splash-screen-reuse-before.png
│ │ │ │ │ └── splash-screen.png
│ │ │ │ └── tutorial-01.png
│ │ │ ├── llms.txt
│ │ │ ├── logo-dark.svg
│ │ │ ├── logo.svg
│ │ │ ├── pg-popout.svg
│ │ │ └── xmlui-logo.svg
│ │ ├── serve.json
│ │ └── staticwebapp.config.json
│ ├── scripts
│ │ ├── download-latest-xmlui.js
│ │ ├── generate-rss.js
│ │ ├── get-releases.js
│ │ └── utils.js
│ ├── src
│ │ ├── components
│ │ │ ├── BlogOverview.xmlui
│ │ │ ├── BlogPage.xmlui
│ │ │ ├── Boxes.xmlui
│ │ │ ├── Breadcrumb.xmlui
│ │ │ ├── ChangeLog.xmlui
│ │ │ ├── ColorPalette.xmlui
│ │ │ ├── DocumentLinks.xmlui
│ │ │ ├── DocumentPage.xmlui
│ │ │ ├── DocumentPageNoTOC.xmlui
│ │ │ ├── Icons.xmlui
│ │ │ ├── IncButton.xmlui
│ │ │ ├── IncButton2.xmlui
│ │ │ ├── LinkButton.xmlui
│ │ │ ├── NameValue.xmlui
│ │ │ ├── PageNotFound.xmlui
│ │ │ ├── PaletteItem.xmlui
│ │ │ ├── Palettes.xmlui
│ │ │ ├── SectionHeader.xmlui
│ │ │ ├── Separator.xmlui
│ │ │ ├── TBD.xmlui
│ │ │ ├── Test.xmlui
│ │ │ ├── ThemesIntro.xmlui
│ │ │ ├── ThousandThemes.xmlui
│ │ │ ├── TubeStops.xmlui
│ │ │ ├── TubeStops.xmlui.xs
│ │ │ └── TwoColumnCode.xmlui
│ │ ├── config.ts
│ │ ├── Main.xmlui
│ │ └── themes
│ │ ├── earthtone.ts
│ │ ├── xmlui-gray-on-default.ts
│ │ ├── xmlui-green-on-default.ts
│ │ └── xmlui-orange-on-default.ts
│ └── tsconfig.json
├── LICENSE
├── package-lock.json
├── package.json
├── packages
│ ├── tsconfig.json
│ ├── xmlui-animations
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── Animation.tsx
│ │ ├── AnimationNative.tsx
│ │ ├── FadeAnimation.tsx
│ │ ├── FadeInAnimation.tsx
│ │ ├── FadeOutAnimation.tsx
│ │ ├── index.tsx
│ │ ├── ScaleAnimation.tsx
│ │ └── SlideInAnimation.tsx
│ ├── xmlui-devtools
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── devtools
│ │ │ │ ├── DevTools.tsx
│ │ │ │ ├── DevToolsNative.module.scss
│ │ │ │ ├── DevToolsNative.tsx
│ │ │ │ ├── ModalDialog.module.scss
│ │ │ │ ├── ModalDialog.tsx
│ │ │ │ ├── ModalVisibilityContext.tsx
│ │ │ │ ├── Tooltip.module.scss
│ │ │ │ ├── Tooltip.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── editor
│ │ │ │ └── Editor.tsx
│ │ │ └── index.tsx
│ │ └── vite.config-overrides.ts
│ ├── xmlui-hello-world
│ │ ├── .gitignore
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── HelloWorld.module.scss
│ │ ├── HelloWorld.tsx
│ │ ├── HelloWorldNative.tsx
│ │ └── index.tsx
│ ├── xmlui-os-frames
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── IPhoneFrame.module.scss
│ │ ├── IPhoneFrame.tsx
│ │ ├── MacOSAppFrame.module.scss
│ │ ├── MacOSAppFrame.tsx
│ │ ├── WindowsAppFrame.module.scss
│ │ └── WindowsAppFrame.tsx
│ ├── xmlui-pdf
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ ├── components
│ │ │ │ └── Pdf.xmlui
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── LazyPdfNative.tsx
│ │ ├── Pdf.module.scss
│ │ └── Pdf.tsx
│ ├── xmlui-playground
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── hooks
│ │ │ ├── usePlayground.ts
│ │ │ └── useToast.ts
│ │ ├── index.tsx
│ │ ├── playground
│ │ │ ├── Box.module.scss
│ │ │ ├── Box.tsx
│ │ │ ├── CodeSelector.module.scss
│ │ │ ├── CodeSelector.tsx
│ │ │ ├── ConfirmationDialog.module.scss
│ │ │ ├── ConfirmationDialog.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── Header.module.scss
│ │ │ ├── Header.tsx
│ │ │ ├── Playground.tsx
│ │ │ ├── PlaygroundContent.module.scss
│ │ │ ├── PlaygroundContent.tsx
│ │ │ ├── PlaygroundNative.module.scss
│ │ │ ├── PlaygroundNative.tsx
│ │ │ ├── Preview.tsx
│ │ │ ├── StandalonePlayground.tsx
│ │ │ ├── StandalonePlaygroundNative.module.scss
│ │ │ ├── StandalonePlaygroundNative.tsx
│ │ │ ├── ThemeSwitcher.module.scss
│ │ │ ├── ThemeSwitcher.tsx
│ │ │ └── utils.ts
│ │ ├── providers
│ │ │ ├── Toast.module.scss
│ │ │ └── ToastProvider.tsx
│ │ ├── state
│ │ │ └── store.ts
│ │ ├── themes
│ │ │ └── theme.ts
│ │ └── utils
│ │ └── helpers.ts
│ ├── xmlui-search
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Search.module.scss
│ │ └── Search.tsx
│ ├── xmlui-spreadsheet
│ │ ├── .gitignore
│ │ ├── demo
│ │ │ └── Main.xmlui
│ │ ├── index.html
│ │ ├── index.ts
│ │ ├── meta
│ │ │ └── componentsMetadata.ts
│ │ ├── package.json
│ │ └── src
│ │ ├── index.tsx
│ │ ├── Spreadsheet.tsx
│ │ └── SpreadsheetNative.tsx
│ └── xmlui-website-blocks
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── demo
│ │ ├── components
│ │ │ ├── HeroBackgroundBreakoutPage.xmlui
│ │ │ ├── HeroBackgroundsPage.xmlui
│ │ │ ├── HeroContentsPage.xmlui
│ │ │ ├── HeroTextAlignPage.xmlui
│ │ │ ├── HeroTextPage.xmlui
│ │ │ └── HeroTonesPage.xmlui
│ │ ├── Main.xmlui
│ │ └── themes
│ │ └── default.ts
│ ├── index.html
│ ├── index.ts
│ ├── meta
│ │ └── componentsMetadata.ts
│ ├── package.json
│ ├── public
│ │ └── resources
│ │ ├── building.jpg
│ │ └── xmlui-logo.svg
│ └── src
│ ├── Carousel
│ │ ├── Carousel.module.scss
│ │ ├── Carousel.tsx
│ │ ├── CarouselContext.tsx
│ │ └── CarouselNative.tsx
│ ├── FancyButton
│ │ ├── FancyButton.module.scss
│ │ ├── FancyButton.tsx
│ │ └── FancyButton.xmlui
│ ├── Hello
│ │ ├── Hello.tsx
│ │ ├── Hello.xmlui
│ │ └── Hello.xmlui.xs
│ ├── HeroSection
│ │ ├── HeroSection.module.scss
│ │ ├── HeroSection.spec.ts
│ │ ├── HeroSection.tsx
│ │ └── HeroSectionNative.tsx
│ ├── index.tsx
│ ├── ScrollToTop
│ │ ├── ScrollToTop.module.scss
│ │ ├── ScrollToTop.tsx
│ │ └── ScrollToTopNative.tsx
│ └── vite-env.d.ts
├── playwright.config.ts
├── README.md
├── tools
│ ├── codefence
│ │ └── xmlui-code-fence-docs.md
│ ├── create-app
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── create-app.ts
│ │ ├── helpers
│ │ │ ├── copy.ts
│ │ │ ├── get-pkg-manager.ts
│ │ │ ├── git.ts
│ │ │ ├── install.ts
│ │ │ ├── is-folder-empty.ts
│ │ │ ├── is-writeable.ts
│ │ │ ├── make-dir.ts
│ │ │ └── validate-pkg.ts
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── templates
│ │ │ ├── default
│ │ │ │ └── ts
│ │ │ │ ├── gitignore
│ │ │ │ ├── index.html
│ │ │ │ ├── index.ts
│ │ │ │ ├── public
│ │ │ │ │ ├── mockServiceWorker.js
│ │ │ │ │ ├── resources
│ │ │ │ │ │ ├── favicon.ico
│ │ │ │ │ │ └── xmlui-logo.svg
│ │ │ │ │ └── serve.json
│ │ │ │ └── src
│ │ │ │ ├── components
│ │ │ │ │ ├── ApiAware.xmlui
│ │ │ │ │ ├── Home.xmlui
│ │ │ │ │ ├── IncButton.xmlui
│ │ │ │ │ └── PagePanel.xmlui
│ │ │ │ ├── config.ts
│ │ │ │ └── Main.xmlui
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── create-xmlui-hello-world
│ │ ├── index.js
│ │ └── package.json
│ └── vscode
│ ├── .gitignore
│ ├── .vscode
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── .vscodeignore
│ ├── build.sh
│ ├── CHANGELOG.md
│ ├── esbuild.js
│ ├── eslint.config.mjs
│ ├── formatter-docs.md
│ ├── generate-test-sample.sh
│ ├── LICENSE.md
│ ├── package-lock.json
│ ├── package.json
│ ├── README.md
│ ├── resources
│ │ ├── xmlui-logo.png
│ │ └── xmlui-markup-syntax-highlighting.png
│ ├── src
│ │ ├── extension.ts
│ │ └── server.ts
│ ├── syntaxes
│ │ └── xmlui.tmLanguage.json
│ ├── test-samples
│ │ └── sample.xmlui
│ ├── tsconfig.json
│ └── tsconfig.tsbuildinfo
├── turbo.json
└── xmlui
├── .gitignore
├── bin
│ ├── bootstrap.cjs
│ ├── bootstrap.js
│ ├── build-lib.ts
│ ├── build.ts
│ ├── index.ts
│ ├── preview.ts
│ ├── start.ts
│ ├── vite-xmlui-plugin.ts
│ └── viteConfig.ts
├── CHANGELOG.md
├── conventions
│ ├── component-qa-checklist.md
│ ├── copilot-conventions.md
│ ├── create-xmlui-components.md
│ ├── mermaid.md
│ ├── testing-conventions.md
│ └── xmlui-in-a-nutshell.md
├── dev-docs
│ ├── accessibility.md
│ ├── app-next.md
│ ├── build-system.md
│ ├── build-xmlui.md
│ ├── component-behaviors.md
│ ├── component-metadata.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.md
│ ├── glossary.md
│ ├── images
│ │ ├── condensed-layout-content-scroll-no-gutters-bottom.svg
│ │ ├── condensed-layout-content-scroll-no-gutters-no-overflow.svg
│ │ ├── condensed-layout-content-scroll-no-gutters-top.svg
│ │ ├── condensed-layout-content-scroll-with-gutters-bottom.svg
│ │ ├── condensed-layout-content-scroll-with-gutters-no-overflow.svg
│ │ ├── condensed-layout-content-scroll-with-gutters-top.svg
│ │ ├── condensed-layout-no-gutters-bottom.svg
│ │ ├── condensed-layout-no-gutters-mid.svg
│ │ ├── condensed-layout-no-gutters-top.svg
│ │ ├── condensed-layout-no-overflow.svg
│ │ ├── condensed-layout-with-gutters-bottom.svg
│ │ ├── condensed-layout-with-gutters-no-overflow.svg
│ │ ├── condensed-layout-with-gutters-top.svg
│ │ ├── condensed-sticky-content-scroll-no-gutters-bottom.svg
│ │ ├── condensed-sticky-content-scroll-no-gutters-no-overflow.svg
│ │ ├── condensed-sticky-content-scroll-no-gutters-top.svg
│ │ ├── condensed-sticky-content-scroll-with-gutters-bottom.svg
│ │ ├── condensed-sticky-content-scroll-with-gutters-no-overflow.svg
│ │ ├── condensed-sticky-content-scroll-with-gutters-top.svg
│ │ ├── condensed-sticky-layout-no-gutters-bottom.svg
│ │ ├── condensed-sticky-layout-no-gutters-top.svg
│ │ ├── condensed-sticky-layout-no-overflow.svg
│ │ ├── condensed-sticky-layout-with-gutters-bottom.svg
│ │ ├── condensed-sticky-layout-with-gutters-no-overflow.svg
│ │ ├── condensed-sticky-layout-with-gutters-top.svg
│ │ ├── desktop-layout-no-overflow.svg
│ │ ├── desktop-layout-overflow-bottom.svg
│ │ ├── desktop-layout-overflow-top.svg
│ │ ├── horizontal-layout-content-scroll-no-gutters-bottom.svg
│ │ ├── horizontal-layout-content-scroll-no-gutters-top.svg
│ │ ├── horizontal-layout-content-scroll-no-gutters.svg
│ │ ├── horizontal-layout-content-scroll-with-gutters-bottom.svg
│ │ ├── horizontal-layout-content-scroll-with-gutters-diagram.svg
│ │ ├── horizontal-layout-content-scroll-with-gutters-top.svg
│ │ ├── horizontal-layout-diagram.svg
│ │ ├── horizontal-layout-no-gutters-overflow-scrollbar-bottom.svg
│ │ ├── horizontal-layout-no-gutters-overflow-scrollbar-top.svg
│ │ ├── horizontal-layout-overflow-scrollbar-bottom.svg
│ │ ├── horizontal-layout-overflow-scrollbar-nogutter-bottom.svg
│ │ ├── horizontal-layout-overflow-scrollbar-nogutter-mid.svg
│ │ ├── horizontal-layout-overflow-scrollbar-top.svg
│ │ ├── horizontal-layout-with-gutters-diagram.svg
│ │ ├── horizontal-sticky-content-scroll-no-gutters-bottom.svg
│ │ ├── horizontal-sticky-content-scroll-no-gutters-top.svg
│ │ ├── horizontal-sticky-content-scroll-no-gutters.svg
│ │ ├── horizontal-sticky-content-scroll-with-gutters-bottom.svg
│ │ ├── horizontal-sticky-content-scroll-with-gutters-top.svg
│ │ ├── horizontal-sticky-content-scroll-with-gutters.svg
│ │ ├── horizontal-sticky-layout-overflow-bottom.svg
│ │ ├── horizontal-sticky-layout-overflow-top.svg
│ │ ├── horizontal-sticky-layout-with-gutters-bottom-scroll.svg
│ │ ├── horizontal-sticky-layout-with-gutters-mid-scroll.svg
│ │ ├── horizontal-sticky-layout-with-gutters-no-overflow.svg
│ │ ├── horizontal-sticky-layout-with-gutters-overflow-bottom.svg
│ │ ├── horizontal-sticky-layout-with-gutters-overflow-mid.svg
│ │ ├── horizontal-sticky-layout-with-gutters-overflow-top.svg
│ │ ├── horizontal-sticky-layout-with-gutters.svg
│ │ ├── horizontal-sticky-layout.svg
│ │ ├── vertical-full-header-content-scroll-no-gutters-overflow-bottom.svg
│ │ ├── vertical-full-header-content-scroll-no-gutters-overflow-top.svg
│ │ ├── vertical-full-header-content-scroll-no-gutters.svg
│ │ ├── vertical-full-header-content-scroll-with-gutters-overflow-bottom.svg
│ │ ├── vertical-full-header-content-scroll-with-gutters-overflow-top.svg
│ │ ├── vertical-full-header-content-scroll-with-gutters.svg
│ │ ├── vertical-full-header-layout-no-gutters-overflow-bottom.svg
│ │ ├── vertical-full-header-layout-no-gutters-overflow-top.svg
│ │ ├── vertical-full-header-layout-no-gutters.svg
│ │ ├── vertical-full-header-layout-with-gutters-overflow-bottom.svg
│ │ ├── vertical-full-header-layout-with-gutters-overflow-top.svg
│ │ ├── vertical-full-header-layout-with-gutters.svg
│ │ ├── vertical-layout-content-scroll-no-gutters-overflow-bottom.svg
│ │ ├── vertical-layout-content-scroll-no-gutters-overflow-top.svg
│ │ ├── vertical-layout-content-scroll-no-gutters.svg
│ │ ├── vertical-layout-content-scroll-with-gutters-overflow-bottom.svg
│ │ ├── vertical-layout-content-scroll-with-gutters-overflow-top.svg
│ │ ├── vertical-layout-content-scroll-with-gutters.svg
│ │ ├── vertical-layout-no-gutters-overflow-bottom.svg
│ │ ├── vertical-layout-no-gutters-overflow-top.svg
│ │ ├── vertical-layout-no-gutters.svg
│ │ ├── vertical-layout-with-gutters-overflow-bottom.svg
│ │ ├── vertical-layout-with-gutters-overflow-top.svg
│ │ ├── vertical-layout-with-gutters.svg
│ │ ├── vertical-sticky-content-scroll-no-gutters-overflow-bottom.svg
│ │ ├── vertical-sticky-content-scroll-no-gutters-overflow-top.svg
│ │ ├── vertical-sticky-content-scroll-no-gutters.svg
│ │ ├── vertical-sticky-content-scroll-with-gutters-overflow-bottom.svg
│ │ ├── vertical-sticky-content-scroll-with-gutters-overflow-top.svg
│ │ ├── vertical-sticky-content-scroll-with-gutters.svg
│ │ ├── vertical-sticky-layout-no-gutters-overflow-bottom.svg
│ │ ├── vertical-sticky-layout-no-gutters-overflow-top.svg
│ │ ├── vertical-sticky-layout-no-gutters.svg
│ │ ├── vertical-sticky-layout-with-gutters-overflow-bottom.svg
│ │ ├── vertical-sticky-layout-with-gutters-overflow-top.svg
│ │ └── vertical-sticky-layout-with-gutters.svg
│ ├── index.md
│ ├── next
│ │ ├── component-dev-guide.md
│ │ ├── configuration-management-enhancement-summary.md
│ │ ├── documentation-scripts-refactoring-complete-summary.md
│ │ ├── documentation-scripts-refactoring-plan.md
│ │ ├── duplicate-pattern-extraction-summary.md
│ │ ├── error-handling-standardization-summary.md
│ │ ├── generating-component-reference.md
│ │ ├── index.md
│ │ ├── logging-consistency-implementation-summary.md
│ │ ├── project-build.md
│ │ ├── project-structure.md
│ │ ├── theme-context.md
│ │ ├── tiptap-design-considerations.md
│ │ ├── working-with-code.md
│ │ ├── xmlui-runtime-architecture
│ │ └── xmlui-wcag-accessibility-report.md
│ ├── react-fundamentals.md
│ ├── release-method.md
│ ├── standalone-app.md
│ ├── svg-diagram-rules.md
│ ├── theme-variables-refactoring.md
│ ├── ud-components.md
│ └── xmlui-repo.md
├── package.json
├── scripts
│ ├── coverage-only.js
│ ├── e2e-test-summary.js
│ ├── extract-component-metadata.js
│ ├── generate-docs
│ │ ├── build-downloads-map.mjs
│ │ ├── build-pages-map.mjs
│ │ ├── components-config.json
│ │ ├── configuration-management.mjs
│ │ ├── constants.mjs
│ │ ├── create-theme-files.mjs
│ │ ├── DocsGenerator.mjs
│ │ ├── error-handling.mjs
│ │ ├── extensions-config.json
│ │ ├── folders.mjs
│ │ ├── generate-summary-files.mjs
│ │ ├── get-docs.mjs
│ │ ├── input-handler.mjs
│ │ ├── logger.mjs
│ │ ├── logging-standards.mjs
│ │ ├── MetadataProcessor.mjs
│ │ ├── pattern-utilities.mjs
│ │ └── utils.mjs
│ ├── generate-metadata-markdown.js
│ ├── get-langserver-metadata.js
│ ├── inline-links.mjs
│ └── README-e2e-summary.md
├── src
│ ├── abstractions
│ │ ├── _conventions.md
│ │ ├── ActionDefs.ts
│ │ ├── AppContextDefs.ts
│ │ ├── ComponentDefs.ts
│ │ ├── ContainerDefs.ts
│ │ ├── ExtensionDefs.ts
│ │ ├── FunctionDefs.ts
│ │ ├── RendererDefs.ts
│ │ ├── scripting
│ │ │ ├── BlockScope.ts
│ │ │ ├── Compilation.ts
│ │ │ ├── LogicalThread.ts
│ │ │ ├── LoopScope.ts
│ │ │ ├── modules.ts
│ │ │ ├── ScriptParserError.ts
│ │ │ ├── Token.ts
│ │ │ ├── TryScope.ts
│ │ │ └── TryScopeExp.ts
│ │ └── ThemingDefs.ts
│ ├── components
│ │ ├── _conventions.md
│ │ ├── abstractions.ts
│ │ ├── Accordion
│ │ │ ├── Accordion.md
│ │ │ ├── Accordion.module.scss
│ │ │ ├── Accordion.spec.ts
│ │ │ ├── Accordion.tsx
│ │ │ ├── AccordionContext.tsx
│ │ │ ├── AccordionItem.tsx
│ │ │ ├── AccordionItemNative.tsx
│ │ │ └── AccordionNative.tsx
│ │ ├── Animation
│ │ │ └── AnimationNative.tsx
│ │ ├── APICall
│ │ │ ├── APICall.md
│ │ │ ├── APICall.spec.ts
│ │ │ ├── APICall.tsx
│ │ │ └── APICallNative.tsx
│ │ ├── App
│ │ │ ├── App-layout.spec.ts
│ │ │ ├── App.md
│ │ │ ├── App.module.scss
│ │ │ ├── App.spec.ts
│ │ │ ├── App.tsx
│ │ │ ├── AppLayoutContext.ts
│ │ │ ├── AppNative.tsx
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── App2
│ │ │ ├── app-refactor.md
│ │ │ ├── App2-layout-mobile.spec.ts
│ │ │ ├── App2-layout.spec.ts
│ │ │ ├── App2.md
│ │ │ ├── App2.module.scss
│ │ │ ├── App2.spec.ts
│ │ │ ├── App2.tsx
│ │ │ ├── App2Native.tsx
│ │ │ ├── App2Navigation.ts
│ │ │ ├── AppStateContext.ts
│ │ │ ├── doc-resources
│ │ │ │ ├── condensed-sticky.xmlui
│ │ │ │ ├── condensed.xmlui
│ │ │ │ ├── horizontal-sticky.xmlui
│ │ │ │ ├── horizontal.xmlui
│ │ │ │ ├── vertical-full-header.xmlui
│ │ │ │ ├── vertical-sticky.xmlui
│ │ │ │ └── vertical.xmlui
│ │ │ ├── IndexerContext.ts
│ │ │ ├── LinkInfoContext.ts
│ │ │ ├── SearchContext.tsx
│ │ │ ├── SearchIndexCollector.tsx
│ │ │ ├── Sheet.module.scss
│ │ │ └── Sheet.tsx
│ │ ├── AppHeader
│ │ │ ├── AppHeader.md
│ │ │ ├── AppHeader.module.scss
│ │ │ ├── AppHeader.spec.ts
│ │ │ ├── AppHeader.tsx
│ │ │ └── AppHeaderNative.tsx
│ │ ├── AppState
│ │ │ ├── AppState.md
│ │ │ ├── AppState.spec.ts
│ │ │ ├── AppState.tsx
│ │ │ └── AppStateNative.tsx
│ │ ├── AutoComplete
│ │ │ ├── AutoComplete.md
│ │ │ ├── AutoComplete.module.scss
│ │ │ ├── AutoComplete.spec.ts
│ │ │ ├── AutoComplete.tsx
│ │ │ ├── AutoCompleteContext.tsx
│ │ │ └── AutoCompleteNative.tsx
│ │ ├── Avatar
│ │ │ ├── Avatar.md
│ │ │ ├── Avatar.module.scss
│ │ │ ├── Avatar.spec.ts
│ │ │ ├── Avatar.tsx
│ │ │ └── AvatarNative.tsx
│ │ ├── Backdrop
│ │ │ ├── Backdrop.md
│ │ │ ├── Backdrop.module.scss
│ │ │ ├── Backdrop.spec.ts
│ │ │ ├── Backdrop.tsx
│ │ │ └── BackdropNative.tsx
│ │ ├── Badge
│ │ │ ├── Badge.md
│ │ │ ├── Badge.module.scss
│ │ │ ├── Badge.spec.ts
│ │ │ ├── Badge.tsx
│ │ │ └── BadgeNative.tsx
│ │ ├── Bookmark
│ │ │ ├── Bookmark.md
│ │ │ ├── Bookmark.module.scss
│ │ │ ├── Bookmark.spec.ts
│ │ │ ├── Bookmark.tsx
│ │ │ └── BookmarkNative.tsx
│ │ ├── Breakout
│ │ │ ├── Breakout.module.scss
│ │ │ ├── Breakout.spec.ts
│ │ │ ├── Breakout.tsx
│ │ │ └── BreakoutNative.tsx
│ │ ├── Button
│ │ │ ├── Button-style.spec.ts
│ │ │ ├── Button.md
│ │ │ ├── Button.module.scss
│ │ │ ├── Button.spec.ts
│ │ │ ├── Button.tsx
│ │ │ └── ButtonNative.tsx
│ │ ├── Card
│ │ │ ├── Card.md
│ │ │ ├── Card.module.scss
│ │ │ ├── Card.spec.ts
│ │ │ ├── Card.tsx
│ │ │ └── CardNative.tsx
│ │ ├── Carousel
│ │ │ ├── Carousel.md
│ │ │ ├── Carousel.module.scss
│ │ │ ├── Carousel.spec.ts
│ │ │ ├── Carousel.tsx
│ │ │ ├── CarouselContext.tsx
│ │ │ ├── CarouselItem.tsx
│ │ │ ├── CarouselItemNative.tsx
│ │ │ └── CarouselNative.tsx
│ │ ├── ChangeListener
│ │ │ ├── ChangeListener.md
│ │ │ ├── ChangeListener.spec.ts
│ │ │ ├── ChangeListener.tsx
│ │ │ └── ChangeListenerNative.tsx
│ │ ├── chart-color-schemes.ts
│ │ ├── Charts
│ │ │ ├── AreaChart
│ │ │ │ ├── AreaChart.md
│ │ │ │ ├── AreaChart.spec.ts
│ │ │ │ ├── AreaChart.tsx
│ │ │ │ └── AreaChartNative.tsx
│ │ │ ├── BarChart
│ │ │ │ ├── BarChart.md
│ │ │ │ ├── BarChart.module.scss
│ │ │ │ ├── BarChart.spec.ts
│ │ │ │ ├── BarChart.tsx
│ │ │ │ └── BarChartNative.tsx
│ │ │ ├── DonutChart
│ │ │ │ ├── DonutChart.spec.ts
│ │ │ │ └── DonutChart.tsx
│ │ │ ├── LabelList
│ │ │ │ ├── LabelList.module.scss
│ │ │ │ ├── LabelList.spec.ts
│ │ │ │ ├── LabelList.tsx
│ │ │ │ └── LabelListNative.tsx
│ │ │ ├── Legend
│ │ │ │ ├── Legend.spec.ts
│ │ │ │ ├── Legend.tsx
│ │ │ │ └── LegendNative.tsx
│ │ │ ├── LineChart
│ │ │ │ ├── LineChart.md
│ │ │ │ ├── LineChart.module.scss
│ │ │ │ ├── LineChart.spec.ts
│ │ │ │ ├── LineChart.tsx
│ │ │ │ └── LineChartNative.tsx
│ │ │ ├── PieChart
│ │ │ │ ├── PieChart.md
│ │ │ │ ├── PieChart.spec.ts
│ │ │ │ ├── PieChart.tsx
│ │ │ │ ├── PieChartNative.module.scss
│ │ │ │ └── PieChartNative.tsx
│ │ │ ├── RadarChart
│ │ │ │ ├── RadarChart.md
│ │ │ │ ├── RadarChart.spec.ts
│ │ │ │ ├── RadarChart.tsx
│ │ │ │ └── RadarChartNative.tsx
│ │ │ ├── Tooltip
│ │ │ │ ├── TooltipContent.module.scss
│ │ │ │ ├── TooltipContent.spec.ts
│ │ │ │ └── TooltipContent.tsx
│ │ │ └── utils
│ │ │ ├── abstractions.ts
│ │ │ └── ChartProvider.tsx
│ │ ├── Checkbox
│ │ │ ├── Checkbox.md
│ │ │ ├── Checkbox.spec.ts
│ │ │ └── Checkbox.tsx
│ │ ├── CodeBlock
│ │ │ ├── CodeBlock.module.scss
│ │ │ ├── CodeBlock.spec.ts
│ │ │ ├── CodeBlock.tsx
│ │ │ ├── CodeBlockNative.tsx
│ │ │ └── highlight-code.ts
│ │ ├── collectedComponentMetadata.ts
│ │ ├── ColorPicker
│ │ │ ├── ColorPicker.md
│ │ │ ├── ColorPicker.module.scss
│ │ │ ├── ColorPicker.spec.ts
│ │ │ ├── ColorPicker.tsx
│ │ │ └── ColorPickerNative.tsx
│ │ ├── Column
│ │ │ ├── Column.md
│ │ │ ├── Column.tsx
│ │ │ ├── ColumnNative.tsx
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ └── TableContext.tsx
│ │ ├── component-utils.ts
│ │ ├── ComponentProvider.tsx
│ │ ├── ComponentRegistryContext.tsx
│ │ ├── container-helpers.tsx
│ │ ├── ContentSeparator
│ │ │ ├── ContentSeparator.md
│ │ │ ├── ContentSeparator.module.scss
│ │ │ ├── ContentSeparator.spec.ts
│ │ │ ├── ContentSeparator.tsx
│ │ │ ├── ContentSeparatorNative.tsx
│ │ │ └── test-padding.xmlui
│ │ ├── DataSource
│ │ │ ├── DataSource.md
│ │ │ └── DataSource.tsx
│ │ ├── DateInput
│ │ │ ├── DateInput.md
│ │ │ ├── DateInput.module.scss
│ │ │ ├── DateInput.spec.ts
│ │ │ ├── DateInput.tsx
│ │ │ └── DateInputNative.tsx
│ │ ├── DatePicker
│ │ │ ├── DatePicker.md
│ │ │ ├── DatePicker.module.scss
│ │ │ ├── DatePicker.spec.ts
│ │ │ ├── DatePicker.tsx
│ │ │ └── DatePickerNative.tsx
│ │ ├── DropdownMenu
│ │ │ ├── DropdownMenu.md
│ │ │ ├── DropdownMenu.module.scss
│ │ │ ├── DropdownMenu.spec.ts
│ │ │ ├── DropdownMenu.tsx
│ │ │ ├── DropdownMenuNative.tsx
│ │ │ ├── MenuItem.md
│ │ │ └── SubMenuItem.md
│ │ ├── EmojiSelector
│ │ │ ├── EmojiSelector.md
│ │ │ ├── EmojiSelector.spec.ts
│ │ │ ├── EmojiSelector.tsx
│ │ │ └── EmojiSelectorNative.tsx
│ │ ├── ExpandableItem
│ │ │ ├── ExpandableItem.module.scss
│ │ │ ├── ExpandableItem.spec.ts
│ │ │ ├── ExpandableItem.tsx
│ │ │ └── ExpandableItemNative.tsx
│ │ ├── FileInput
│ │ │ ├── FileInput.md
│ │ │ ├── FileInput.module.scss
│ │ │ ├── FileInput.spec.ts
│ │ │ ├── FileInput.tsx
│ │ │ └── FileInputNative.tsx
│ │ ├── FileUploadDropZone
│ │ │ ├── FileUploadDropZone.md
│ │ │ ├── FileUploadDropZone.module.scss
│ │ │ ├── FileUploadDropZone.spec.ts
│ │ │ ├── FileUploadDropZone.tsx
│ │ │ └── FileUploadDropZoneNative.tsx
│ │ ├── FlowLayout
│ │ │ ├── FlowLayout.md
│ │ │ ├── FlowLayout.module.scss
│ │ │ ├── FlowLayout.spec.ts
│ │ │ ├── FlowLayout.spec.ts-snapshots
│ │ │ │ └── Edge-cases-boxShadow-is-not-clipped-1-non-smoke-darwin.png
│ │ │ ├── FlowLayout.tsx
│ │ │ └── FlowLayoutNative.tsx
│ │ ├── Footer
│ │ │ ├── Footer.md
│ │ │ ├── Footer.module.scss
│ │ │ ├── Footer.spec.ts
│ │ │ ├── Footer.tsx
│ │ │ └── FooterNative.tsx
│ │ ├── Form
│ │ │ ├── Form.md
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.ts
│ │ │ ├── Form.tsx
│ │ │ ├── formActions.ts
│ │ │ ├── FormContext.ts
│ │ │ └── FormNative.tsx
│ │ ├── FormItem
│ │ │ ├── FormItem.md
│ │ │ ├── FormItem.module.scss
│ │ │ ├── FormItem.spec.ts
│ │ │ ├── FormItem.tsx
│ │ │ ├── FormItemNative.tsx
│ │ │ ├── HelperText.module.scss
│ │ │ ├── HelperText.tsx
│ │ │ ├── ItemWithLabel.tsx
│ │ │ └── Validations.ts
│ │ ├── FormSection
│ │ │ ├── FormSection.md
│ │ │ ├── FormSection.ts
│ │ │ └── FormSection.xmlui
│ │ ├── Fragment
│ │ │ ├── Fragment.spec.ts
│ │ │ └── Fragment.tsx
│ │ ├── Heading
│ │ │ ├── abstractions.ts
│ │ │ ├── H1.md
│ │ │ ├── H1.spec.ts
│ │ │ ├── H2.md
│ │ │ ├── H2.spec.ts
│ │ │ ├── H3.md
│ │ │ ├── H3.spec.ts
│ │ │ ├── H4.md
│ │ │ ├── H4.spec.ts
│ │ │ ├── H5.md
│ │ │ ├── H5.spec.ts
│ │ │ ├── H6.md
│ │ │ ├── H6.spec.ts
│ │ │ ├── Heading.md
│ │ │ ├── Heading.module.scss
│ │ │ ├── Heading.spec.ts
│ │ │ ├── Heading.tsx
│ │ │ └── HeadingNative.tsx
│ │ ├── HoverCard
│ │ │ ├── HoverCard.tsx
│ │ │ └── HovercardNative.tsx
│ │ ├── HtmlTags
│ │ │ ├── HtmlTags.module.scss
│ │ │ ├── HtmlTags.spec.ts
│ │ │ └── HtmlTags.tsx
│ │ ├── Icon
│ │ │ ├── AdmonitionDanger.tsx
│ │ │ ├── AdmonitionInfo.tsx
│ │ │ ├── AdmonitionNote.tsx
│ │ │ ├── AdmonitionTip.tsx
│ │ │ ├── AdmonitionWarning.tsx
│ │ │ ├── ApiIcon.tsx
│ │ │ ├── ArrowDropDown.module.scss
│ │ │ ├── ArrowDropDown.tsx
│ │ │ ├── ArrowDropUp.module.scss
│ │ │ ├── ArrowDropUp.tsx
│ │ │ ├── ArrowLeft.module.scss
│ │ │ ├── ArrowLeft.tsx
│ │ │ ├── ArrowRight.module.scss
│ │ │ ├── ArrowRight.tsx
│ │ │ ├── Attach.tsx
│ │ │ ├── Binding.module.scss
│ │ │ ├── Binding.tsx
│ │ │ ├── BoardIcon.tsx
│ │ │ ├── BoxIcon.tsx
│ │ │ ├── CheckIcon.tsx
│ │ │ ├── ChevronDownIcon.tsx
│ │ │ ├── ChevronLeft.tsx
│ │ │ ├── ChevronRight.tsx
│ │ │ ├── ChevronUpIcon.tsx
│ │ │ ├── CodeFileIcon.tsx
│ │ │ ├── CodeSandbox.tsx
│ │ │ ├── CompactListIcon.tsx
│ │ │ ├── ContentCopyIcon.tsx
│ │ │ ├── DarkToLightIcon.tsx
│ │ │ ├── DatabaseIcon.module.scss
│ │ │ ├── DatabaseIcon.tsx
│ │ │ ├── DocFileIcon.tsx
│ │ │ ├── DocIcon.tsx
│ │ │ ├── DotMenuHorizontalIcon.tsx
│ │ │ ├── DotMenuIcon.tsx
│ │ │ ├── EmailIcon.tsx
│ │ │ ├── EmptyFolderIcon.tsx
│ │ │ ├── ErrorIcon.tsx
│ │ │ ├── ExpressionIcon.tsx
│ │ │ ├── FillPlusCricleIcon.tsx
│ │ │ ├── FilterIcon.tsx
│ │ │ ├── FolderIcon.tsx
│ │ │ ├── GlobeIcon.tsx
│ │ │ ├── HomeIcon.tsx
│ │ │ ├── HyperLinkIcon.tsx
│ │ │ ├── Icon.md
│ │ │ ├── Icon.module.scss
│ │ │ ├── Icon.spec.ts
│ │ │ ├── Icon.tsx
│ │ │ ├── IconNative.tsx
│ │ │ ├── ImageFileIcon.tsx
│ │ │ ├── Inspect.tsx
│ │ │ ├── LightToDark.tsx
│ │ │ ├── LinkIcon.tsx
│ │ │ ├── ListIcon.tsx
│ │ │ ├── LooseListIcon.tsx
│ │ │ ├── MoonIcon.tsx
│ │ │ ├── MoreOptionsIcon.tsx
│ │ │ ├── NoSortIcon.tsx
│ │ │ ├── PDFIcon.tsx
│ │ │ ├── PenIcon.tsx
│ │ │ ├── PhoneIcon.tsx
│ │ │ ├── PhotoIcon.tsx
│ │ │ ├── PlusIcon.tsx
│ │ │ ├── SearchIcon.tsx
│ │ │ ├── ShareIcon.tsx
│ │ │ ├── SortAscendingIcon.tsx
│ │ │ ├── SortDescendingIcon.tsx
│ │ │ ├── StarsIcon.tsx
│ │ │ ├── SunIcon.tsx
│ │ │ ├── svg
│ │ │ │ ├── admonition_danger.svg
│ │ │ │ ├── admonition_info.svg
│ │ │ │ ├── admonition_note.svg
│ │ │ │ ├── admonition_tip.svg
│ │ │ │ ├── admonition_warning.svg
│ │ │ │ ├── api.svg
│ │ │ │ ├── arrow-dropdown.svg
│ │ │ │ ├── arrow-left.svg
│ │ │ │ ├── arrow-right.svg
│ │ │ │ ├── arrow-up.svg
│ │ │ │ ├── attach.svg
│ │ │ │ ├── binding.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── bulb.svg
│ │ │ │ ├── code-file.svg
│ │ │ │ ├── code-sandbox.svg
│ │ │ │ ├── dark_to_light.svg
│ │ │ │ ├── database.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── empty-folder.svg
│ │ │ │ ├── expression.svg
│ │ │ │ ├── eye-closed.svg
│ │ │ │ ├── eye-dark.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── file-text.svg
│ │ │ │ ├── filter.svg
│ │ │ │ ├── folder.svg
│ │ │ │ ├── img.svg
│ │ │ │ ├── inspect.svg
│ │ │ │ ├── light_to_dark.svg
│ │ │ │ ├── moon.svg
│ │ │ │ ├── pdf.svg
│ │ │ │ ├── photo.svg
│ │ │ │ ├── share.svg
│ │ │ │ ├── stars.svg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── trending-down.svg
│ │ │ │ ├── trending-level.svg
│ │ │ │ ├── trending-up.svg
│ │ │ │ ├── txt.svg
│ │ │ │ ├── unknown-file.svg
│ │ │ │ ├── unlink.svg
│ │ │ │ └── xls.svg
│ │ │ ├── TableDeleteColumnIcon.tsx
│ │ │ ├── TableDeleteRowIcon.tsx
│ │ │ ├── TableInsertColumnIcon.tsx
│ │ │ ├── TableInsertRowIcon.tsx
│ │ │ ├── TrashIcon.tsx
│ │ │ ├── TrendingDownIcon.tsx
│ │ │ ├── TrendingLevelIcon.tsx
│ │ │ ├── TrendingUpIcon.tsx
│ │ │ ├── TxtIcon.tsx
│ │ │ ├── UnknownFileIcon.tsx
│ │ │ ├── UnlinkIcon.tsx
│ │ │ ├── UserIcon.tsx
│ │ │ ├── WarningIcon.tsx
│ │ │ └── XlsIcon.tsx
│ │ ├── IconProvider.tsx
│ │ ├── IconRegistryContext.tsx
│ │ ├── IFrame
│ │ │ ├── IFrame.md
│ │ │ ├── IFrame.module.scss
│ │ │ ├── IFrame.spec.ts
│ │ │ ├── IFrame.tsx
│ │ │ └── IFrameNative.tsx
│ │ ├── Image
│ │ │ ├── Image.md
│ │ │ ├── Image.module.scss
│ │ │ ├── Image.spec.ts
│ │ │ ├── Image.tsx
│ │ │ └── ImageNative.tsx
│ │ ├── Input
│ │ │ ├── index.ts
│ │ │ ├── InputAdornment.module.scss
│ │ │ ├── InputAdornment.tsx
│ │ │ ├── InputDivider.module.scss
│ │ │ ├── InputDivider.tsx
│ │ │ ├── InputLabel.module.scss
│ │ │ ├── InputLabel.tsx
│ │ │ ├── PartialInput.module.scss
│ │ │ └── PartialInput.tsx
│ │ ├── InspectButton
│ │ │ ├── InspectButton.module.scss
│ │ │ └── InspectButton.tsx
│ │ ├── Items
│ │ │ ├── Items.md
│ │ │ ├── Items.spec.ts
│ │ │ ├── Items.tsx
│ │ │ └── ItemsNative.tsx
│ │ ├── Link
│ │ │ ├── Link.md
│ │ │ ├── Link.module.scss
│ │ │ ├── Link.spec.ts
│ │ │ ├── Link.tsx
│ │ │ └── LinkNative.tsx
│ │ ├── List
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── List.md
│ │ │ ├── List.module.scss
│ │ │ ├── List.spec.ts
│ │ │ ├── List.tsx
│ │ │ └── ListNative.tsx
│ │ ├── Logo
│ │ │ ├── doc-resources
│ │ │ │ └── xmlui-logo.svg
│ │ │ ├── Logo.md
│ │ │ ├── Logo.tsx
│ │ │ └── LogoNative.tsx
│ │ ├── Markdown
│ │ │ ├── CodeText.module.scss
│ │ │ ├── CodeText.tsx
│ │ │ ├── Markdown.md
│ │ │ ├── Markdown.module.scss
│ │ │ ├── Markdown.spec.ts
│ │ │ ├── Markdown.tsx
│ │ │ ├── MarkdownNative.tsx
│ │ │ ├── parse-binding-expr.ts
│ │ │ └── utils.ts
│ │ ├── metadata-helpers.ts
│ │ ├── ModalDialog
│ │ │ ├── ConfirmationModalContextProvider.tsx
│ │ │ ├── Dialog.module.scss
│ │ │ ├── Dialog.tsx
│ │ │ ├── ModalDialog.md
│ │ │ ├── ModalDialog.module.scss
│ │ │ ├── ModalDialog.spec.ts
│ │ │ ├── ModalDialog.tsx
│ │ │ ├── ModalDialogNative.tsx
│ │ │ └── ModalVisibilityContext.tsx
│ │ ├── NavGroup
│ │ │ ├── NavGroup.md
│ │ │ ├── NavGroup.module.scss
│ │ │ ├── NavGroup.spec.ts
│ │ │ ├── NavGroup.tsx
│ │ │ ├── NavGroupContext.ts
│ │ │ └── NavGroupNative.tsx
│ │ ├── NavLink
│ │ │ ├── NavLink.md
│ │ │ ├── NavLink.module.scss
│ │ │ ├── NavLink.spec.ts
│ │ │ ├── NavLink.tsx
│ │ │ └── NavLinkNative.tsx
│ │ ├── NavPanel
│ │ │ ├── NavPanel.md
│ │ │ ├── NavPanel.module.scss
│ │ │ ├── NavPanel.spec.ts
│ │ │ ├── NavPanel.tsx
│ │ │ └── NavPanelNative.tsx
│ │ ├── NestedApp
│ │ │ ├── AppWithCodeView.module.scss
│ │ │ ├── AppWithCodeView.tsx
│ │ │ ├── AppWithCodeViewNative.tsx
│ │ │ ├── defaultProps.tsx
│ │ │ ├── logo.svg
│ │ │ ├── NestedApp.module.scss
│ │ │ ├── NestedApp.tsx
│ │ │ ├── NestedAppNative.tsx
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.tsx
│ │ │ └── utils.ts
│ │ ├── NoResult
│ │ │ ├── NoResult.md
│ │ │ ├── NoResult.module.scss
│ │ │ ├── NoResult.spec.ts
│ │ │ ├── NoResult.tsx
│ │ │ └── NoResultNative.tsx
│ │ ├── NumberBox
│ │ │ ├── numberbox-abstractions.ts
│ │ │ ├── NumberBox.md
│ │ │ ├── NumberBox.module.scss
│ │ │ ├── NumberBox.spec.ts
│ │ │ ├── NumberBox.tsx
│ │ │ └── NumberBoxNative.tsx
│ │ ├── Option
│ │ │ ├── Option.md
│ │ │ ├── Option.spec.ts
│ │ │ ├── Option.tsx
│ │ │ ├── OptionNative.tsx
│ │ │ └── OptionTypeProvider.tsx
│ │ ├── PageMetaTitle
│ │ │ ├── PageMetaTilteNative.tsx
│ │ │ ├── PageMetaTitle.md
│ │ │ ├── PageMetaTitle.spec.ts
│ │ │ └── PageMetaTitle.tsx
│ │ ├── Pages
│ │ │ ├── Page.md
│ │ │ ├── Pages.md
│ │ │ ├── Pages.module.scss
│ │ │ ├── Pages.tsx
│ │ │ └── PagesNative.tsx
│ │ ├── Pagination
│ │ │ ├── Pagination.md
│ │ │ ├── Pagination.module.scss
│ │ │ ├── Pagination.spec.ts
│ │ │ ├── Pagination.tsx
│ │ │ └── PaginationNative.tsx
│ │ ├── Part
│ │ │ └── Part.tsx
│ │ ├── PositionedContainer
│ │ │ ├── PositionedContainer.module.scss
│ │ │ ├── PositionedContainer.tsx
│ │ │ └── PositionedContainerNative.tsx
│ │ ├── ProfileMenu
│ │ │ ├── ProfileMenu.module.scss
│ │ │ └── ProfileMenu.tsx
│ │ ├── ProgressBar
│ │ │ ├── ProgressBar.md
│ │ │ ├── ProgressBar.module.scss
│ │ │ ├── ProgressBar.spec.ts
│ │ │ ├── ProgressBar.tsx
│ │ │ └── ProgressBarNative.tsx
│ │ ├── Queue
│ │ │ ├── Queue.md
│ │ │ ├── Queue.spec.ts
│ │ │ ├── Queue.tsx
│ │ │ ├── queueActions.ts
│ │ │ └── QueueNative.tsx
│ │ ├── RadioGroup
│ │ │ ├── RadioGroup.md
│ │ │ ├── RadioGroup.module.scss
│ │ │ ├── RadioGroup.spec.ts
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── RadioGroupNative.tsx
│ │ │ ├── RadioItem.tsx
│ │ │ └── RadioItemNative.tsx
│ │ ├── RealTimeAdapter
│ │ │ ├── RealTimeAdapter.tsx
│ │ │ └── RealTimeAdapterNative.tsx
│ │ ├── Redirect
│ │ │ ├── Redirect.md
│ │ │ ├── Redirect.spec.ts
│ │ │ └── Redirect.tsx
│ │ ├── ResponsiveBar
│ │ │ ├── README.md
│ │ │ ├── ResponsiveBar.md
│ │ │ ├── ResponsiveBar.module.scss
│ │ │ ├── ResponsiveBar.spec.ts
│ │ │ ├── ResponsiveBar.tsx
│ │ │ ├── ResponsiveBarItem.tsx
│ │ │ └── ResponsiveBarNative.tsx
│ │ ├── Select
│ │ │ ├── HiddenOption.tsx
│ │ │ ├── MultiSelectOption.tsx
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ ├── SelectNative.tsx
│ │ │ ├── SelectOption.tsx
│ │ │ └── SimpleSelect.tsx
│ │ ├── SelectionStore
│ │ │ ├── SelectionStore.md
│ │ │ ├── SelectionStore.tsx
│ │ │ └── SelectionStoreNative.tsx
│ │ ├── Slider
│ │ │ ├── Slider.md
│ │ │ ├── Slider.module.scss
│ │ │ ├── Slider.spec.ts
│ │ │ ├── Slider.tsx
│ │ │ └── SliderNative.tsx
│ │ ├── Slot
│ │ │ ├── Slot.md
│ │ │ ├── Slot.spec.ts
│ │ │ └── Slot.ts
│ │ ├── SlotItem.tsx
│ │ ├── SpaceFiller
│ │ │ ├── SpaceFiller.md
│ │ │ ├── SpaceFiller.module.scss
│ │ │ ├── SpaceFiller.spec.ts
│ │ │ ├── SpaceFiller.tsx
│ │ │ └── SpaceFillerNative.tsx
│ │ ├── Spinner
│ │ │ ├── Spinner.md
│ │ │ ├── Spinner.module.scss
│ │ │ ├── Spinner.spec.ts
│ │ │ ├── Spinner.tsx
│ │ │ └── SpinnerNative.tsx
│ │ ├── Splitter
│ │ │ ├── HSplitter.md
│ │ │ ├── HSplitter.spec.ts
│ │ │ ├── Splitter.md
│ │ │ ├── Splitter.module.scss
│ │ │ ├── Splitter.spec.ts
│ │ │ ├── Splitter.tsx
│ │ │ ├── SplitterNative.tsx
│ │ │ ├── utils.ts
│ │ │ ├── VSplitter.md
│ │ │ └── VSplitter.spec.ts
│ │ ├── Stack
│ │ │ ├── CHStack.md
│ │ │ ├── CHStack.spec.ts
│ │ │ ├── CVStack.md
│ │ │ ├── CVStack.spec.ts
│ │ │ ├── HStack.md
│ │ │ ├── HStack.spec.ts
│ │ │ ├── Stack.md
│ │ │ ├── Stack.module.scss
│ │ │ ├── Stack.spec.ts
│ │ │ ├── Stack.tsx
│ │ │ ├── StackNative.tsx
│ │ │ ├── VStack.md
│ │ │ └── VStack.spec.ts
│ │ ├── StickyBox
│ │ │ ├── StickyBox.md
│ │ │ ├── StickyBox.module.scss
│ │ │ ├── StickyBox.tsx
│ │ │ └── StickyBoxNative.tsx
│ │ ├── Switch
│ │ │ ├── Switch.md
│ │ │ ├── Switch.spec.ts
│ │ │ └── Switch.tsx
│ │ ├── Table
│ │ │ ├── doc-resources
│ │ │ │ └── list-component-data.js
│ │ │ ├── react-table-config.d.ts
│ │ │ ├── Table.md
│ │ │ ├── Table.module.scss
│ │ │ ├── Table.spec.ts
│ │ │ ├── Table.tsx
│ │ │ ├── TableNative.tsx
│ │ │ └── useRowSelection.tsx
│ │ ├── TableOfContents
│ │ │ ├── TableOfContents.module.scss
│ │ │ ├── TableOfContents.spec.ts
│ │ │ ├── TableOfContents.tsx
│ │ │ └── TableOfContentsNative.tsx
│ │ ├── Tabs
│ │ │ ├── TabContext.tsx
│ │ │ ├── TabItem.md
│ │ │ ├── TabItem.tsx
│ │ │ ├── TabItemNative.tsx
│ │ │ ├── Tabs.md
│ │ │ ├── Tabs.module.scss
│ │ │ ├── Tabs.spec.ts
│ │ │ ├── Tabs.tsx
│ │ │ └── TabsNative.tsx
│ │ ├── Text
│ │ │ ├── Text.md
│ │ │ ├── Text.module.scss
│ │ │ ├── Text.spec.ts
│ │ │ ├── Text.tsx
│ │ │ └── TextNative.tsx
│ │ ├── TextArea
│ │ │ ├── TextArea.md
│ │ │ ├── TextArea.module.scss
│ │ │ ├── TextArea.spec.ts
│ │ │ ├── TextArea.tsx
│ │ │ ├── TextAreaNative.tsx
│ │ │ ├── TextAreaResizable.tsx
│ │ │ └── useComposedRef.ts
│ │ ├── TextBox
│ │ │ ├── TextBox.md
│ │ │ ├── TextBox.module.scss
│ │ │ ├── TextBox.spec.ts
│ │ │ ├── TextBox.tsx
│ │ │ └── TextBoxNative.tsx
│ │ ├── Theme
│ │ │ ├── NotificationToast.tsx
│ │ │ ├── Theme.md
│ │ │ ├── Theme.module.scss
│ │ │ ├── Theme.spec.ts
│ │ │ ├── Theme.tsx
│ │ │ └── ThemeNative.tsx
│ │ ├── TimeInput
│ │ │ ├── TimeInput.md
│ │ │ ├── TimeInput.module.scss
│ │ │ ├── TimeInput.spec.ts
│ │ │ ├── TimeInput.tsx
│ │ │ ├── TimeInputNative.tsx
│ │ │ └── utils.ts
│ │ ├── Timer
│ │ │ ├── Timer.md
│ │ │ ├── Timer.spec.ts
│ │ │ ├── Timer.tsx
│ │ │ └── TimerNative.tsx
│ │ ├── Toast
│ │ │ ├── Toast.spec.ts
│ │ │ ├── Toast.tsx
│ │ │ └── ToastNative.tsx
│ │ ├── Toggle
│ │ │ ├── Toggle.module.scss
│ │ │ └── Toggle.tsx
│ │ ├── ToneChangerButton
│ │ │ ├── ToneChangerButton.md
│ │ │ ├── ToneChangerButton.spec.ts
│ │ │ └── ToneChangerButton.tsx
│ │ ├── ToneSwitch
│ │ │ ├── ToneSwitch.md
│ │ │ ├── ToneSwitch.module.scss
│ │ │ ├── ToneSwitch.spec.ts
│ │ │ ├── ToneSwitch.tsx
│ │ │ └── ToneSwitchNative.tsx
│ │ ├── Tooltip
│ │ │ ├── Tooltip.md
│ │ │ ├── Tooltip.module.scss
│ │ │ ├── Tooltip.spec.ts
│ │ │ ├── Tooltip.tsx
│ │ │ └── TooltipNative.tsx
│ │ ├── Tree
│ │ │ ├── testData.ts
│ │ │ ├── Tree-dynamic.spec.ts
│ │ │ ├── Tree-icons.spec.ts
│ │ │ ├── Tree.md
│ │ │ ├── Tree.spec.ts
│ │ │ ├── TreeComponent.module.scss
│ │ │ ├── TreeComponent.tsx
│ │ │ └── TreeNative.tsx
│ │ ├── TreeDisplay
│ │ │ ├── TreeDisplay.md
│ │ │ ├── TreeDisplay.module.scss
│ │ │ ├── TreeDisplay.tsx
│ │ │ └── TreeDisplayNative.tsx
│ │ ├── ValidationSummary
│ │ │ ├── ValidationSummary.module.scss
│ │ │ └── ValidationSummary.tsx
│ │ └── VisuallyHidden.tsx
│ ├── components-core
│ │ ├── abstractions
│ │ │ ├── ComponentRenderer.ts
│ │ │ ├── LoaderRenderer.ts
│ │ │ ├── standalone.ts
│ │ │ └── treeAbstractions.ts
│ │ ├── action
│ │ │ ├── actions.ts
│ │ │ ├── APICall.tsx
│ │ │ ├── FileDownloadAction.tsx
│ │ │ ├── FileUploadAction.tsx
│ │ │ ├── NavigateAction.tsx
│ │ │ └── TimedAction.tsx
│ │ ├── ApiBoundComponent.tsx
│ │ ├── appContext
│ │ │ ├── date-functions.ts
│ │ │ ├── math-function.ts
│ │ │ └── misc-utils.ts
│ │ ├── AppContext.tsx
│ │ ├── behaviors
│ │ │ ├── Behavior.tsx
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.js
│ ├── logging
│ │ ├── LoggerContext.tsx
│ │ ├── LoggerInitializer.tsx
│ │ ├── LoggerService.ts
│ │ └── xmlui.ts
│ ├── logo.svg
│ ├── parsers
│ │ ├── common
│ │ │ ├── GenericToken.ts
│ │ │ ├── InputStream.ts
│ │ │ └── utils.ts
│ │ ├── scripting
│ │ │ ├── code-behind-collect.ts
│ │ │ ├── Lexer.ts
│ │ │ ├── modules.ts
│ │ │ ├── Parser.ts
│ │ │ ├── ParserError.ts
│ │ │ ├── ScriptingNodeTypes.ts
│ │ │ ├── TokenTrait.ts
│ │ │ ├── TokenType.ts
│ │ │ └── tree-visitor.ts
│ │ ├── style-parser
│ │ │ ├── errors.ts
│ │ │ ├── source-tree.ts
│ │ │ ├── StyleInputStream.ts
│ │ │ ├── StyleLexer.ts
│ │ │ ├── StyleParser.ts
│ │ │ └── tokens.ts
│ │ └── xmlui-parser
│ │ ├── CharacterCodes.ts
│ │ ├── diagnostics.ts
│ │ ├── fileExtensions.ts
│ │ ├── index.ts
│ │ ├── lint.ts
│ │ ├── parser.ts
│ │ ├── ParserError.ts
│ │ ├── scanner.ts
│ │ ├── syntax-kind.ts
│ │ ├── syntax-node.ts
│ │ ├── transform.ts
│ │ ├── utils.ts
│ │ ├── xmlui-serializer.ts
│ │ └── xmlui-tree.ts
│ ├── react-app-env.d.ts
│ ├── syntax
│ │ ├── monaco
│ │ │ ├── grammar.monacoLanguage.ts
│ │ │ ├── index.ts
│ │ │ ├── xmlui-dark.ts
│ │ │ ├── xmlui-light.ts
│ │ │ └── xmluiscript.monacoLanguage.ts
│ │ └── textMate
│ │ ├── index.ts
│ │ ├── xmlui-dark.json
│ │ ├── xmlui-light.json
│ │ ├── xmlui.json
│ │ └── xmlui.tmLanguage.json
│ ├── testing
│ │ ├── assertions.ts
│ │ ├── component-test-helpers.ts
│ │ ├── ComponentDrivers.ts
│ │ ├── drivers
│ │ │ ├── DateInputDriver.ts
│ │ │ ├── index.ts
│ │ │ ├── ModalDialogDriver.ts
│ │ │ ├── NumberBoxDriver.ts
│ │ │ ├── TextBoxDriver.ts
│ │ │ ├── TimeInputDriver.ts
│ │ │ ├── TimerDriver.ts
│ │ │ └── TreeDriver.ts
│ │ ├── fixtures.ts
│ │ ├── index.ts
│ │ └── infrastructure
│ │ ├── index.html
│ │ ├── main.tsx
│ │ ├── public
│ │ │ ├── mockServiceWorker.js
│ │ │ ├── resources
│ │ │ │ ├── bell.svg
│ │ │ │ ├── box.svg
│ │ │ │ ├── doc.svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── flower-640x480.jpg
│ │ │ │ ├── sun.svg
│ │ │ │ ├── test-image-100x100.jpg
│ │ │ │ └── txt.svg
│ │ │ └── serve.json
│ │ └── TestBed.tsx
│ └── vite-env.d.ts
├── tests
│ ├── components
│ │ ├── CodeBlock
│ │ │ └── hightlight-code.test.ts
│ │ ├── playground-pattern.test.ts
│ │ └── Tree
│ │ └── Tree-states.test.ts
│ ├── components-core
│ │ ├── abstractions
│ │ │ └── treeAbstractions.test.ts
│ │ ├── container
│ │ │ └── buildProxy.test.ts
│ │ ├── interception
│ │ │ ├── orderBy.test.ts
│ │ │ ├── ReadOnlyCollection.test.ts
│ │ │ └── request-param-converter.test.ts
│ │ ├── scripts-runner
│ │ │ ├── AttributeValueParser.test.ts
│ │ │ ├── eval-tree-arrow-async.test.ts
│ │ │ ├── eval-tree-arrow.test.ts
│ │ │ ├── eval-tree-func-decl-async.test.ts
│ │ │ ├── eval-tree-func-decl.test.ts
│ │ │ ├── eval-tree-pre-post.test.ts
│ │ │ ├── eval-tree-regression.test.ts
│ │ │ ├── eval-tree.test.ts
│ │ │ ├── function-proxy.test.ts
│ │ │ ├── parser-regression.test.ts
│ │ │ ├── process-event.test.ts
│ │ │ ├── process-function.test.ts
│ │ │ ├── process-implicit-context.test.ts
│ │ │ ├── process-statement-asgn.test.ts
│ │ │ ├── process-statement-destruct.test.ts
│ │ │ ├── process-statement-regs.test.ts
│ │ │ ├── process-statement-sync.test.ts
│ │ │ ├── process-statement.test.ts
│ │ │ ├── process-switch-sync.test.ts
│ │ │ ├── process-switch.test.ts
│ │ │ ├── process-try-sync.test.ts
│ │ │ ├── process-try.test.ts
│ │ │ └── test-helpers.ts
│ │ ├── test-metadata-handler.ts
│ │ ├── theming
│ │ │ ├── border-segments.test.ts
│ │ │ ├── component-layout.resolver.test.ts
│ │ │ ├── layout-property-parser.test.ts
│ │ │ ├── layout-resolver-disabled.test.ts
│ │ │ ├── layout-resolver.test.ts
│ │ │ ├── layout-resolver2.test.ts
│ │ │ ├── layout-vp-override.test.ts
│ │ │ └── padding-segments.test.ts
│ │ └── utils
│ │ ├── date-utils.test.ts
│ │ ├── format-human-elapsed-time.test.ts
│ │ └── LruCache.test.ts
│ ├── language-server
│ │ ├── completion.test.ts
│ │ ├── format.test.ts
│ │ ├── hover.test.ts
│ │ └── mockData.ts
│ └── parsers
│ ├── common
│ │ └── input-stream.test.ts
│ ├── markdown
│ │ └── parse-binding-expression.test.ts
│ ├── parameter-parser.test.ts
│ ├── paremeter-parser.test.ts
│ ├── scripting
│ │ ├── eval-tree-arrow.test.ts
│ │ ├── eval-tree-pre-post.test.ts
│ │ ├── eval-tree.test.ts
│ │ ├── function-proxy.test.ts
│ │ ├── lexer-literals.test.ts
│ │ ├── lexer-misc.test.ts
│ │ ├── module-parse.test.ts
│ │ ├── parser-arrow.test.ts
│ │ ├── parser-assignments.test.ts
│ │ ├── parser-binary.test.ts
│ │ ├── parser-destructuring.test.ts
│ │ ├── parser-errors.test.ts
│ │ ├── parser-expressions.test.ts
│ │ ├── parser-function.test.ts
│ │ ├── parser-literals.test.ts
│ │ ├── parser-primary.test.ts
│ │ ├── parser-regex.test.ts
│ │ ├── parser-statements.test.ts
│ │ ├── parser-unary.test.ts
│ │ ├── process-event.test.ts
│ │ ├── process-implicit-context.test.ts
│ │ ├── process-statement-asgn.test.ts
│ │ ├── process-statement-destruct.test.ts
│ │ ├── process-statement-regs.test.ts
│ │ ├── process-statement-sync.test.ts
│ │ ├── process-statement.test.ts
│ │ ├── process-switch-sync.test.ts
│ │ ├── process-switch.test.ts
│ │ ├── process-try-sync.test.ts
│ │ ├── process-try.test.ts
│ │ ├── simplify-expression.test.ts
│ │ ├── statement-hooks.test.ts
│ │ └── test-helpers.ts
│ ├── style-parser
│ │ ├── generateHvarChain.test.ts
│ │ ├── parseHVar.test.ts
│ │ ├── parser.test.ts
│ │ └── tokens.test.ts
│ └── xmlui
│ ├── lint.test.ts
│ ├── parser.test.ts
│ ├── scanner.test.ts
│ ├── transform.attr.test.ts
│ ├── transform.circular.test.ts
│ ├── transform.element.test.ts
│ ├── transform.errors.test.ts
│ ├── transform.escape.test.ts
│ ├── transform.regression.test.ts
│ ├── transform.script.test.ts
│ ├── transform.test.ts
│ └── xmlui.ts
├── tests-e2e
│ ├── api-bound-component-regression.spec.ts
│ ├── api-call-as-extracted-component.spec.ts
│ ├── assign-to-object-or-array-regression.spec.ts
│ ├── binding-regression.spec.ts
│ ├── children-as-template-context-vars.spec.ts
│ ├── compound-component.spec.ts
│ ├── context-vars-regression.spec.ts
│ ├── data-bindings.spec.ts
│ ├── datasource-and-api-usage-in-var.spec.ts
│ ├── datasource-direct-binding.spec.ts
│ ├── datasource-onLoaded-regression.spec.ts
│ ├── inline-styles-disabled.spec.ts
│ ├── modify-array-item-regression.spec.ts
│ ├── namespaces.spec.ts
│ ├── push-to-array-regression.spec.ts
│ ├── screen-breakpoints.spec.ts
│ ├── scripting.spec.ts
│ ├── state-scope-in-pages.spec.ts
│ └── state-var-scopes.spec.ts
├── tsconfig.json
├── tsdown.config.ts
├── vite.config.ts
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/xmlui/src/components/Form/Form.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import type { ApiInterceptorDefinition } from "../../components-core/interception/abstractions";
2 | import { labelPositionValues } from "../abstractions";
3 | import { expect, test } from "../../testing/fixtures";
4 |
5 | // Test data constants
6 | const errorDisplayInterceptor: ApiInterceptorDefinition = {
7 | initialize: `
8 | $state.items = {
9 | [10]: { name: "Smith", id: 10 }
10 | };
11 | $state.currentId = 10;
12 | `,
13 | operations: {
14 | "no-validation-error": {
15 | url: "/no-validation-error",
16 | method: "post",
17 | handler: `return true;`,
18 | },
19 | "general-validation-error": {
20 | url: "/general-validation-error",
21 | method: "post",
22 | handler: `
23 | throw Errors.HttpError(404,
24 | {
25 | message: "General error message from the backend",
26 | issues: [
27 | { message: "Error for the whole form", severity: "error" },
28 | { message: "Warning for the whole form", severity: "warning" },
29 | ]
30 | }
31 | );
32 | `,
33 | },
34 | "field-validation-error": {
35 | url: "/field-validation-error",
36 | method: "post",
37 | handler: `
38 | throw Errors.HttpError(404,
39 | {
40 | message: "Field error message from the backend",
41 | issues: [
42 | { field: "test", message: "Display warning", severity: "warning" },
43 | ]
44 | }
45 | );
46 | `,
47 | },
48 | },
49 | };
50 |
51 | // =============================================================================
52 | // BASIC FUNCTIONALITY TESTS
53 | // =============================================================================
54 |
55 | test.describe("Basic Functionality", () => {
56 | test("component renders with default props", async ({ initTestBed, createFormDriver }) => {
57 | await initTestBed(`<Form testId="form"/>`);
58 | const driver = await createFormDriver("form");
59 | await expect(driver.component).toBeVisible();
60 | });
61 |
62 | test("component renders with form items", async ({ initTestBed, page }) => {
63 | await initTestBed(`
64 | <Form>
65 | <FormItem label="Name" bindTo="name" />
66 | <FormItem label="Email" bindTo="email" />
67 | </Form>
68 | `);
69 |
70 | await expect(page.getByText("Name")).toBeVisible();
71 | await expect(page.getByText("Email")).toBeVisible();
72 | });
73 |
74 | test("component renders save and cancel buttons by default", async ({ initTestBed, page }) => {
75 | await initTestBed(`<Form/>`);
76 |
77 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
78 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
79 | });
80 |
81 | test("component renders custom button labels", async ({ initTestBed, page }) => {
82 | await initTestBed(`
83 | <Form cancelLabel="Go Back" saveLabel="Submit"/>
84 | `);
85 |
86 | await expect(page.getByRole("button", { name: "Go Back" })).toBeVisible();
87 | await expect(page.getByRole("button", { name: "Submit" })).toBeVisible();
88 | });
89 |
90 | test("component swaps cancel and save button positions", async ({ initTestBed, page }) => {
91 | await initTestBed(`
92 | <Form swapCancelAndSave="true"/>
93 | `);
94 |
95 | const buttons = page.getByRole("button");
96 | await expect(buttons.first()).toHaveText("Save");
97 | await expect(buttons.last()).toHaveText("Cancel");
98 | });
99 |
100 | // =============================================================================
101 | // HIDE BUTTON ROW TESTS
102 | // =============================================================================
103 |
104 | test.describe("hideButtonRow property", () => {
105 | test("hides button row when set to true", async ({ initTestBed, page }) => {
106 | await initTestBed(`<Form hideButtonRow="true"/>`);
107 |
108 | await expect(page.getByRole("button", { name: "Cancel" })).not.toBeVisible();
109 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
110 | });
111 |
112 | test("shows button row when set to false", async ({ initTestBed, page }) => {
113 | await initTestBed(`<Form hideButtonRow="false"/>`);
114 |
115 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
116 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
117 | });
118 |
119 | test("shows button row by default when property not set", async ({ initTestBed, page }) => {
120 | await initTestBed(`<Form/>`);
121 |
122 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
123 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
124 | });
125 |
126 | test("hides custom button row template when set to true", async ({ initTestBed, page }) => {
127 | await initTestBed(`
128 | <Form hideButtonRow="true">
129 | <property name="buttonRowTemplate">
130 | <Button label="Custom Save" type="submit" testId="customSave" />
131 | <Button label="Custom Cancel" type="button" testId="customCancel" />
132 | </property>
133 | </Form>
134 | `);
135 |
136 | await expect(page.getByTestId("customSave")).not.toBeVisible();
137 | await expect(page.getByTestId("customCancel")).not.toBeVisible();
138 | });
139 |
140 | test("overrides hideButtonRowUntilDirty when both are set", async ({
141 | initTestBed,
142 | page,
143 | createFormItemDriver,
144 | createTextBoxDriver,
145 | }) => {
146 | await initTestBed(`
147 | <Form hideButtonRow="true" hideButtonRowUntilDirty="true">
148 | <FormItem label="Name" bindTo="name" testId="nameField" />
149 | </Form>
150 | `);
151 |
152 | // Button row should be hidden even before making changes
153 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
154 | await expect(page.getByRole("button", { name: "Cancel" })).not.toBeVisible();
155 |
156 | // Make the form dirty
157 | const driver = await createFormItemDriver("nameField");
158 | const input = await createTextBoxDriver(driver.input);
159 | await input.field.fill("John");
160 |
161 | // Button row should still be hidden even after making changes
162 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
163 | await expect(page.getByRole("button", { name: "Cancel" })).not.toBeVisible();
164 | });
165 |
166 | test("handles null value gracefully", async ({ initTestBed, page }) => {
167 | await initTestBed(`<Form hideButtonRow="{null}"/>`);
168 |
169 | // Should show button row (default behavior)
170 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
171 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
172 | });
173 |
174 | test("handles undefined value gracefully", async ({ initTestBed, page }) => {
175 | await initTestBed(`<Form hideButtonRow="{undefined}"/>`);
176 |
177 | // Should show button row (default behavior)
178 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
179 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
180 | });
181 |
182 | test("handles string 'true' value", async ({ initTestBed, page }) => {
183 | await initTestBed(`<Form hideButtonRow="true"/>`);
184 |
185 | await expect(page.getByRole("button", { name: "Cancel" })).not.toBeVisible();
186 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
187 | });
188 |
189 | test("handles string 'false' value", async ({ initTestBed, page }) => {
190 | await initTestBed(`<Form hideButtonRow="false"/>`);
191 |
192 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
193 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
194 | });
195 |
196 | test("form submission still works with hidden button row via external submit", async ({
197 | initTestBed,
198 | page,
199 | createFormItemDriver,
200 | createTextBoxDriver,
201 | }) => {
202 | const { testStateDriver } = await initTestBed(`
203 | <Fragment>
204 | <Form id="testForm" hideButtonRow="true" onSubmit="arg => testState = arg">
205 | <FormItem label="Name" bindTo="name" testId="nameField" />
206 | <Button type="submit" label="External Submit" testId="externalSubmit" />
207 | </Form>
208 | </Fragment>
209 | `);
210 |
211 | const driver = await createFormItemDriver("nameField");
212 | const input = await createTextBoxDriver(driver.input);
213 | await input.field.fill("John Doe");
214 |
215 | await page.getByTestId("externalSubmit").click();
216 |
217 | const submittedData = await testStateDriver.testState();
218 | expect(submittedData).toEqual({ name: "John Doe" });
219 | });
220 | });
221 |
222 | // =============================================================================
223 | // HIDE BUTTON ROW UNTIL DIRTY TESTS
224 | // =============================================================================
225 |
226 | test.describe("hideButtonRowUntilDirty property", () => {
227 | test("hides button row initially when form is not dirty", async ({ initTestBed, page }) => {
228 | await initTestBed(`
229 | <Form hideButtonRowUntilDirty="true">
230 | <FormItem label="Name" bindTo="name" testId="nameField" />
231 | </Form>
232 | `);
233 |
234 | await expect(page.getByRole("button", { name: "Cancel" })).not.toBeVisible();
235 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
236 | });
237 |
238 | test("shows button row when form becomes dirty", async ({
239 | initTestBed,
240 | page,
241 | createFormItemDriver,
242 | createTextBoxDriver,
243 | }) => {
244 | await initTestBed(`
245 | <Form hideButtonRowUntilDirty="true">
246 | <FormItem label="Name" bindTo="name" testId="nameField" />
247 | </Form>
248 | `);
249 |
250 | // Initially hidden
251 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
252 |
253 | // Make form dirty
254 | const driver = await createFormItemDriver("nameField");
255 | const input = await createTextBoxDriver(driver.input);
256 | await input.field.fill("John");
257 |
258 | // Now visible
259 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
260 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
261 | });
262 |
263 | test("keeps button row visible after form becomes dirty", async ({
264 | initTestBed,
265 | page,
266 | createFormItemDriver,
267 | createTextBoxDriver,
268 | }) => {
269 | await initTestBed(`
270 | <Form hideButtonRowUntilDirty="true">
271 | <FormItem label="Name" bindTo="name" testId="nameField" />
272 | </Form>
273 | `);
274 |
275 | const driver = await createFormItemDriver("nameField");
276 | const input = await createTextBoxDriver(driver.input);
277 |
278 | // Make form dirty
279 | await input.field.fill("John");
280 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
281 |
282 | // Clear the input (form is still dirty)
283 | await input.field.clear();
284 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
285 | });
286 |
287 | test("shows button row by default when property set to false", async ({
288 | initTestBed,
289 | page,
290 | }) => {
291 | await initTestBed(`
292 | <Form hideButtonRowUntilDirty="false">
293 | <FormItem label="Name" bindTo="name" testId="nameField" />
294 | </Form>
295 | `);
296 |
297 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
298 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
299 | });
300 |
301 | test("shows button row by default when property not set", async ({ initTestBed, page }) => {
302 | await initTestBed(`
303 | <Form>
304 | <FormItem label="Name" bindTo="name" testId="nameField" />
305 | </Form>
306 | `);
307 |
308 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
309 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
310 | });
311 |
312 | test("works with multiple form items", async ({
313 | initTestBed,
314 | page,
315 | createFormItemDriver,
316 | createTextBoxDriver,
317 | }) => {
318 | await initTestBed(`
319 | <Form hideButtonRowUntilDirty="true">
320 | <FormItem label="Name" bindTo="name" testId="nameField" />
321 | <FormItem label="Email" bindTo="email" testId="emailField" />
322 | </Form>
323 | `);
324 |
325 | // Initially hidden
326 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
327 |
328 | // Modify second field
329 | const emailDriver = await createFormItemDriver("emailField");
330 | const emailInput = await createTextBoxDriver(emailDriver.input);
331 | await emailInput.field.fill("[email protected]");
332 |
333 | // Now visible
334 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
335 | });
336 |
337 | test("hides custom button row template until dirty", async ({
338 | initTestBed,
339 | page,
340 | createFormItemDriver,
341 | createTextBoxDriver,
342 | }) => {
343 | await initTestBed(`
344 | <Form hideButtonRowUntilDirty="true">
345 | <FormItem label="Name" bindTo="name" testId="nameField" />
346 | <property name="buttonRowTemplate">
347 | <Button label="Custom Save" type="submit" testId="customSave" />
348 | </property>
349 | </Form>
350 | `);
351 |
352 | // Initially hidden
353 | await expect(page.getByTestId("customSave")).not.toBeVisible();
354 |
355 | // Make form dirty
356 | const driver = await createFormItemDriver("nameField");
357 | const input = await createTextBoxDriver(driver.input);
358 | await input.field.fill("John");
359 |
360 | // Now visible
361 | await expect(page.getByTestId("customSave")).toBeVisible();
362 | });
363 |
364 | test("handles null value gracefully", async ({ initTestBed, page }) => {
365 | await initTestBed(`
366 | <Form hideButtonRowUntilDirty="{null}">
367 | <FormItem label="Name" bindTo="name" testId="nameField" />
368 | </Form>
369 | `);
370 |
371 | // Should show button row (default behavior)
372 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
373 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
374 | });
375 |
376 | test("handles undefined value gracefully", async ({ initTestBed, page }) => {
377 | await initTestBed(`
378 | <Form hideButtonRowUntilDirty="{undefined}">
379 | <FormItem label="Name" bindTo="name" testId="nameField" />
380 | </Form>
381 | `);
382 |
383 | // Should show button row (default behavior)
384 | await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
385 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
386 | });
387 |
388 | test("works with form initialized with data", async ({
389 | initTestBed,
390 | page,
391 | createFormItemDriver,
392 | createTextBoxDriver,
393 | }) => {
394 | await initTestBed(`
395 | <Form hideButtonRowUntilDirty="true" data="{{ name: 'Initial' }}">
396 | <FormItem label="Name" bindTo="name" testId="nameField" />
397 | </Form>
398 | `);
399 |
400 | // Initially hidden (form has data but is not dirty)
401 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
402 |
403 | // Make form dirty
404 | const driver = await createFormItemDriver("nameField");
405 | const input = await createTextBoxDriver(driver.input);
406 | await input.field.fill("Modified");
407 |
408 | // Now visible
409 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
410 | });
411 |
412 | test("button row appears when checkbox is checked", async ({ initTestBed, page }) => {
413 | await initTestBed(`
414 | <Form hideButtonRowUntilDirty="true">
415 | <FormItem label="Accept Terms" bindTo="terms" type="checkbox" />
416 | </Form>
417 | `);
418 |
419 | // Initially hidden
420 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
421 |
422 | // Check the checkbox
423 | const checkbox = page.getByRole("checkbox");
424 | await checkbox.check();
425 |
426 | // Now visible
427 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
428 | });
429 |
430 | test("button row appears when slider value changes", async ({ initTestBed, page }) => {
431 | await initTestBed(`
432 | <Form hideButtonRowUntilDirty="true">
433 | <FormItem label="Volume" bindTo="volume" type="slider" testId="volumeField" />
434 | </Form>
435 | `);
436 |
437 | // Initially hidden
438 | await expect(page.getByRole("button", { name: "Save" })).not.toBeVisible();
439 |
440 | // Move the slider using keyboard
441 | const slider = page.getByRole("slider");
442 | await slider.press("ArrowRight");
443 |
444 | // Now visible
445 | await expect(page.getByRole("button", { name: "Save" })).toBeVisible();
446 | });
447 | });
448 |
449 | // =============================================================================
450 | // ENABLE SUBMIT PROPERTY TESTS
451 | // =============================================================================
452 |
453 | test.describe("enableSubmit property", () => {
454 | test("disables submit button when set to false", async ({ initTestBed, page }) => {
455 | await initTestBed(`<Form enableSubmit="false"/>`);
456 |
457 | const saveButton = page.getByRole("button", { name: "Save" });
458 | await expect(saveButton).toBeVisible();
459 | await expect(saveButton).toBeDisabled();
460 | });
461 |
462 | test("enables submit button when set to true", async ({ initTestBed, page }) => {
463 | await initTestBed(`<Form enableSubmit="true"/>`);
464 |
465 | const saveButton = page.getByRole("button", { name: "Save" });
466 | await expect(saveButton).toBeVisible();
467 | await expect(saveButton).toBeEnabled();
468 | });
469 |
470 | test("submit button is enabled by default when property not set", async ({
471 | initTestBed,
472 | page,
473 | }) => {
474 | await initTestBed(`<Form/>`);
475 |
476 | const saveButton = page.getByRole("button", { name: "Save" });
477 | await expect(saveButton).toBeEnabled();
478 | });
479 |
480 | test("prevents form submission when set to false", async ({ initTestBed, page }) => {
481 | const { testStateDriver } = await initTestBed(`
482 | <Form enableSubmit="false" onSubmit="arg => testState = arg">
483 | <FormItem label="Name" bindTo="name" testId="nameField" />
484 | </Form>
485 | `);
486 |
487 | const saveButton = page.getByRole("button", { name: "Save" });
488 | await expect(saveButton).toBeDisabled();
489 |
490 | // Verify form does not submit (button is disabled, so click won't work)
491 | await saveButton.click({ force: true }); // Force click on disabled button
492 |
493 | // testState should remain null since submit was prevented
494 | await expect.poll(testStateDriver.testState).toBeNull();
495 | });
496 |
497 | test("allows form submission when set to true", async ({
498 | initTestBed,
499 | page,
500 | createFormItemDriver,
501 | createTextBoxDriver,
502 | }) => {
503 | const { testStateDriver } = await initTestBed(`
504 | <Form enableSubmit="true" onSubmit="arg => testState = arg">
505 | <FormItem label="Name" bindTo="name" testId="nameField" />
506 | </Form>
507 | `);
508 |
509 | const driver = await createFormItemDriver("nameField");
510 | const input = await createTextBoxDriver(driver.input);
511 | await input.field.fill("John Doe");
512 |
513 | const saveButton = page.getByRole("button", { name: "Save" });
514 | await expect(saveButton).toBeEnabled();
515 | await saveButton.click();
516 |
517 | const submittedData = await testStateDriver.testState();
518 | expect(submittedData).toEqual({ name: "John Doe" });
519 | });
520 |
521 | test("handles null value gracefully (defaults to enabled)", async ({ initTestBed, page }) => {
522 | await initTestBed(`<Form enableSubmit="{null}"/>`);
523 |
524 | const saveButton = page.getByRole("button", { name: "Save" });
525 | await expect(saveButton).toBeEnabled();
526 | });
527 |
528 | test("handles string 'true' value", async ({ initTestBed, page }) => {
529 | await initTestBed(`<Form enableSubmit="true"/>`);
530 |
531 | const saveButton = page.getByRole("button", { name: "Save" });
532 | await expect(saveButton).toBeEnabled();
533 | });
534 |
535 | test("handles string 'false' value", async ({ initTestBed, page }) => {
536 | await initTestBed(`<Form enableSubmit="false"/>`);
537 |
538 | const saveButton = page.getByRole("button", { name: "Save" });
539 | await expect(saveButton).toBeDisabled();
540 | });
541 |
542 | test("does not affect cancel button", async ({ initTestBed, page }) => {
543 | await initTestBed(`<Form enableSubmit="false"/>`);
544 |
545 | const cancelButton = page.getByRole("button", { name: "Cancel" });
546 | await expect(cancelButton).toBeEnabled();
547 | });
548 |
549 | test("works with custom submit button label", async ({ initTestBed, page }) => {
550 | await initTestBed(`<Form enableSubmit="false" saveLabel="Submit Now"/>`);
551 |
552 | const submitButton = page.getByRole("button", { name: "Submit Now" });
553 | await expect(submitButton).toBeDisabled();
554 | });
555 |
556 | test("works together with form disabled state", async ({ initTestBed, page }) => {
557 | await initTestBed(`<Form enabled="false" enableSubmit="true"/>`);
558 |
559 | const saveButton = page.getByRole("button", { name: "Save" });
560 | // Form disabled takes precedence
561 | await expect(saveButton).toBeDisabled();
562 | });
563 | });
564 |
565 | // =============================================================================
566 | // DATA PROPERTY TESTS
567 | // =============================================================================
568 |
569 | test.describe("data property", () => {
570 | test("sets initial form data", async ({
571 | initTestBed,
572 | createFormItemDriver,
573 | createTextBoxDriver,
574 | }) => {
575 | await initTestBed(`
576 | <Form data="{{ name: 'John', age: 30 }}">
577 | <FormItem label="Name" bindTo="name" testId="nameField" />
578 | <FormItem label="Age" bindTo="age" type="integer" testId="ageField" />
579 | </Form>
580 | `);
581 |
582 | const nameDriver = await createFormItemDriver("nameField");
583 | const nameInput = await createTextBoxDriver(nameDriver.input);
584 | const ageDriver = await createFormItemDriver("ageField");
585 | const ageInput = await createTextBoxDriver(ageDriver.input);
586 |
587 | await expect(nameInput.field).toHaveValue("John");
588 | await expect(ageInput.field).toHaveValue("30");
589 | });
590 |
591 | test("handles null data gracefully", async ({ initTestBed, createFormDriver }) => {
592 | await initTestBed(`<Form data="{null}" testId="form"/>`);
593 | const driver = await createFormDriver("form");
594 | await expect(driver.component).toBeVisible();
595 | });
596 |
597 | test("handles undefined data gracefully", async ({ initTestBed, createFormDriver }) => {
598 | await initTestBed(`<Form data="{undefined}" testId="form"/>`);
599 | const driver = await createFormDriver("form");
600 | await expect(driver.component).toBeVisible();
601 | });
602 |
603 | test("handles empty object data", async ({ initTestBed, createFormDriver }) => {
604 | await initTestBed(`<Form data="{{}}" testId="form"/>`);
605 | const driver = await createFormDriver("form");
606 | await expect(driver.component).toBeVisible();
607 | });
608 | });
609 |
610 | // =============================================================================
611 | // ITEM LABEL POSITION TESTS
612 | // =============================================================================
613 |
614 | test.describe("itemLabelPosition property", () => {
615 | labelPositionValues.forEach((position) => {
616 | test(`sets item label position to ${position}`, async ({
617 | initTestBed,
618 | createFormItemDriver,
619 | }) => {
620 | await initTestBed(`
621 | <Form itemLabelPosition="${position}">
622 | <FormItem label="Test Label" bindTo="test" testId="testField" />
623 | </Form>
624 | `);
625 |
626 | const driver = await createFormItemDriver("testField");
627 | await expect(driver.label).toBeVisible();
628 | });
629 | });
630 |
631 | test("handles invalid itemLabelPosition gracefully", async ({
632 | initTestBed,
633 | createFormDriver,
634 | }) => {
635 | await initTestBed(`<Form itemLabelPosition="invalid" testId="form"/>`);
636 | const driver = await createFormDriver("form");
637 | await expect(driver.component).toBeVisible();
638 | });
639 | });
640 |
641 | // =============================================================================
642 | // ITEM LABEL WIDTH TESTS
643 | // =============================================================================
644 |
645 | test.describe("itemLabelWidth property", () => {
646 | test("sets custom label width", async ({ initTestBed, createFormItemDriver }) => {
647 | await initTestBed(`
648 | <Form itemLabelWidth="200px">
649 | <FormItem label="Test Label" bindTo="test" testId="testField" />
650 | </Form>
651 | `);
652 |
653 | const driver = await createFormItemDriver("testField");
654 | await expect(driver.label).toBeVisible();
655 | });
656 |
657 | test("handles numeric label width", async ({ initTestBed, createFormItemDriver }) => {
658 | await initTestBed(`
659 | <Form itemLabelWidth="150">
660 | <FormItem label="Test Label" bindTo="test" testId="testField" />
661 | </Form>
662 | `);
663 |
664 | const driver = await createFormItemDriver("testField");
665 | await expect(driver.label).toBeVisible();
666 | });
667 |
668 | test("handles invalid label width gracefully", async ({ initTestBed, createFormDriver }) => {
669 | await initTestBed(`<Form itemLabelWidth="invalid" testId="form"/>`);
670 | const driver = await createFormDriver("form");
671 | await expect(driver.component).toBeVisible();
672 | });
673 |
674 | test("handles theme variable", async ({ initTestBed, createFormItemDriver }) => {
675 | const spaceBase = 0.25; //rem
676 | const labelSize = 10;
677 | const widthInPx = labelSize * spaceBase * 16; //px
678 | await initTestBed(`
679 | <Theme space-base="${spaceBase}rem">
680 | <Form itemLabelWidth="$space-${labelSize}">
681 | <FormItem label="Test Label" bindTo="test" testId="testField" />
682 | </Form>
683 | </Theme>
684 | `);
685 | const driver = await createFormItemDriver("testField");
686 | await expect(driver.label).toHaveCSS("width", widthInPx + "px");
687 | });
688 | });
689 |
690 | // =============================================================================
691 | // ITEM LABEL BREAK TESTS
692 | // =============================================================================
693 |
694 | test.describe("itemLabelBreak property", () => {
695 | test("enables label line breaking", async ({ initTestBed, createFormItemDriver }) => {
696 | await initTestBed(`
697 | <Form itemLabelBreak="true">
698 | <FormItem label="Very Long Label That Should Break" bindTo="test" testId="testField" />
699 | </Form>
700 | `);
701 |
702 | const driver = await createFormItemDriver("testField");
703 | await expect(driver.label).toBeVisible();
704 | });
705 |
706 | test("disables label line breaking", async ({ initTestBed, createFormItemDriver }) => {
707 | await initTestBed(`
708 | <Form itemLabelBreak="false">
709 | <FormItem label="Very Long Label That Should Not Break" bindTo="test" testId="testField" />
710 | </Form>
711 | `);
712 |
713 | const driver = await createFormItemDriver("testField");
714 | await expect(driver.label).toBeVisible();
715 | });
716 | });
717 |
718 | // =============================================================================
719 | // ENABLED PROPERTY TESTS
720 | // =============================================================================
721 |
722 | test.describe("enabled property", () => {
723 | test("disables save button when enabled is false", async ({ initTestBed, page }) => {
724 | await initTestBed(`
725 | <Form enabled="false">
726 | <FormItem label="Test" bindTo="test" />
727 | </Form>
728 | `);
729 |
730 | const saveButton = page.getByRole("button", { name: "Save" });
731 | await expect(saveButton).toBeDisabled();
732 | });
733 |
734 | test("enables form when enabled is true", async ({ initTestBed, page }) => {
735 | await initTestBed(`
736 | <Form enabled="true">
737 | <FormItem label="Test" bindTo="test" />
738 | </Form>
739 | `);
740 |
741 | const saveButton = page.getByRole("button", { name: "Save" });
742 | const cancelButton = page.getByRole("button", { name: "Cancel" });
743 |
744 | await expect(saveButton).toBeEnabled();
745 | await expect(cancelButton).toBeEnabled();
746 | });
747 | });
748 |
749 | // =============================================================================
750 | // BUTTON ROW TEMPLATE TESTS
751 | // =============================================================================
752 |
753 | test.describe("buttonRowTemplate property", () => {
754 | test("supports custom button row template", async ({ initTestBed, page }) => {
755 | await initTestBed(`
756 | <Form>
757 | <FormItem label="Test" bindTo="test" />
758 | <property name="buttonRowTemplate">
759 | <Button label="Custom Save" type="submit" />
760 | <Button label="Custom Cancel" type="button" />
761 | </property>
762 | </Form>
763 | `);
764 |
765 | await expect(page.getByRole("button", { name: "Custom Save" })).toBeVisible();
766 | await expect(page.getByRole("button", { name: "Custom Cancel" })).toBeVisible();
767 | });
768 | });
769 |
770 | // =============================================================================
771 | // EVENT TESTS
772 | // =============================================================================
773 |
774 | test.describe("Events", () => {
775 | test("onSubmit event fires with form data", async ({ initTestBed, page }) => {
776 | const { testStateDriver } = await initTestBed(`
777 | <Form data="{{ name: 'John', email: '[email protected]' }}" onSubmit="data => testState = data">
778 | <FormItem label="Name" bindTo="name" />
779 | <FormItem label="Email" bindTo="email" />
780 | </Form>
781 | `);
782 |
783 | await page.getByRole("button", { name: "Save" }).click();
784 |
785 | await expect.poll(testStateDriver.testState).toEqual({
786 | name: "John",
787 | email: "[email protected]",
788 | });
789 | });
790 |
791 | test("onCancel event fires when cancel button clicked", async ({ initTestBed, page }) => {
792 | const { testStateDriver } = await initTestBed(`
793 | <Form onCancel="testState = 'cancelled'">
794 | <FormItem label="Test" bindTo="test" />
795 | </Form>
796 | `);
797 |
798 | await page.getByRole("button", { name: "Cancel" }).click();
799 |
800 | await expect.poll(testStateDriver.testState).toEqual("cancelled");
801 | });
802 |
803 | test("onSuccess event fires on successful submission", async ({
804 | initTestBed,
805 | page,
806 | createFormDriver,
807 | }) => {
808 | const { testStateDriver } = await initTestBed(
809 | `
810 | <Form
811 | testId="form"
812 | submitUrl="/test-success"
813 | onSuccess="testState = 'success'; console.log('Submitted successfully')"
814 | data="{{ name: 'Test' }}">
815 | <FormItem label="Name" bindTo="name" />
816 | </Form>
817 | `,
818 | {
819 | apiInterceptor: {
820 | operations: {
821 | testSuccess: {
822 | url: "/test-success",
823 | method: "put",
824 | handler: `return { success: true };`,
825 | },
826 | },
827 | },
828 | },
829 | );
830 |
831 | const driver = await createFormDriver("form");
832 | await driver.submitForm();
833 | await expect.poll(testStateDriver.testState).toEqual("success");
834 | });
835 |
836 | test("onReset event fires when form is reset", async ({ initTestBed, page }) => {
837 | const { testStateDriver } = await initTestBed(`
838 | <Form
839 | id="testForm"
840 | onReset="testState = 'reset'"
841 | data="{{ name: 'Test' }}">
842 | <FormItem label="Name" bindTo="name" />
843 | <Button onClick="testForm.reset()" label="Reset Form" />
844 | </Form>
845 | `);
846 |
847 | await page.getByRole("button", { name: "Reset Form" }).click();
848 |
849 | await expect.poll(testStateDriver.testState).toEqual("reset");
850 | });
851 |
852 | test("onWillSubmit allows submission when returning null", async ({ initTestBed, page }) => {
853 | const { testStateDriver } = await initTestBed(`
854 | <Form
855 | data="{{ name: 'John' }}"
856 | onWillSubmit="data => null"
857 | onSubmit="data => testState = data">
858 | <FormItem label="Name" bindTo="name" />
859 | </Form>
860 | `);
861 |
862 | await page.getByRole("button", { name: "Save" }).click();
863 |
864 | await expect.poll(testStateDriver.testState).toEqual({ name: "John" });
865 | });
866 |
867 | test("onWillSubmit allows submission when returning undefined", async ({
868 | initTestBed,
869 | page,
870 | }) => {
871 | const { testStateDriver } = await initTestBed(`
872 | <Form
873 | data="{{ name: 'Jane' }}"
874 | onWillSubmit="data => undefined"
875 | onSubmit="data => testState = data">
876 | <FormItem label="Name" bindTo="name" />
877 | </Form>
878 | `);
879 |
880 | await page.getByRole("button", { name: "Save" }).click();
881 |
882 | await expect.poll(testStateDriver.testState).toEqual({ name: "Jane" });
883 | });
884 |
885 | test("onWillSubmit allows submission when returning empty string", async ({
886 | initTestBed,
887 | page,
888 | }) => {
889 | const { testStateDriver } = await initTestBed(`
890 | <Form
891 | data="{{ name: 'Bob' }}"
892 | onWillSubmit='data => ""'
893 | onSubmit="data => testState = data">
894 | <FormItem label="Name" bindTo="name" />
895 | </Form>
896 | `);
897 |
898 | await page.getByRole("button", { name: "Save" }).click();
899 |
900 | await expect.poll(testStateDriver.testState).toEqual({ name: "Bob" });
901 | });
902 |
903 | test("onWillSubmit allows submission when returning true", async ({ initTestBed, page }) => {
904 | const { testStateDriver } = await initTestBed(`
905 | <Form
906 | data="{{ name: 'Alice' }}"
907 | onWillSubmit="data => true"
908 | onSubmit="data => testState = data">
909 | <FormItem label="Name" bindTo="name" />
910 | </Form>
911 | `);
912 |
913 | await page.getByRole("button", { name: "Save" }).click();
914 |
915 | await expect.poll(testStateDriver.testState).toEqual({ name: "Alice" });
916 | });
917 |
918 | test("onWillSubmit allows submission when returning a number", async ({
919 | initTestBed,
920 | page,
921 | }) => {
922 | const { testStateDriver } = await initTestBed(`
923 | <Form
924 | data="{{ name: 'Charlie' }}"
925 | onWillSubmit="data => 42"
926 | onSubmit="data => testState = data">
927 | <FormItem label="Name" bindTo="name" />
928 | </Form>
929 | `);
930 |
931 | await page.getByRole("button", { name: "Save" }).click();
932 |
933 | await expect.poll(testStateDriver.testState).toEqual({ name: "Charlie" });
934 | });
935 |
936 | test("onWillSubmit cancels submission when returning false", async ({ initTestBed, page }) => {
937 | const { testStateDriver } = await initTestBed(`
938 | <Form
939 | data="{{ name: 'Test' }}"
940 | onWillSubmit="data => false"
941 | onSubmit="data => testState = 'submitted'">
942 | <FormItem label="Name" bindTo="name" />
943 | </Form>
944 | `);
945 |
946 | await page.getByRole("button", { name: "Save" }).click();
947 |
948 | // Wait a bit to ensure submission doesn't happen
949 | await page.waitForTimeout(200);
950 | await expect.poll(testStateDriver.testState).toEqual(null);
951 | });
952 |
953 | test("onWillSubmit submits modified data when returning a plain object", async ({
954 | initTestBed,
955 | page,
956 | }) => {
957 | const { testStateDriver } = await initTestBed(`
958 | <Form
959 | data="{{ name: 'Original', email: '[email protected]' }}"
960 | onWillSubmit="data => ({ name: 'Modified', email: '[email protected]', extra: 'added' })"
961 | onSubmit="data => testState = data">
962 | <FormItem label="Name" bindTo="name" />
963 | <FormItem label="Email" bindTo="email" />
964 | </Form>
965 | `);
966 |
967 | await page.getByRole("button", { name: "Save" }).click();
968 |
969 | await expect.poll(testStateDriver.testState).toEqual({
970 | name: "Modified",
971 | email: "[email protected]",
972 | extra: "added",
973 | });
974 | });
975 |
976 | test("onWillSubmit can add fields to submission data", async ({ initTestBed, page }) => {
977 | const { testStateDriver } = await initTestBed(`
978 | <Form
979 | data="{{ name: 'User' }}"
980 | onWillSubmit="data => ({ name: data.name, timestamp: 1234567890, source: 'web' })"
981 | onSubmit="data => testState = data">
982 | <FormItem label="Name" bindTo="name" />
983 | </Form>
984 | `);
985 |
986 | await page.getByRole("button", { name: "Save" }).click();
987 |
988 | await expect.poll(testStateDriver.testState).toEqual({
989 | name: "User",
990 | timestamp: 1234567890,
991 | source: "web",
992 | });
993 | });
994 |
995 | test("onWillSubmit can remove fields from submission data", async ({ initTestBed, page }) => {
996 | const { testStateDriver } = await initTestBed(`
997 | <Form
998 | data="{{ name: 'User', password: 'secret', email: '[email protected]' }}"
999 | onWillSubmit="data => ({ name: data.name, email: data.email })"
1000 | onSubmit="data => testState = data">
1001 | <FormItem label="Name" bindTo="name" />
1002 | <FormItem label="Password" bindTo="password" />
1003 | <FormItem label="Email" bindTo="email" />
1004 | </Form>
1005 | `);
1006 |
1007 | await page.getByRole("button", { name: "Save" }).click();
1008 |
1009 | await expect.poll(testStateDriver.testState).toEqual({
1010 | name: "User",
1011 | email: "[email protected]",
1012 | });
1013 | });
1014 |
1015 | test("onWillSubmit does not submit array when returned", async ({ initTestBed, page }) => {
1016 | const { testStateDriver } = await initTestBed(`
1017 | <Form
1018 | data="{{ name: 'Test' }}"
1019 | onWillSubmit="data => ['array', 'value']"
1020 | onSubmit="data => testState = data">
1021 | <FormItem label="Name" bindTo="name" />
1022 | </Form>
1023 | `);
1024 |
1025 | await page.getByRole("button", { name: "Save" }).click();
1026 |
1027 | // Array is treated as non-object, so original data should be submitted
1028 | await expect.poll(testStateDriver.testState).toEqual({ name: "Test" });
1029 | });
1030 |
1031 | test("onWillSubmit with complex object transformation", async ({ initTestBed, page }) => {
1032 | const { testStateDriver } = await initTestBed(`
1033 | <Form
1034 | data="{{ firstName: 'John', lastName: 'Doe', age: 30 }}"
1035 | onWillSubmit="data => ({ fullName: data.firstName + ' ' + data.lastName, age: data.age })"
1036 | onSubmit="data => testState = data">
1037 | <FormItem label="First Name" bindTo="firstName" />
1038 | <FormItem label="Last Name" bindTo="lastName" />
1039 | <FormItem label="Age" bindTo="age" type="integer" />
1040 | </Form>
1041 | `);
1042 |
1043 | await page.getByRole("button", { name: "Save" }).click();
1044 |
1045 | await expect.poll(testStateDriver.testState).toEqual({
1046 | fullName: "John Doe",
1047 | age: 30,
1048 | });
1049 | });
1050 | });
1051 |
1052 | // =============================================================================
1053 | // API TESTS
1054 | // =============================================================================
1055 |
1056 | test.describe("APIs", () => {
1057 | test("update method updates form data", async ({
1058 | initTestBed,
1059 | page,
1060 | createFormItemDriver,
1061 | createTextBoxDriver,
1062 | }) => {
1063 | await initTestBed(`
1064 | <Form
1065 | id="testForm"
1066 | data="{{ name: 'Original', age: 25 }}">
1067 | <FormItem label="Name" bindTo="name" testId="nameField" />
1068 | <FormItem label="Age" bindTo="age" type="integer" testId="ageField" />
1069 | <Button onClick="testForm.update({ name: 'Updated', age: 30 })" label="Update" />
1070 | </Form>
1071 | `);
1072 |
1073 | const nameDriver = await createFormItemDriver("nameField");
1074 | const nameInput = await createTextBoxDriver(nameDriver.input);
1075 | const ageDriver = await createFormItemDriver("ageField");
1076 | const ageInput = await createTextBoxDriver(ageDriver.input);
1077 |
1078 | await expect(nameInput.field).toHaveValue("Original");
1079 | await expect(ageInput.field).toHaveValue("25");
1080 |
1081 | await page.getByRole("button", { name: "Update" }).click();
1082 |
1083 | await expect(nameInput.field).toHaveValue("Updated");
1084 | await expect(ageInput.field).toHaveValue("30");
1085 | });
1086 |
1087 | test("reset method resets form to initial state", async ({
1088 | initTestBed,
1089 | page,
1090 | createFormItemDriver,
1091 | createTextBoxDriver,
1092 | }) => {
1093 | await initTestBed(`
1094 | <Form
1095 | id="testForm"
1096 | data="{{ name: 'Initial' }}">
1097 | <FormItem label="Name" bindTo="name" testId="nameField" />
1098 | <Button onClick="testForm.reset()" label="Reset" />
1099 | </Form>
1100 | `);
1101 |
1102 | const nameDriver = await createFormItemDriver("nameField");
1103 | const nameInput = await createTextBoxDriver(nameDriver.input);
1104 |
1105 | // Change the input value
1106 | await nameInput.field.fill("Changed");
1107 | await expect(nameInput.field).toHaveValue("Changed");
1108 |
1109 | // Reset the form
1110 | await page.getByRole("button", { name: "Reset" }).click();
1111 |
1112 | await expect(nameInput.field).toHaveValue("Initial");
1113 | });
1114 |
1115 | test("validate method returns validation results without submitting", async ({
1116 | initTestBed,
1117 | page,
1118 | createFormItemDriver,
1119 | createTextBoxDriver,
1120 | }) => {
1121 | const { testStateDriver } = await initTestBed(`
1122 | <Form id="testForm">
1123 | <FormItem label="Name" bindTo="name" required="true" testId="nameField" />
1124 | <FormItem label="Email" bindTo="email" testId="emailField" />
1125 | <Button onClick="testState = testForm.validate()" label="Validate" testId="validateBtn" />
1126 | </Form>
1127 | `);
1128 |
1129 | // Click validate button without filling required field
1130 | await page.getByTestId("validateBtn").click();
1131 |
1132 | // Wait for validation to complete
1133 | await page.waitForTimeout(100);
1134 |
1135 | const result = await testStateDriver.testState();
1136 | expect(result).toBeTruthy();
1137 | expect(result.isValid).toBe(false);
1138 | expect(result.errors).toBeDefined();
1139 | expect(result.errors.length).toBeGreaterThan(0);
1140 | });
1141 |
1142 | test("validate method returns isValid true when all validations pass", async ({
1143 | initTestBed,
1144 | page,
1145 | createFormItemDriver,
1146 | createTextBoxDriver,
1147 | }) => {
1148 | const { testStateDriver } = await initTestBed(`
1149 | <Form id="testForm">
1150 | <FormItem label="Name" bindTo="name" required="true" testId="nameField" />
1151 | <Button onClick="testState = testForm.validate()" label="Validate" testId="validateBtn" />
1152 | </Form>
1153 | `);
1154 |
1155 | // Fill the required field
1156 | const nameDriver = await createFormItemDriver("nameField");
1157 | const nameInput = await createTextBoxDriver(nameDriver.input);
1158 | await nameInput.field.fill("John Doe");
1159 |
1160 | // Click validate button
1161 | await page.getByTestId("validateBtn").click();
1162 |
1163 | // Wait for validation to complete
1164 | await page.waitForTimeout(100);
1165 |
1166 | const result = await testStateDriver.testState();
1167 | expect(result).toBeTruthy();
1168 | expect(result.isValid).toBe(true);
1169 | expect(result.errors.length).toBe(0);
1170 | });
1171 |
1172 | test("validate method returns cleaned form data", async ({
1173 | initTestBed,
1174 | page,
1175 | createFormItemDriver,
1176 | createTextBoxDriver,
1177 | }) => {
1178 | const { testStateDriver } = await initTestBed(`
1179 | <Form id="testForm">
1180 | <FormItem label="Name" bindTo="name" testId="nameField" />
1181 | <FormItem label="Age" bindTo="age" type="integer" testId="ageField" />
1182 | <Button onClick="testState = testForm.validate()" label="Validate" testId="validateBtn" />
1183 | </Form>
1184 | `);
1185 |
1186 | // Fill form fields
1187 | const nameDriver = await createFormItemDriver("nameField");
1188 | const nameInput = await createTextBoxDriver(nameDriver.input);
1189 | await nameInput.field.fill("John Doe");
1190 |
1191 | const ageDriver = await createFormItemDriver("ageField");
1192 | const ageInput = await createTextBoxDriver(ageDriver.input);
1193 | await ageInput.field.fill("30");
1194 |
1195 | // Click validate button
1196 | await page.getByTestId("validateBtn").click();
1197 |
1198 | // Wait for validation to complete
1199 | await page.waitForTimeout(100);
1200 |
1201 | const result = await testStateDriver.testState();
1202 | expect(result).toBeTruthy();
1203 | expect(result.data).toEqual({ name: "John Doe", age: 30 });
1204 | });
1205 |
1206 | test("validate method displays validation errors on form", async ({
1207 | initTestBed,
1208 | page,
1209 | createFormItemDriver,
1210 | }) => {
1211 | await initTestBed(`
1212 | <Form id="testForm">
1213 | <FormItem label="Name" bindTo="name" required="true" testId="nameField" />
1214 | <Button onClick="testForm.validate()" label="Validate" testId="validateBtn" />
1215 | </Form>
1216 | `);
1217 |
1218 | // Click validate without filling required field
1219 | await page.getByTestId("validateBtn").click();
1220 |
1221 | // Validation error should be displayed
1222 | const nameField = page.getByTestId("nameField");
1223 | await expect(nameField).toContainText("This field is required");
1224 | });
1225 |
1226 | test("validate method does not trigger form submission", async ({
1227 | initTestBed,
1228 | page,
1229 | createFormItemDriver,
1230 | createTextBoxDriver,
1231 | }) => {
1232 | const { testStateDriver } = await initTestBed(`
1233 | <Form id="testForm" onSubmit="testState = 'submitted'">
1234 | <FormItem label="Name" bindTo="name" testId="nameField" />
1235 | <Button onClick="testForm.validate()" label="Validate" testId="validateBtn" />
1236 | </Form>
1237 | `);
1238 |
1239 | // Fill form
1240 | const nameDriver = await createFormItemDriver("nameField");
1241 | const nameInput = await createTextBoxDriver(nameDriver.input);
1242 | await nameInput.field.fill("John");
1243 |
1244 | // Click validate button
1245 | await page.getByTestId("validateBtn").click();
1246 |
1247 | // Wait a bit
1248 | await page.waitForTimeout(200);
1249 |
1250 | // testState should remain null (not 'submitted')
1251 | await expect.poll(testStateDriver.testState).toBeNull();
1252 | });
1253 |
1254 | test("validate method returns complete validation results object", async ({
1255 | initTestBed,
1256 | page,
1257 | createFormItemDriver,
1258 | createTextBoxDriver,
1259 | }) => {
1260 | const { testStateDriver } = await initTestBed(`
1261 | <Form id="testForm">
1262 | <FormItem label="Name" bindTo="name" required="true" testId="nameField" />
1263 | <FormItem label="Email" bindTo="email" testId="emailField" />
1264 | <Button onClick="testState = testForm.validate()" label="Validate" testId="validateBtn" />
1265 | </Form>
1266 | `);
1267 |
1268 | // Fill only email (name is required)
1269 | const emailDriver = await createFormItemDriver("emailField");
1270 | const emailInput = await createTextBoxDriver(emailDriver.input);
1271 | await emailInput.field.fill("[email protected]");
1272 |
1273 | // Click validate button
1274 | await page.getByTestId("validateBtn").click();
1275 |
1276 | // Wait for validation to complete
1277 | await page.waitForTimeout(100);
1278 |
1279 | const result = await testStateDriver.testState();
1280 | expect(result).toBeTruthy();
1281 | expect(result.isValid).toBeDefined();
1282 | expect(result.data).toBeDefined();
1283 | expect(result.errors).toBeDefined();
1284 | expect(result.warnings).toBeDefined();
1285 | expect(result.validationResults).toBeDefined();
1286 | });
1287 | });
1288 |
1289 | // =============================================================================
1290 | // CONTEXT VARIABLE TESTS
1291 | // =============================================================================
1292 |
1293 | test.describe("Context Variables", () => {
1294 | test("$data context variable provides access to form data", async ({
1295 | initTestBed,
1296 | page,
1297 | createFormItemDriver,
1298 | }) => {
1299 | // This test needs specific FormItem behavior that may vary
1300 | await initTestBed(`
1301 | <Form data="{{ isEnabled: true, name: 'Joe' }}">
1302 | <FormItem testId="isEnabled" label="Enable name" bindTo="isEnabled" type="checkbox" />
1303 | <FormItem testId="name" enabled="{$data.isEnabled}" label="Name" bindTo="name" />
1304 | </Form>
1305 | `);
1306 |
1307 | const enableSwitch = (await createFormItemDriver("isEnabled")).checkbox;
1308 | const nameInput = (await createFormItemDriver("name")).textBox;
1309 |
1310 | await expect(enableSwitch).toBeVisible();
1311 | await expect(nameInput).toBeEnabled();
1312 | await enableSwitch.click();
1313 | await expect(nameInput).toBeDisabled();
1314 | });
1315 |
1316 | test("$data.update method updates form data", async ({
1317 | initTestBed,
1318 | page,
1319 | createFormItemDriver,
1320 | }) => {
1321 | await initTestBed(`
1322 | <Form data="{{ counter: 0 }}">
1323 | <FormItem testId="counter" label="Counter" bindTo="counter" type="integer" />
1324 | <Button onClick="$data.update({ counter: $data.counter + 1 })" label="Increment" />
1325 | </Form>
1326 | `);
1327 |
1328 | const counterDriver = await createFormItemDriver("counter");
1329 | const counterInput = counterDriver.textBox;
1330 | await expect(counterInput).toHaveValue("0");
1331 |
1332 | await page.getByRole("button", { name: "Increment" }).click({ force: true });
1333 |
1334 | await expect(counterInput).toHaveValue("1");
1335 | });
1336 | });
1337 |
1338 | // =============================================================================
1339 | // SUBMIT URL AND METHOD TESTS
1340 | // =============================================================================
1341 |
1342 | test.describe("Submit URL and Method", () => {
1343 | test("submits to custom URL with POST method (new date)", async ({
1344 | initTestBed,
1345 | createFormDriver,
1346 | }) => {
1347 | await initTestBed(
1348 | `<App><Form testId="form" submitUrl="/custom-endpoint" data="{null}">
1349 | <FormItem label="Name" bindTo="name" />
1350 | </Form></App>`,
1351 | {
1352 | apiInterceptor: {
1353 | operations: {
1354 | customEndpoint: {
1355 | url: "/custom-endpoint",
1356 | method: "post",
1357 | handler: `return { success: true };`,
1358 | },
1359 | },
1360 | },
1361 | },
1362 | );
1363 |
1364 | const driver = await createFormDriver("form");
1365 | await driver.submitForm();
1366 |
1367 | const response = await driver.getSubmitResponse("/custom-endpoint");
1368 | expect(response.ok()).toEqual(true);
1369 | });
1370 |
1371 | test("uses PUT method for existing data", async ({ initTestBed, createFormDriver }) => {
1372 | await initTestBed(
1373 | `<Form submitUrl="/entities/1" data="{{ id: 1, name: 'Existing' }}">
1374 | <FormItem label="Name" bindTo="name" />
1375 | </Form>`,
1376 | {
1377 | apiInterceptor: {
1378 | operations: {
1379 | updateEntity: {
1380 | url: "/entities/1",
1381 | method: "put",
1382 | handler: `return { success: true };`,
1383 | },
1384 | },
1385 | },
1386 | },
1387 | );
1388 |
1389 | const driver = await createFormDriver();
1390 | await driver.submitForm();
1391 |
1392 | const response = await driver.getSubmitResponse("/entities/1");
1393 | expect(response.ok()).toEqual(true);
1394 | });
1395 |
1396 | test("uses custom submit method", async ({ initTestBed, createFormDriver }) => {
1397 | await initTestBed(
1398 | `<Form submitUrl="/custom" submitMethod="put" data="{{ name: 'Test' }}">
1399 | <FormItem label="Name" bindTo="name" />
1400 | </Form>`,
1401 | {
1402 | apiInterceptor: {
1403 | operations: {
1404 | putCustom: {
1405 | url: "/custom",
1406 | method: "put",
1407 | handler: `return { success: true };`,
1408 | },
1409 | },
1410 | },
1411 | },
1412 | );
1413 |
1414 | const driver = await createFormDriver();
1415 | await driver.submitForm();
1416 |
1417 | const response = await driver.getSubmitResponse("/custom");
1418 | expect(response.ok()).toEqual(true);
1419 | });
1420 | });
1421 | });
1422 |
1423 | // =============================================================================
1424 | // ACCESSIBILITY TESTS
1425 | // =============================================================================
1426 |
1427 | test.describe("Accessibility", () => {
1428 | test("form has correct semantic role", async ({ initTestBed, page }) => {
1429 | await initTestBed(`<Form/>`);
1430 | await expect(page.locator("form")).toBeVisible();
1431 | });
1432 |
1433 | test("form items are properly associated with labels", async ({
1434 | initTestBed,
1435 | createFormItemDriver,
1436 | }) => {
1437 | await initTestBed(`
1438 | <Form>
1439 | <FormItem label="Full Name" bindTo="name" testId="nameField" />
1440 | </Form>
1441 | `);
1442 |
1443 | const driver = await createFormItemDriver("nameField");
1444 | await expect(driver.label).toBeVisible();
1445 | await expect(driver.label).toHaveText("Full Name");
1446 | });
1447 |
1448 | test("form submission is keyboard accessible", async ({ initTestBed, page }) => {
1449 | const { testStateDriver } = await initTestBed(`
1450 | <Form onSubmit="testState = 'submitted via keyboard'">
1451 | <FormItem label="Name" bindTo="name" />
1452 | </Form>
1453 | `);
1454 |
1455 | const submitButton = page.getByRole("button", { name: "Save" });
1456 | await submitButton.focus();
1457 | await page.keyboard.press("Enter");
1458 |
1459 | await expect.poll(testStateDriver.testState).toEqual("submitted via keyboard");
1460 | });
1461 |
1462 | test("form cancel is keyboard accessible", async ({ initTestBed, page }) => {
1463 | const { testStateDriver } = await initTestBed(`
1464 | <Form onCancel="testState = 'cancelled via keyboard'">
1465 | <FormItem label="Name" bindTo="name" />
1466 | </Form>
1467 | `);
1468 |
1469 | const cancelButton = page.getByRole("button", { name: "Cancel" });
1470 | await cancelButton.focus();
1471 | await page.keyboard.press("Enter");
1472 |
1473 | await expect.poll(testStateDriver.testState).toEqual("cancelled via keyboard");
1474 | });
1475 |
1476 | test("disabled form buttons are properly disabled", async ({ initTestBed, page }) => {
1477 | await initTestBed(`
1478 | <Form enabled="false">
1479 | <FormItem label="Name" bindTo="name" />
1480 | </Form>
1481 | `);
1482 |
1483 | const saveButton = page.getByRole("button", { name: "Save" });
1484 | await expect(saveButton).toBeDisabled();
1485 | });
1486 | });
1487 |
1488 | // =============================================================================
1489 | // THEME VARIABLE TESTS
1490 | // =============================================================================
1491 |
1492 | test.describe("Theme Variables", () => {
1493 | test("applies custom gap theme variable", async ({ initTestBed, createFormDriver }) => {
1494 | await initTestBed(`<Form testId="form"/>`, {
1495 | testThemeVars: {
1496 | "gap-Form": "2rem",
1497 | },
1498 | });
1499 |
1500 | const driver = await createFormDriver("form");
1501 | await expect(driver.component).toHaveCSS("gap", "32px");
1502 | });
1503 |
1504 | test("applies custom button row gap theme variable", async ({
1505 | initTestBed,
1506 | createFormDriver,
1507 | }) => {
1508 | await initTestBed(`<Form testId="form"/>`, {
1509 | testThemeVars: {
1510 | "gap-buttonRow-Form": "1rem",
1511 | },
1512 | });
1513 |
1514 | const driver = await createFormDriver("form");
1515 | await expect(driver.component).toBeVisible();
1516 | });
1517 |
1518 | test("applies validation display theme variables", async ({ initTestBed, page }) => {
1519 | // This test requires validation system to trigger error display
1520 | await initTestBed(
1521 | `
1522 | <Form>
1523 | <FormItem testId="email" label="Email" bindTo="email" type="email" required="true" />
1524 | </Form>
1525 | `,
1526 | {
1527 | testThemeVars: {
1528 | "backgroundColor-ValidationDisplay-error": "rgb(255, 0, 0)",
1529 | "textColor-ValidationDisplay-error": "rgb(255, 255, 255)",
1530 | },
1531 | },
1532 | );
1533 |
1534 | // Trigger validation by submitting with empty required field
1535 | await page.getByRole("button", { name: "Save" }).click();
1536 |
1537 | const emailComp = page.getByTestId("email");
1538 | await expect(emailComp).toContainText("This field is required");
1539 | });
1540 | });
1541 |
1542 | // =============================================================================
1543 | // EDGE CASES TESTS
1544 | // =============================================================================
1545 |
1546 | test.describe("Edge Cases", () => {
1547 | test("handles form without any form items", async ({ initTestBed, createFormDriver }) => {
1548 | await initTestBed(`<Form testId="form"/>`);
1549 | const driver = await createFormDriver("form");
1550 | await expect(driver.component).toBeVisible();
1551 | });
1552 |
1553 | test("handles malformed data input gracefully", async ({ initTestBed, createFormDriver }) => {
1554 | await initTestBed(`<Form data="{invalidJson}" testId="form"/>`);
1555 | const driver = await createFormDriver("form");
1556 | await expect(driver.component).toBeVisible();
1557 | });
1558 |
1559 | test("Form does not render if data receives malformed input", async ({
1560 | initTestBed,
1561 | createFormDriver,
1562 | }) => {
1563 | await initTestBed(`<Form data="{}" />`);
1564 | await expect((await createFormDriver()).component).not.toBeAttached();
1565 | });
1566 |
1567 | test("handles deeply nested data structure", async ({
1568 | initTestBed,
1569 | createFormItemDriver,
1570 | createTextBoxDriver,
1571 | }) => {
1572 | await initTestBed(`
1573 | <Form data="{{ user: { profile: { name: 'John' } } }}">
1574 | <FormItem label="Name" bindTo="user.profile.name" testId="nameField" />
1575 | </Form>
1576 | `);
1577 |
1578 | const driver = await createFormItemDriver("nameField");
1579 | const input = await createTextBoxDriver(driver.input);
1580 | await expect(input.field).toHaveValue("John");
1581 | });
1582 |
1583 | test("handles form with validation errors", async ({ initTestBed, page }) => {
1584 | await initTestBed(`
1585 | <Form>
1586 | <FormItem label="Email" bindTo="email" type="email" required="true" />
1587 | </Form>
1588 | `);
1589 |
1590 | // Try to submit form without filling required field
1591 | await page.getByRole("button", { name: "Save" }).click();
1592 |
1593 | // Validation should prevent submission and show error
1594 | const form = page.locator("form");
1595 | await expect(form).toBeVisible();
1596 | });
1597 |
1598 | test("handles rapid form submissions", async ({ initTestBed, page }) => {
1599 | const { testStateDriver } = await initTestBed(`
1600 | <Form onSubmit="testState = (testState || 0) + 1">
1601 | <FormItem label="Name" bindTo="name" />
1602 | </Form>
1603 | `);
1604 |
1605 | const submitButton = page.getByRole("button", { name: "Save" });
1606 |
1607 | // Click submit button multiple times rapidly
1608 | await submitButton.click();
1609 | await submitButton.click();
1610 | await submitButton.click();
1611 |
1612 | // Should only submit once or handle gracefully
1613 | await expect.poll(testStateDriver.testState).toBeGreaterThanOrEqual(1);
1614 | });
1615 |
1616 | test("handles null and undefined in nested data", async ({
1617 | initTestBed,
1618 | createFormItemDriver,
1619 | createTextBoxDriver,
1620 | }) => {
1621 | await initTestBed(`
1622 | <Form data="{{ user: null, settings: undefined, name: 'Test' }}">
1623 | <FormItem label="Name" bindTo="name" testId="nameField" />
1624 | </Form>
1625 | `);
1626 |
1627 | const driver = await createFormItemDriver("nameField");
1628 | const input = await createTextBoxDriver(driver.input);
1629 | await expect(input.field).toHaveValue("Test");
1630 | });
1631 |
1632 | test("handles form with empty string properties", async ({ initTestBed, page }) => {
1633 | await initTestBed(`
1634 | <Form
1635 | cancelLabel=""
1636 | saveLabel=""
1637 | data="{{ name: '' }}">
1638 | <FormItem label="Name" bindTo="name" />
1639 | </Form>
1640 | `);
1641 |
1642 | // Form should still be visible
1643 | const form = page.locator("form");
1644 | await expect(form).toBeVisible();
1645 | });
1646 |
1647 | test("handles special characters in form data", async ({
1648 | initTestBed,
1649 | createFormItemDriver,
1650 | createTextBoxDriver,
1651 | }) => {
1652 | await initTestBed(`
1653 | <Form data="{{ name: 'José María', description: 'Test & symbols' }}">
1654 | <FormItem label="Name" bindTo="name" testId="nameField" />
1655 | <FormItem label="Description" bindTo="description" testId="descField" />
1656 | </Form>
1657 | `);
1658 |
1659 | const nameDriver = await createFormItemDriver("nameField");
1660 | const nameInput = await createTextBoxDriver(nameDriver.input);
1661 | const descDriver = await createFormItemDriver("descField");
1662 | const descInput = await createTextBoxDriver(descDriver.input);
1663 |
1664 | await expect(nameInput.field).toHaveValue("José María");
1665 | await expect(descInput.field).toHaveValue("Test & symbols");
1666 | });
1667 |
1668 | test("user cannot submit with clientside errors present", async ({
1669 | initTestBed,
1670 | createFormDriver,
1671 | }) => {
1672 | const { testStateDriver } = await initTestBed(`
1673 | <Form onSubmit="testState = true">
1674 | <FormItem bindTo="name" required="true" />
1675 | </Form>
1676 | `);
1677 | const driver = await createFormDriver();
1678 |
1679 | // The onSubmit event should have been triggered if not for the client error of an empty required field
1680 | await driver.submitForm("click");
1681 | await expect.poll(testStateDriver.testState).toEqual(null);
1682 | });
1683 |
1684 | test("can submit with invisible required field", async ({
1685 | initTestBed,
1686 | createFormDriver,
1687 | createFormItemDriver,
1688 | createTextBoxDriver,
1689 | page,
1690 | }) => {
1691 | const { testStateDriver } = await initTestBed(`
1692 | <Form onSubmit="testState = true">
1693 | <FormItem testId="select" bindTo="authenticationType"
1694 | type="select" label="Authentication Type:" initialValue="{0}">
1695 | <Option value="{0}" label="Password" />
1696 | <Option value="{1}" label="Public Key" />
1697 | </FormItem>
1698 | <FormItem label="name1" testId="name1" bindTo="name1"
1699 | required="true" when="{$data.authenticationType == 0}"/>
1700 | <FormItem label="name2" testId="name2" bindTo="name2"
1701 | required="true" when="{$data.authenticationType == 1}"/>
1702 | </Form>
1703 | `);
1704 | const formDriver = await createFormDriver();
1705 | const selectDriver = await createFormItemDriver("select");
1706 | const textfieldElement = (await createFormItemDriver("name2")).input;
1707 | const textfieldDriver = await createTextBoxDriver(textfieldElement);
1708 |
1709 | await selectDriver.component.click();
1710 | await page.getByText("Public Key").click();
1711 | await textfieldDriver.field.fill("John");
1712 | await formDriver.submitForm();
1713 |
1714 | await expect.poll(testStateDriver.testState).toEqual(true);
1715 | });
1716 |
1717 | test("conditional fields keep the state", async ({
1718 | initTestBed,
1719 | createFormItemDriver,
1720 | createOptionDriver,
1721 | createTextBoxDriver,
1722 | }) => {
1723 | await initTestBed(`
1724 | <Form>
1725 | <FormItem testId="select" bindTo="authenticationType"
1726 | type="radioGroup" label="Authentication Type:" initialValue="{0}">
1727 | <Option value="{0}" label="Password" testId="password"/>
1728 | <Option value="{1}" label="Public Key" testId="publicKey" />
1729 | </FormItem>
1730 | <FormItem label="name1" testId="name1" bindTo="name1"
1731 | required="true" when="{$data.authenticationType == 0}"/>
1732 | <FormItem label="name2" testId="name2" bindTo="name2"
1733 | required="true" when="{$data.authenticationType == 1}"/>
1734 | </Form>
1735 | `);
1736 | const option1Driver = await createFormItemDriver("password");
1737 | const option2Driver = await createOptionDriver("publicKey");
1738 | const textfield1Element = (await createFormItemDriver("name1")).input;
1739 | const textfield1Driver = await createTextBoxDriver(textfield1Element);
1740 |
1741 | // Fill in first field
1742 | await textfield1Driver.field.fill("Test Value");
1743 | await expect(textfield1Driver.field).toHaveValue("Test Value");
1744 |
1745 | // Switch to second option
1746 | await option2Driver.component.click();
1747 |
1748 | // Switch back to first option
1749 | await option1Driver.component.click();
1750 |
1751 | // Field should retain its value
1752 | await expect(textfield1Driver.field).toHaveValue("Test Value");
1753 | });
1754 | });
1755 |
1756 | // =============================================================================
1757 | // ORIGINAL TEST SUITE (LEGACY TESTS)
1758 | // =============================================================================
1759 |
1760 | test("mock service responds", async ({ initTestBed, createFormDriver }) => {
1761 | await initTestBed(
1762 | `
1763 | <Form submitUrl="/test" />`,
1764 | {
1765 | apiInterceptor: {
1766 | operations: {
1767 | test: {
1768 | url: "/test",
1769 | method: "post",
1770 | handler: `return true;`,
1771 | },
1772 | },
1773 | },
1774 | },
1775 | );
1776 | const driver = await createFormDriver();
1777 | await driver.submitForm();
1778 |
1779 | const request = await driver.getSubmitResponse("/test");
1780 | expect(request.ok()).toEqual(true);
1781 | });
1782 |
1783 | // --- $data
1784 |
1785 | test("$data is correctly bound to form data", async ({ initTestBed, createButtonDriver }) => {
1786 | await initTestBed(`
1787 | <Form data="{{ field: 'test' }}">
1788 | <FormItem label="testField" bindTo="field">
1789 | <Button testId="custom" label="{$data.field}" />
1790 | </FormItem>
1791 | </Form> `);
1792 | const driver = await createButtonDriver("custom");
1793 | await expect(driver.component).toHaveExplicitLabel("test");
1794 | });
1795 |
1796 | test("$data is correctly undefined if data is not set in props", async ({
1797 | initTestBed,
1798 | createButtonDriver,
1799 | }) => {
1800 | await initTestBed(`
1801 | <Form>
1802 | <FormItem label="testField" bindTo="field">
1803 | <Button testId="custom" label="{$data.field}" />
1804 | </FormItem>
1805 | </Form> `);
1806 | const driver = await createButtonDriver("custom");
1807 | await expect(driver.component).toHaveExplicitLabel(undefined);
1808 | });
1809 |
1810 | test("Form buttons and contained FormItems are enabled", async ({
1811 | initTestBed,
1812 | page,
1813 | createFormDriver,
1814 | }) => {
1815 | await initTestBed(`
1816 | <Form testId="form">
1817 | <FormItem label="Name" bindTo="name" />
1818 | <FormItem label="Email" bindTo="email" />
1819 | </Form>
1820 | `);
1821 |
1822 | const driver = await createFormDriver("form");
1823 | await expect(page.getByText("Name")).toBeVisible();
1824 | await expect(page.getByText("Email")).toBeVisible();
1825 | await expect(driver.cancelButton).toBeEnabled();
1826 | await expect(driver.submitButton).toBeEnabled();
1827 | });
1828 |
1829 | test("submit only triggers when enabled", async ({ initTestBed, createFormDriver }) => {
1830 | const { testStateDriver } = await initTestBed(
1831 | `
1832 | <Form enabled="false" data="{{ name: 'John' }}" onSubmit="testState = true">
1833 | <FormItem bindTo="name" />
1834 | </Form>`,
1835 | );
1836 | const driver = await createFormDriver();
1837 | await expect(driver.submitButton).toBeDisabled();
1838 |
1839 | await driver.submitForm("keypress");
1840 | await expect.poll(testStateDriver.testState).toEqual(null);
1841 | });
1842 |
1843 | test("submit with unbound fields", async ({ page, initTestBed, createFormDriver }) => {
1844 | await initTestBed(`
1845 | <Fragment var.output="none">
1846 | <Form testId="form"
1847 | data="{{ firstname: 'James', lastname: 'Clewell' }}"
1848 | onSubmit="args => output = JSON.stringify(args)">
1849 | <FormItem label="Firstname" bindTo="firstname" />
1850 | <FormItem label="Middle name" initialValue="Robert" />
1851 | <FormItem label="Lastname" />
1852 | </Form>
1853 | <Text testId="text">{output}</Text>
1854 | </Fragment>
1855 | `);
1856 | const driver = await createFormDriver("form");
1857 | await driver.submitForm();
1858 | await expect(page.getByTestId("text")).toHaveText('{"firstname":"James"}');
1859 | });
1860 |
1861 | test(`submit with type 'items'`, async ({
1862 | initTestBed,
1863 | createFormDriver,
1864 | createButtonDriver,
1865 | createFormItemDriver,
1866 | }) => {
1867 | const { testStateDriver } = await initTestBed(`
1868 | <Form onSubmit="data => testState = data" testId="form">
1869 | <FormItem testId="formItem" type="items" bindTo="arrayItems" id="arrayItems">
1870 | <FormItem bindTo="name" testId="text{$itemIndex}"/>
1871 | </FormItem>
1872 | <Button testId="addButton" onClick="arrayItems.addItem()"/>
1873 | </Form>`);
1874 |
1875 | await (await createButtonDriver("addButton")).click();
1876 | await (await createFormItemDriver("text0")).textBox.fill("John");
1877 | await (await createButtonDriver("addButton")).click();
1878 | await (await createFormItemDriver("text1")).textBox.fill("Peter");
1879 | const driver = await createFormDriver("form");
1880 | await driver.submitForm();
1881 | await expect.poll(testStateDriver.testState).toStrictEqual({
1882 | arrayItems: [{ name: "John" }, { name: "Peter" }],
1883 | });
1884 | });
1885 |
1886 | test(`submit with type 'items', empty bindTo`, async ({
1887 | initTestBed,
1888 | createFormDriver,
1889 | createButtonDriver,
1890 | createFormItemDriver,
1891 | }) => {
1892 | const { testStateDriver } = await initTestBed(`
1893 | <Form onSubmit="data => testState = data" testId="form">
1894 | <FormItem testId="formItem" type="items" bindTo="arrayItems" id="arrayItems">
1895 | <FormItem testId="text{$itemIndex}" bindTo=""/>
1896 | </FormItem>
1897 | <Button testId="addButton" onClick="arrayItems.addItem()"/>
1898 | </Form>`);
1899 |
1900 | await (await createButtonDriver("addButton")).click();
1901 | await (await createFormItemDriver("text0")).textBox.fill("John");
1902 | await (await createButtonDriver("addButton")).click();
1903 | await (await createFormItemDriver("text1")).textBox.fill("Peter");
1904 | const driver = await createFormDriver("form");
1905 | await driver.submitForm();
1906 | await expect.poll(testStateDriver.testState).toStrictEqual({
1907 | arrayItems: ["John", "Peter"],
1908 | });
1909 | });
1910 |
1911 | // --- Testing
1912 |
1913 | // --- --- buttonRowTemplate
1914 |
1915 | test("buttonRowTemplate can render buttons", async ({ initTestBed, createButtonDriver }) => {
1916 | await initTestBed(`
1917 | <Form>
1918 | <property name="buttonRowTemplate">
1919 | <Button testId="submitBtn" type="submit" label="Hello Button" />
1920 | </property>
1921 | </Form>`);
1922 | await expect((await createButtonDriver("submitBtn")).component).toBeAttached();
1923 | });
1924 |
1925 | test("buttonRowTemplate replaces built-in buttons", async ({ initTestBed, createFormDriver }) => {
1926 | await initTestBed(`
1927 | <Form testId="form">
1928 | <property name="buttonRowTemplate">
1929 | <Button testId="submitBtn" type="submit" label="Hello Button" />
1930 | </property>
1931 | </Form>`);
1932 |
1933 | const driver = await createFormDriver("form");
1934 | await expect(driver.submitButton).not.toBeVisible();
1935 | await expect(driver.cancelButton).not.toBeVisible();
1936 | });
1937 |
1938 | test("setting buttonRowTemplate without buttons still runs submit on Enter", async ({
1939 | initTestBed,
1940 | createFormDriver,
1941 | }) => {
1942 | const { testStateDriver } = await initTestBed(`
1943 | <Form onSubmit="testState = true">
1944 | <property name="buttonRowTemplate">
1945 | <Fragment />
1946 | </property>
1947 | <FormItem bindTo="name" />
1948 | </Form>
1949 | `);
1950 | const driver = await createFormDriver();
1951 |
1952 | await driver.submitForm("keypress");
1953 | await expect.poll(testStateDriver.testState).toBe(true);
1954 | });
1955 |
1956 | test("data accepts an object", async ({
1957 | initTestBed,
1958 | createFormItemDriver,
1959 | createTextBoxDriver,
1960 | }) => {
1961 | await initTestBed(`
1962 | <Form data="{{ field1: 'test' }}">
1963 | <FormItem testId="inputField" bindTo="field1" />
1964 | </Form>
1965 | `);
1966 | const driver = await createFormItemDriver("inputField");
1967 | await expect((await createTextBoxDriver(driver.input)).field).toHaveValue("test");
1968 | });
1969 |
1970 | test(`data accepts primitive`, async ({ initTestBed, createFormDriver }) => {
1971 | await initTestBed(`
1972 | <Form data="test">
1973 | <FormItem bindTo="field1" />
1974 | </Form>
1975 | `);
1976 | const component = (await createFormDriver()).component;
1977 | await expect(component).toBeAttached();
1978 | });
1979 |
1980 | test(`data accepts empty array`, async ({ initTestBed, createFormDriver }) => {
1981 | await initTestBed(`
1982 | <Form data="{[]}">
1983 | <FormItem bindTo="field1" />
1984 | </Form>
1985 | `);
1986 | const component = (await createFormDriver()).component;
1987 | await expect(component).toBeAttached();
1988 | });
1989 |
1990 | test("data accepts relative URL endpoint", async ({
1991 | initTestBed,
1992 | createFormItemDriver,
1993 | createTextBoxDriver,
1994 | }) => {
1995 | await initTestBed(
1996 | `
1997 | <Form data="/test">
1998 | <FormItem testId="inputField" bindTo="name" />
1999 | </Form>`,
2000 | {
2001 | apiInterceptor: {
2002 | operations: {
2003 | test: {
2004 | url: "/test",
2005 | method: "get",
2006 | handler: `return { name: 'John' };`,
2007 | },
2008 | },
2009 | },
2010 | },
2011 | );
2012 | const driver = await createFormItemDriver("inputField");
2013 | await expect((await createTextBoxDriver(driver.input)).field).toHaveValue("John");
2014 | });
2015 |
2016 | test("cancel button and save button use default label", async ({
2017 | initTestBed,
2018 | createFormDriver,
2019 | }) => {
2020 | await initTestBed(`
2021 | <Form testId="form">
2022 | <FormItem label="Name" bindTo="name" />
2023 | <FormItem label="Email" bindTo="email" />
2024 | </Form>
2025 | `);
2026 |
2027 | const driver = await createFormDriver("form");
2028 | await expect(driver.cancelButton).toHaveText("Cancel");
2029 | await expect(driver.submitButton).toHaveText("Save");
2030 | });
2031 |
2032 | test("cancel button is rendered with cancelLabel", async ({ initTestBed, createFormDriver }) => {
2033 | await initTestBed(`
2034 | <Form testId="form" cancelLabel="Abort">
2035 | <FormItem label="Name" bindTo="name" />
2036 | <FormItem label="Email" bindTo="email" />
2037 | </Form>
2038 | `);
2039 |
2040 | const driver = await createFormDriver("form");
2041 | await expect(driver.cancelButton).toHaveText("Abort");
2042 | await expect(driver.submitButton).toHaveText("Save");
2043 | });
2044 |
2045 | test("save button is rendered with saveLabel", async ({ initTestBed, createFormDriver }) => {
2046 | await initTestBed(`
2047 | <Form testId="form" saveLabel="Submit">
2048 | <FormItem label="Name" bindTo="name" />
2049 | <FormItem label="Email" bindTo="email" />
2050 | </Form>
2051 | `);
2052 |
2053 | const driver = await createFormDriver("form");
2054 | await expect(driver.cancelButton).toHaveText("Cancel");
2055 | await expect(driver.submitButton).toHaveText("Submit");
2056 | });
2057 |
2058 | // swapCancelAndSave
2059 |
2060 | test("built-in button row order is default if swapCancelAndSave is false", async ({
2061 | initTestBed,
2062 | createFormDriver,
2063 | }) => {
2064 | await initTestBed(`
2065 | <Form testId="form" saveLabel="Submit">
2066 | <FormItem label="Name" bindTo="name" />
2067 | <FormItem label="Email" bindTo="email" />
2068 | </Form>
2069 | `);
2070 |
2071 | const driver = await createFormDriver("form");
2072 | const cancelBox = await driver.cancelButton.boundingBox();
2073 | const submitBox = await driver.submitButton.boundingBox();
2074 | expect(cancelBox.x).toBeLessThan(submitBox.x);
2075 | });
2076 |
2077 | test("built-in button row order flips if swapCancelAndSave is true", async ({
2078 | initTestBed,
2079 | createFormDriver,
2080 | }) => {
2081 | await initTestBed(`
2082 | <Form testId="form" saveLabel="Submit" swapCancelAndSave="true">
2083 | <FormItem label="Name" bindTo="name" />
2084 | <FormItem label="Email" bindTo="email" />
2085 | </Form>
2086 | `);
2087 |
2088 | const driver = await createFormDriver("form");
2089 | const cancelBox = await driver.cancelButton.boundingBox();
2090 | const submitBox = await driver.submitButton.boundingBox();
2091 | expect(cancelBox.x).toBeGreaterThan(submitBox.x);
2092 | });
2093 |
2094 | // --- submitUrl
2095 |
2096 | test("form submits to correct url", async ({ initTestBed, createFormDriver }) => {
2097 | const endpoint = "/test";
2098 | await initTestBed(
2099 | `
2100 | <Form data="{{ name: 'John' }}" submitUrl="${endpoint}" submitMethod="post">
2101 | <FormItem bindTo="name" />
2102 | </Form>`,
2103 | {
2104 | apiInterceptor: {
2105 | operations: {
2106 | test: {
2107 | url: endpoint,
2108 | method: "post",
2109 | handler: `{ return true; }`,
2110 | },
2111 | },
2112 | },
2113 | },
2114 | );
2115 | const driver = await createFormDriver();
2116 |
2117 | await driver.submitForm();
2118 | const response = await driver.getSubmitResponse(endpoint);
2119 | expect(response.ok()).toBe(true);
2120 | expect(new URL(response.url()).pathname).toBe(endpoint);
2121 | });
2122 |
2123 | // --- submitMethod
2124 |
2125 | // NOTE: GET doesn't work because GET/HEAD cannot have a 'body'
2126 | ["post", "put", "delete"].forEach((method) => {
2127 | test(`${method} REST op on submit`, async ({ initTestBed, createFormDriver }) => {
2128 | await initTestBed(`<Form submitUrl="/test" submitMethod="${method}" />`, {
2129 | apiInterceptor: {
2130 | operations: {
2131 | testPost: {
2132 | url: "/test",
2133 | method: "post",
2134 | handler: `return true;`,
2135 | },
2136 | testPut: {
2137 | url: "/test",
2138 | method: "put",
2139 | handler: `return true;`,
2140 | },
2141 | testDelete: {
2142 | url: "/test",
2143 | method: "delete",
2144 | handler: `return true;`,
2145 | },
2146 | },
2147 | },
2148 | });
2149 | const driver = await createFormDriver();
2150 | const request = await driver.getSubmitRequest("/test", method, "click");
2151 | expect(request.failure()).toBeNull();
2152 | });
2153 | });
2154 |
2155 | // --- submitting the Form
2156 |
2157 | test("submit triggers when clicking save/submit button", async ({
2158 | initTestBed,
2159 | createFormDriver,
2160 | }) => {
2161 | await initTestBed(
2162 | `
2163 | <Form data="{{ name: 'John' }}" submitUrl="/test" submitMethod="post">
2164 | <FormItem bindTo="name" />
2165 | </Form>`,
2166 | {
2167 | apiInterceptor: {
2168 | operations: {
2169 | test: {
2170 | url: "/test",
2171 | method: "post",
2172 | handler: `return true;`,
2173 | },
2174 | },
2175 | },
2176 | },
2177 | );
2178 | const driver = await createFormDriver();
2179 |
2180 | const request = await driver.getSubmitRequest("/test", "POST", "click");
2181 | expect(request.failure()).toBeNull();
2182 | });
2183 |
2184 | test("submit triggers when pressing Enter", async ({ initTestBed, createFormDriver }) => {
2185 | await initTestBed(
2186 | `
2187 | <Form data="{{ name: 'John' }}" submitUrl="/test" submitMethod="post">
2188 | <FormItem bindTo="name" />
2189 | </Form>`,
2190 | {
2191 | apiInterceptor: {
2192 | operations: {
2193 | test: {
2194 | url: "/test",
2195 | method: "post",
2196 | handler: `return true;`,
2197 | },
2198 | },
2199 | },
2200 | },
2201 | );
2202 | const driver = await createFormDriver();
2203 |
2204 | const request = await driver.getSubmitRequest("/test", "POST", "keypress");
2205 | expect(request.failure()).toBeNull();
2206 | });
2207 |
2208 | test("user cannot submit with clientside errors present", async ({
2209 | initTestBed,
2210 | createFormDriver,
2211 | }) => {
2212 | const { testStateDriver } = await initTestBed(`
2213 | <Form onSubmit="testState = true">
2214 | <FormItem bindTo="name" required="true" />
2215 | </Form>
2216 | `);
2217 | const driver = await createFormDriver();
2218 |
2219 | // The onSubmit event should have been triggered if not for the client error of an empty required field
2220 | await driver.submitForm("click");
2221 | await expect.poll(testStateDriver.testState).toEqual(null);
2222 | });
2223 |
2224 | // --- backend validation summary
2225 |
2226 | test("submitting with errors shows validation summary", async ({
2227 | initTestBed,
2228 | createFormDriver,
2229 | }) => {
2230 | await initTestBed(`<Form submitUrl="/general-validation-error" submitMethod="post" />`, {
2231 | apiInterceptor: errorDisplayInterceptor,
2232 | });
2233 | const driver = await createFormDriver();
2234 | await driver.submitForm();
2235 | await expect(await driver.getValidationSummary()).toBeVisible();
2236 | });
2237 |
2238 | test("submitting without errors does not show summary", async ({
2239 | initTestBed,
2240 | createFormDriver,
2241 | }) => {
2242 | await initTestBed(`<Form submitUrl="/no-validation-error" submitMethod="post" />`, {
2243 | apiInterceptor: errorDisplayInterceptor,
2244 | });
2245 | const driver = await createFormDriver();
2246 | await driver.submitForm();
2247 | await expect(await driver.getValidationSummary()).not.toBeVisible();
2248 | });
2249 |
2250 | test("general error messages are rendered in the summary", async ({
2251 | initTestBed,
2252 | createFormDriver,
2253 | createValidationDisplayDriver,
2254 | }) => {
2255 | await initTestBed(`<Form submitUrl="/general-validation-error" submitMethod="post" />`, {
2256 | apiInterceptor: errorDisplayInterceptor,
2257 | });
2258 | const formDriver = await createFormDriver();
2259 | await formDriver.submitForm();
2260 |
2261 | // TODO: strip this down -> it's verbose but hard to read
2262 | const warningDisplay = await createValidationDisplayDriver(
2263 | await formDriver.getValidationDisplaysBySeverity("warning"),
2264 | );
2265 | const errorDisplay = await createValidationDisplayDriver(
2266 | await formDriver.getValidationDisplaysBySeverity("error"),
2267 | );
2268 |
2269 | expect(await warningDisplay.getText()).toContain("Warning for the whole form");
2270 | expect(await errorDisplay.getText()).toContain("Error for the whole form");
2271 | });
2272 |
2273 | test("field-related errors are rendered at FormItems", async ({
2274 | initTestBed,
2275 | createFormDriver,
2276 | createFormItemDriver,
2277 | }) => {
2278 | await initTestBed(
2279 | `
2280 | <Form submitUrl="/field-validation-error" submitMethod="post">
2281 | <FormItem testId="testField" bindTo="test" label="test" />
2282 | </Form>`,
2283 | {
2284 | apiInterceptor: errorDisplayInterceptor,
2285 | },
2286 | );
2287 | const formDriver = await createFormDriver();
2288 | const fieldDriver = await createFormItemDriver("testField");
2289 |
2290 | await formDriver.submitForm();
2291 | await expect(fieldDriver.validationStatusIndicator).toHaveAttribute(
2292 | fieldDriver.validationStatusTag,
2293 | "warning",
2294 | );
2295 | });
2296 |
2297 | test("field-related errors map to correct FormItems", async ({
2298 | initTestBed,
2299 | createFormDriver,
2300 | createFormItemDriver,
2301 | }) => {
2302 | await initTestBed(
2303 | `
2304 | <Form submitUrl="/field-validation-error" submitMethod="post">
2305 | <FormItem testId="testField" bindTo="test" label="test" />
2306 | <FormItem testId="testField2" bindTo="test2" label="test2" />
2307 | </Form>`,
2308 | {
2309 | apiInterceptor: errorDisplayInterceptor,
2310 | },
2311 | );
2312 | const formDriver = await createFormDriver();
2313 | const fieldDriver = await createFormItemDriver("testField");
2314 |
2315 | await formDriver.submitForm();
2316 | await expect(fieldDriver.validationStatusIndicator).toHaveAttribute(
2317 | fieldDriver.validationStatusTag,
2318 | "warning",
2319 | );
2320 | });
2321 |
2322 | test.skip("field-related errors disappear if user updates FormItems", async ({
2323 | initTestBed,
2324 | page,
2325 | createFormItemDriver,
2326 | }) => {
2327 | await initTestBed(
2328 | `
2329 | <Form testId="form">
2330 | <FormItem testId="testField" bindTo="test" label="test" required />
2331 | <FormItem testId="testField2" bindTo="test2" label="test2" />
2332 | </Form>`,
2333 | );
2334 |
2335 | const fieldDriver = await createFormItemDriver("testField");
2336 | const fieldDriver2 = await createFormItemDriver("testField2");
2337 |
2338 | await fieldDriver.component.focus();
2339 | await fieldDriver.textBox.fill("a");
2340 | await fieldDriver.textBox.fill("");
2341 | await fieldDriver.textBox.blur();
2342 |
2343 | // Should show required error now
2344 | await expect(fieldDriver.textBox).toHaveValue("");
2345 | await expect(fieldDriver.validationStatusIndicator).toHaveAttribute(
2346 | fieldDriver.validationStatusTag,
2347 | "error",
2348 | );
2349 |
2350 | await fieldDriver.textBox.fill("a");
2351 | await fieldDriver.textBox.blur();
2352 |
2353 | await fieldDriver2.textBox.focus();
2354 |
2355 | await expect(fieldDriver2.textBox).toBeFocused();
2356 |
2357 | await fieldDriver2.textBox.fill("b");
2358 | await expect(fieldDriver.validationStatusIndicator).not.toBeVisible();
2359 | });
2360 |
2361 | const smartCrudInterceptor: ApiInterceptorDefinition = {
2362 | initialize: `
2363 | $state.items = {
2364 | [10]: { name: "Smith", id: 10 }
2365 | };
2366 | $state.currentId = 10;
2367 | `,
2368 | operations: {
2369 | create: {
2370 | url: "/entities",
2371 | method: "post",
2372 | handler: `() => {
2373 | $state.currentId++;
2374 | $state.items[$state.currentId] = $requestBody;
2375 | $state.items[$state.currentId].id = $state.currentId;
2376 |
2377 | return $state.items[$state.currentId];
2378 | }`,
2379 | },
2380 | read: {
2381 | url: "/entities/:id",
2382 | method: "get",
2383 | handler: `() => {
2384 | return $state.items[$pathParams.id];
2385 | }`,
2386 | },
2387 | update: {
2388 | url: "/entities/:id",
2389 | method: "put",
2390 | handler: `() => {
2391 | $state.items[$pathParams.id] = { ...$state.items[$pathParams.id], ...$requestBody };
2392 | return $state.items[$pathParams.id];
2393 | }`,
2394 | },
2395 | },
2396 | };
2397 |
2398 | test("create form works with submitUrl", async ({
2399 | initTestBed,
2400 | createFormDriver,
2401 | createFormItemDriver,
2402 | createTextBoxDriver,
2403 | }) => {
2404 | await initTestBed(
2405 | `
2406 | <Form submitUrl="/entities">
2407 | <FormItem bindTo="name" testId="nameInput"/>
2408 | </Form>
2409 | `,
2410 | { apiInterceptor: smartCrudInterceptor },
2411 | );
2412 | const formDriver = await createFormDriver();
2413 | const inputElement = (await createFormItemDriver("nameInput")).input;
2414 | const fieldDriver = await createTextBoxDriver(inputElement);
2415 |
2416 | await fieldDriver.field.fill("John");
2417 | await formDriver.submitForm("click");
2418 |
2419 | const response = await formDriver.getSubmitResponse();
2420 | expect(await response.json()).toEqual({
2421 | name: "John",
2422 | id: 11,
2423 | });
2424 | });
2425 |
2426 | test("regression: data url through modal context", async ({
2427 | initTestBed,
2428 | createButtonDriver,
2429 | createFormDriver,
2430 | createFormItemDriver,
2431 | createTextBoxDriver,
2432 | }) => {
2433 | await initTestBed(
2434 | `
2435 | <Fragment>
2436 | <Button testId="openModalButton" onClick="modal.open({data: '/entities/10'})"/>
2437 | <ModalDialog id="modal">
2438 | <Form testId="modalForm" data="{$param.data}" submitUrl="{$param.submitUrl}">
2439 | <FormItem bindTo="name" testId="nameInput"/>
2440 | </Form>
2441 | </ModalDialog>
2442 | </Fragment>
2443 | `,
2444 | {
2445 | apiInterceptor: smartCrudInterceptor,
2446 | },
2447 | );
2448 | const formDriver = await createFormDriver("modalForm");
2449 | const inputElement = (await createFormItemDriver("nameInput")).input;
2450 | const inputDriver = await createTextBoxDriver(inputElement);
2451 |
2452 | await (await createButtonDriver("openModalButton")).click();
2453 |
2454 | await expect(inputDriver.field).toHaveValue("Smith");
2455 |
2456 | await inputDriver.field.fill("EDITED-Smith");
2457 | await formDriver.submitForm("click");
2458 |
2459 | const response = await formDriver.getSubmitResponse();
2460 | expect(await response.json()).toEqual({
2461 | name: "EDITED-Smith",
2462 | id: 10,
2463 | });
2464 | });
2465 |
2466 | // --- Conditional Rendering Cases
2467 |
2468 | test("can submit with invisible required field", async ({
2469 | initTestBed,
2470 | createFormDriver,
2471 | createFormItemDriver,
2472 | createTextBoxDriver,
2473 | page,
2474 | }) => {
2475 | const { testStateDriver } = await initTestBed(`
2476 | <Form onSubmit="testState = true">
2477 | <FormItem testId="select" bindTo="authenticationType"
2478 | type="select" label="Authentication Type:" initialValue="{0}">
2479 | <Option value="{0}" label="Password" />
2480 | <Option value="{1}" label="Public Key" />
2481 | </FormItem>
2482 | <FormItem label="name1" testId="name1" bindTo="name1"
2483 | required="true" when="{$data.authenticationType == 0}"/>
2484 | <FormItem label="name2" testId="name2" bindTo="name2"
2485 | required="true" when="{$data.authenticationType == 1}"/>
2486 | </Form>
2487 | `);
2488 | const formDriver = await createFormDriver();
2489 | const selectDriver = await createFormItemDriver("select");
2490 | const textfieldElement = (await createFormItemDriver("name2")).input;
2491 | const textfieldDriver = await createTextBoxDriver(textfieldElement);
2492 |
2493 | await selectDriver.component.click();
2494 | await page.getByText("Public Key").click();
2495 | await textfieldDriver.field.fill("John");
2496 | await formDriver.submitForm();
2497 |
2498 | await expect.poll(testStateDriver.testState).toEqual(true);
2499 | });
2500 |
2501 | test("conditional fields keep the state", async ({
2502 | initTestBed,
2503 | createFormItemDriver,
2504 | createOptionDriver,
2505 | createTextBoxDriver,
2506 | }) => {
2507 | await initTestBed(`
2508 | <Form>
2509 | <FormItem testId="select" bindTo="authenticationType"
2510 | type="radioGroup" label="Authentication Type:" initialValue="{0}">
2511 | <Option value="{0}" label="Password" testId="password"/>
2512 | <Option value="{1}" label="Public Key" testId="publicKey" />
2513 | </FormItem>
2514 | <FormItem label="name1" testId="name1" bindTo="name1"
2515 | required="true" when="{$data.authenticationType == 0}"/>
2516 | <FormItem label="name2" testId="name2" bindTo="name2"
2517 | required="true" when="{$data.authenticationType == 1}"/>
2518 | </Form>
2519 | `);
2520 | const option1Driver = await createFormItemDriver("password");
2521 | const option2Driver = await createOptionDriver("publicKey");
2522 | const textfield1Element = (await createFormItemDriver("name1")).input;
2523 | const textfield1Driver = await createTextBoxDriver(textfield1Element);
2524 | const textfield2Element = (await createFormItemDriver("name2")).input;
2525 | const textfield2Driver = await createTextBoxDriver(textfield2Element);
2526 |
2527 | await textfield1Driver.field.fill("name1");
2528 | await option2Driver.click();
2529 | await textfield2Driver.field.fill("name2");
2530 | await option1Driver.click();
2531 |
2532 | await expect(textfield1Driver.field).toHaveValue("name1");
2533 | });
2534 |
2535 | // =============================================================================
2536 | // BEHAVIORS AND PARTS TESTS
2537 | // =============================================================================
2538 |
2539 | test.describe("Behaviors and Parts", () => {
2540 | test("can select part: 'cancelButton'", async ({ page, initTestBed }) => {
2541 | await initTestBed(`<Form testId="test" />`);
2542 | const cancelButton = page.locator("[data-part-id='cancelButton']");
2543 | await expect(cancelButton).toBeVisible();
2544 | await expect(cancelButton).toHaveText("Cancel");
2545 | });
2546 |
2547 | test("can select part: 'submitButton'", async ({ page, initTestBed }) => {
2548 | await initTestBed(`<Form testId="test" />`);
2549 | const submitButton = page.locator("[data-part-id='submitButton']");
2550 | await expect(submitButton).toBeVisible();
2551 | await expect(submitButton).toHaveText("Save");
2552 | });
2553 |
2554 | test("cancelButton part is not present when cancelLabel is empty", async ({
2555 | page,
2556 | initTestBed,
2557 | }) => {
2558 | await initTestBed(`<Form testId="test" cancelLabel="" />`);
2559 | const cancelButton = page.locator("[data-part-id='cancelButton']");
2560 | await expect(cancelButton).not.toBeVisible();
2561 | });
2562 |
2563 | test("both parts are visible with default props", async ({ page, initTestBed }) => {
2564 | await initTestBed(`<Form testId="test" />`);
2565 |
2566 | const cancelButton = page.locator("[data-part-id='cancelButton']");
2567 | const submitButton = page.locator("[data-part-id='submitButton']");
2568 |
2569 | await expect(cancelButton).toBeVisible();
2570 | await expect(submitButton).toBeVisible();
2571 | });
2572 | });
2573 |
2574 | // =============================================================================
2575 | // API TESTS
2576 | // =============================================================================
2577 |
2578 | test.describe("Api", () => {
2579 | test("getData returns a copy of current form data", async ({ initTestBed, page }) => {
2580 | const { testStateDriver } = await initTestBed(`
2581 | <Fragment>
2582 | <Form id="myForm">
2583 | <FormItem label="Name" bindTo="name" initialValue="John" />
2584 | <FormItem label="Email" bindTo="email" initialValue="[email protected]" />
2585 | </Form>
2586 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2587 | </Fragment>
2588 | `);
2589 |
2590 | await page.getByTestId("getDataBtn").click();
2591 | const data = await testStateDriver.testState();
2592 |
2593 | expect(data).toEqual({
2594 | name: "John",
2595 | email: "[email protected]",
2596 | });
2597 | });
2598 |
2599 | test("getData returns updated data after field changes", async ({
2600 | initTestBed,
2601 | page,
2602 | createTextBoxDriver,
2603 | createFormItemDriver,
2604 | }) => {
2605 | const { testStateDriver } = await initTestBed(`
2606 | <Fragment>
2607 | <Form id="myForm">
2608 | <FormItem label="Name" bindTo="name" initialValue="John" testId="nameInput" />
2609 | </Form>
2610 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2611 | </Fragment>
2612 | `);
2613 |
2614 | const inputElement = (await createFormItemDriver("nameInput")).input;
2615 | const fieldDriver = await createTextBoxDriver(inputElement);
2616 |
2617 | await fieldDriver.field.fill("Jane");
2618 | await page.getByTestId("getDataBtn").click();
2619 | const data = await testStateDriver.testState();
2620 |
2621 | expect(data.name).toEqual("Jane");
2622 | });
2623 |
2624 | test("getData returns empty object for form with no data", async ({ initTestBed, page }) => {
2625 | const { testStateDriver } = await initTestBed(`
2626 | <Fragment>
2627 | <Form id="myForm" />
2628 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2629 | </Fragment>
2630 | `);
2631 |
2632 | await page.getByTestId("getDataBtn").click();
2633 | const data = await testStateDriver.testState();
2634 |
2635 | expect(data).toEqual({});
2636 | });
2637 |
2638 | test("getData returns deep clone - modifications do not affect form state", async ({
2639 | initTestBed,
2640 | page,
2641 | createTextBoxDriver,
2642 | createFormItemDriver,
2643 | }) => {
2644 | const { testStateDriver } = await initTestBed(`
2645 | <Fragment>
2646 | <Form id="myForm">
2647 | <FormItem label="Name" bindTo="name" initialValue="John" testId="nameInput" />
2648 | </Form>
2649 | <Button testId="getDataBtn" onClick="testState = myForm.getData(); testState.name = 'Modified'" />
2650 | <Button testId="getDataAgainBtn" onClick="testState = myForm.getData()" />
2651 | </Fragment>
2652 | `);
2653 |
2654 | await page.getByTestId("getDataBtn").click();
2655 | let data = await testStateDriver.testState();
2656 | expect(data.name).toEqual("Modified");
2657 |
2658 | // Get data again to verify form still has original value
2659 | await page.getByTestId("getDataAgainBtn").click();
2660 | data = await testStateDriver.testState();
2661 | expect(data.name).toEqual("John");
2662 | });
2663 |
2664 | test("getData excludes unbound fields (fields ending with __UNBOUND_FIELD__)", async ({
2665 | initTestBed,
2666 | page,
2667 | }) => {
2668 | const { testStateDriver } = await initTestBed(`
2669 | <Fragment>
2670 | <Form id="myForm">
2671 | <FormItem label="Name" bindTo="name" initialValue="John" />
2672 | <FormItem label="Confirm" bindTo="confirm__UNBOUND_FIELD__" initialValue="yes" />
2673 | </Form>
2674 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2675 | </Fragment>
2676 | `);
2677 |
2678 | await page.getByTestId("getDataBtn").click();
2679 | const data = await testStateDriver.testState();
2680 |
2681 | expect(data).toEqual({
2682 | name: "John",
2683 | });
2684 | expect(data.confirm__UNBOUND_FIELD__).toBeUndefined();
2685 | });
2686 |
2687 | test("getData excludes fields marked with noSubmit", async ({ initTestBed, page }) => {
2688 | const { testStateDriver } = await initTestBed(`
2689 | <Fragment>
2690 | <Form id="myForm">
2691 | <FormItem label="Name" bindTo="name" initialValue="John" />
2692 | <FormItem label="Password" bindTo="password" initialValue="secret" noSubmit="true" />
2693 | </Form>
2694 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2695 | </Fragment>
2696 | `);
2697 |
2698 | await page.getByTestId("getDataBtn").click();
2699 | const data = await testStateDriver.testState();
2700 |
2701 | expect(data).toEqual({
2702 | name: "John",
2703 | });
2704 | expect(data.password).toBeUndefined();
2705 | });
2706 |
2707 | test("getData works with complex nested data structures", async ({
2708 | initTestBed,
2709 | page,
2710 | createTextBoxDriver,
2711 | createFormItemDriver,
2712 | }) => {
2713 | const { testStateDriver } = await initTestBed(`
2714 | <Fragment>
2715 | <Form id="myForm" data="{{address: {street: '123 Main St', city: 'Springfield'}}}">
2716 | <FormItem label="Street" bindTo="address.street" testId="streetInput" />
2717 | <FormItem label="City" bindTo="address.city" testId="cityInput" />
2718 | </Form>
2719 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2720 | </Fragment>
2721 | `);
2722 |
2723 | const streetElement = (await createFormItemDriver("streetInput")).input;
2724 | const streetDriver = await createTextBoxDriver(streetElement);
2725 | await streetDriver.field.fill("456 Oak Ave");
2726 |
2727 | await page.getByTestId("getDataBtn").click();
2728 | const data = await testStateDriver.testState();
2729 |
2730 | expect(data).toEqual({
2731 | address: {
2732 | street: "456 Oak Ave",
2733 | city: "Springfield",
2734 | },
2735 | });
2736 | });
2737 |
2738 | test("getData can be called multiple times without side effects", async ({
2739 | initTestBed,
2740 | page,
2741 | }) => {
2742 | const { testStateDriver } = await initTestBed(`
2743 | <Fragment>
2744 | <Form id="myForm">
2745 | <FormItem label="Name" bindTo="name" initialValue="John" />
2746 | </Form>
2747 | <Button testId="getDataBtn" onClick="testState = (testState || 0) + 1; myForm.getData()" />
2748 | </Fragment>
2749 | `);
2750 |
2751 | await page.getByTestId("getDataBtn").click();
2752 | let counter = await testStateDriver.testState();
2753 | expect(counter).toEqual(1);
2754 |
2755 | await page.getByTestId("getDataBtn").click();
2756 | counter = await testStateDriver.testState();
2757 | expect(counter).toEqual(2);
2758 |
2759 | await page.getByTestId("getDataBtn").click();
2760 | counter = await testStateDriver.testState();
2761 | expect(counter).toEqual(3);
2762 | });
2763 |
2764 | test("getData with empty field values", async ({
2765 | initTestBed,
2766 | page,
2767 | createTextBoxDriver,
2768 | createFormItemDriver,
2769 | }) => {
2770 | const { testStateDriver } = await initTestBed(`
2771 | <Fragment>
2772 | <Form id="myForm">
2773 | <FormItem label="Name" bindTo="name" initialValue="John" testId="nameInput" />
2774 | <FormItem label="Email" bindTo="email" testId="emailInput" />
2775 | </Form>
2776 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2777 | </Fragment>
2778 | `);
2779 |
2780 | const nameElement = (await createFormItemDriver("nameInput")).input;
2781 | const nameDriver = await createTextBoxDriver(nameElement);
2782 | await nameDriver.field.fill("");
2783 |
2784 | await page.getByTestId("getDataBtn").click();
2785 | const data = await testStateDriver.testState();
2786 |
2787 | expect(data.name).toEqual("");
2788 | expect(data.email).toBeUndefined();
2789 | });
2790 |
2791 | test("getData with multiple calls returns consistent data", async ({
2792 | initTestBed,
2793 | page,
2794 | createTextBoxDriver,
2795 | createFormItemDriver,
2796 | }) => {
2797 | const { testStateDriver } = await initTestBed(`
2798 | <Fragment>
2799 | <Form id="myForm">
2800 | <FormItem label="Name" bindTo="name" initialValue="John" testId="nameInput" />
2801 | </Form>
2802 | <Button testId="getDataBtn" onClick="testState = myForm.getData()" />
2803 | </Fragment>
2804 | `);
2805 |
2806 | const inputElement = (await createFormItemDriver("nameInput")).input;
2807 | const fieldDriver = await createTextBoxDriver(inputElement);
2808 | await fieldDriver.field.fill("Jane");
2809 |
2810 | // First call
2811 | await page.getByTestId("getDataBtn").click();
2812 | let data = await testStateDriver.testState();
2813 | expect(data.name).toEqual("Jane");
2814 |
2815 | // Second call should return same data
2816 | await page.getByTestId("getDataBtn").click();
2817 | data = await testStateDriver.testState();
2818 | expect(data.name).toEqual("Jane");
2819 | });
2820 |
2821 | // =============================================================================
2822 | // IMMUTABILITY TESTS - Proves that modifications to returned data don't affect form
2823 | // =============================================================================
2824 |
2825 | test("modifying returned data property does not affect form data - simple property", async ({
2826 | initTestBed,
2827 | page,
2828 | }) => {
2829 | const { testStateDriver } = await initTestBed(`
2830 | <Fragment>
2831 | <Form id="myForm">
2832 | <FormItem label="Name" bindTo="name" initialValue="John" />
2833 | </Form>
2834 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); data.name = 'Hacker'; testState = myForm.getData()" />
2835 | </Fragment>
2836 | `);
2837 |
2838 | await page.getByTestId("getAndModifyBtn").click();
2839 | const data = await testStateDriver.testState();
2840 |
2841 | expect(data.name).toEqual("John");
2842 | });
2843 |
2844 | test("modifying returned data property does not affect form data - adding new property", async ({
2845 | initTestBed,
2846 | page,
2847 | }) => {
2848 | const { testStateDriver } = await initTestBed(`
2849 | <Fragment>
2850 | <Form id="myForm">
2851 | <FormItem label="Name" bindTo="name" initialValue="John" />
2852 | </Form>
2853 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); data.injected = 'malicious'; testState = myForm.getData()" />
2854 | </Fragment>
2855 | `);
2856 |
2857 | await page.getByTestId("getAndModifyBtn").click();
2858 | const data = await testStateDriver.testState();
2859 |
2860 | expect(data.name).toEqual("John");
2861 | expect(data.injected).toBeUndefined();
2862 | });
2863 |
2864 | test("modifying returned data property does not affect form data - nested object", async ({
2865 | initTestBed,
2866 | page,
2867 | }) => {
2868 | const { testStateDriver } = await initTestBed(`
2869 | <Fragment>
2870 | <Form id="myForm" data="{{address: {street: '123 Main St', city: 'Springfield'}}}">
2871 | <FormItem label="Street" bindTo="address.street" />
2872 | <FormItem label="City" bindTo="address.city" />
2873 | </Form>
2874 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); data.address.street = 'Hacked'; testState = myForm.getData()" />
2875 | </Fragment>
2876 | `);
2877 |
2878 | await page.getByTestId("getAndModifyBtn").click();
2879 | const data = await testStateDriver.testState();
2880 |
2881 | expect(data.address.street).toEqual("123 Main St");
2882 | expect(data.address.city).toEqual("Springfield");
2883 | });
2884 |
2885 | test("modifying returned data property does not affect form data - setting nested property to null", async ({
2886 | initTestBed,
2887 | page,
2888 | }) => {
2889 | const { testStateDriver } = await initTestBed(`
2890 | <Fragment>
2891 | <Form id="myForm" data="{{address: {street: '123 Main St', city: 'Springfield'}}}">
2892 | <FormItem label="Street" bindTo="address.street" />
2893 | <FormItem label="City" bindTo="address.city" />
2894 | </Form>
2895 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); data.address = null; testState = myForm.getData()" />
2896 | </Fragment>
2897 | `);
2898 |
2899 | await page.getByTestId("getAndModifyBtn").click();
2900 | const data = await testStateDriver.testState();
2901 |
2902 | expect(data.address).not.toBeNull();
2903 | expect(data.address.street).toEqual("123 Main St");
2904 | });
2905 |
2906 | test("modifying returned data by deleting property does not affect form data", async ({
2907 | initTestBed,
2908 | page,
2909 | }) => {
2910 | const { testStateDriver } = await initTestBed(`
2911 | <Fragment>
2912 | <Form id="myForm">
2913 | <FormItem label="Name" bindTo="name" initialValue="John" />
2914 | <FormItem label="Email" bindTo="email" initialValue="[email protected]" />
2915 | </Form>
2916 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); delete data.name; testState = myForm.getData()" />
2917 | </Fragment>
2918 | `);
2919 |
2920 | await page.getByTestId("getAndModifyBtn").click();
2921 | const data = await testStateDriver.testState();
2922 |
2923 | expect(data.name).toEqual("John");
2924 | expect(data.email).toEqual("[email protected]");
2925 | });
2926 |
2927 | test("modifying returned data array property does not affect form data", async ({
2928 | initTestBed,
2929 | page,
2930 | }) => {
2931 | const { testStateDriver } = await initTestBed(`
2932 | <Fragment>
2933 | <Form id="myForm" data="{{tags: ['tag1', 'tag2']}}">
2934 | <FormItem label="Tags" bindTo="tags" />
2935 | </Form>
2936 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); data.tags.push('hacked'); testState = myForm.getData()" />
2937 | </Fragment>
2938 | `);
2939 |
2940 | await page.getByTestId("getAndModifyBtn").click();
2941 | const data = await testStateDriver.testState();
2942 |
2943 | expect(data.tags).toEqual(["tag1", "tag2"]);
2944 | expect(data.tags.length).toEqual(2);
2945 | });
2946 |
2947 | test("modifying returned data by assigning new object does not affect form data", async ({
2948 | initTestBed,
2949 | page,
2950 | }) => {
2951 | const { testStateDriver } = await initTestBed(`
2952 | <Fragment>
2953 | <Form id="myForm">
2954 | <FormItem label="Name" bindTo="name" initialValue="John" />
2955 | <FormItem label="Email" bindTo="email" initialValue="[email protected]" />
2956 | </Form>
2957 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); data.name = 'Hacker'; data.email = '[email protected]'; testState = myForm.getData()" />
2958 | </Fragment>
2959 | `);
2960 |
2961 | await page.getByTestId("getAndModifyBtn").click();
2962 | const data = await testStateDriver.testState();
2963 |
2964 | expect(data.name).toEqual("John");
2965 | expect(data.email).toEqual("[email protected]");
2966 | });
2967 |
2968 | test("each call to getData returns independent copy - modifying first does not affect second", async ({
2969 | initTestBed,
2970 | page,
2971 | }) => {
2972 | const { testStateDriver } = await initTestBed(`
2973 | <Fragment>
2974 | <Form id="myForm">
2975 | <FormItem label="Name" bindTo="name" initialValue="John" />
2976 | </Form>
2977 | <Button testId="testBtn" onClick="const first = myForm.getData(); first.name = 'Modified'; const second = myForm.getData(); testState = {first: first, second: second, current: myForm.getData()}" />
2978 | </Fragment>
2979 | `);
2980 |
2981 | await page.getByTestId("testBtn").click();
2982 | const result = await testStateDriver.testState();
2983 |
2984 | expect(result.first.name).toEqual("Modified");
2985 | expect(result.second.name).toEqual("John");
2986 | expect(result.current.name).toEqual("John");
2987 | });
2988 |
2989 | test("modifying returned data in multiple ways simultaneously does not affect form", async ({
2990 | initTestBed,
2991 | page,
2992 | }) => {
2993 | const { testStateDriver } = await initTestBed(`
2994 | <Fragment>
2995 | <Form id="myForm" data="{{user: {name: 'John', age: 30}, tags: ['a', 'b']}}">
2996 | <FormItem label="Name" bindTo="user.name" />
2997 | <FormItem label="Age" bindTo="user.age" />
2998 | <FormItem label="Tags" bindTo="tags" />
2999 | </Form>
3000 | <Button testId="getAndModifyBtn" onClick="const data = myForm.getData(); data.user.name = 'Hacked'; data.user.age = 99; data.tags.push('hacked'); data.newField = 'injected'; testState = myForm.getData()" />
3001 | </Fragment>
3002 | `);
3003 |
3004 | await page.getByTestId("getAndModifyBtn").click();
3005 | const data = await testStateDriver.testState();
3006 |
3007 | expect(data.user.name).toEqual("John");
3008 | expect(data.user.age).toEqual(30);
3009 | expect(data.tags).toEqual(["a", "b"]);
3010 | expect(data.newField).toBeUndefined();
3011 | });
3012 |
3013 | test("initialValue works with undefined and null fields", async ({ initTestBed, page }) => {
3014 | const { testStateDriver } = await initTestBed(
3015 | `
3016 | <Fragment>
3017 | <Form id="myForm" data="/formData">
3018 | <FormItem label="First name" bindTo="firstName" initialValue="Lucy" testId="firstNameField" />
3019 | <FormItem label="Last name" bindTo="lastName" initialValue="Rose" testId="lastNameField"/>
3020 | </Form>
3021 | <Button testId="getBtn" onClick="testState = myForm.getData()" />
3022 | </Fragment>
3023 | `,
3024 | {
3025 | apiInterceptor: {
3026 | operations: {
3027 | read: {
3028 | url: "/formData",
3029 | method: "get",
3030 | handler: `() => {
3031 | return {
3032 | firstName: null
3033 | };
3034 | }`,
3035 | },
3036 | },
3037 | },
3038 | },
3039 | );
3040 |
3041 | await expect(page.getByTestId("firstNameField").getByRole("textbox")).toBeEnabled(); //wait for form to load
3042 | await expect(page.getByTestId("firstNameField").getByRole("textbox")).toHaveValue("Lucy");
3043 | await expect(page.getByTestId("lastNameField").getByRole("textbox")).toHaveValue("Rose");
3044 |
3045 | await page.getByTestId("getBtn").click();
3046 | const data = await testStateDriver.testState();
3047 |
3048 | expect(data.firstName).toEqual("Lucy");
3049 | expect(data.lastName).toEqual("Rose");
3050 | });
3051 | });
3052 |
```