This is page 133 of 180. Use http://codebase.md/xmlui-org/xmlui/xmlui/tools/vscode/resources/xmlui-markup-syntax-highlighting.png?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ └── config.json ├── .eslintrc.cjs ├── .github │ ├── build-checklist.png │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows │ ├── deploy-docs-optimized.yml │ ├── deploy-docs.yml │ ├── prepare-versions.yml │ ├── release-packages.yml │ ├── run-all-tests.yml │ └── run-smoke-tests.yml ├── .gitignore ├── .prettierrc.js ├── .vscode │ ├── launch.json │ └── settings.json ├── blog │ ├── .gitignore │ ├── .gitkeep │ ├── CHANGELOG.md │ ├── extensions.ts │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── public │ │ ├── blog │ │ │ ├── images │ │ │ │ ├── blog-page-component.png │ │ │ │ ├── blog-scrabble.png │ │ │ │ ├── integrated-blog-search.png │ │ │ │ └── lorem-ipsum.png │ │ │ ├── lorem-ipsum.md │ │ │ ├── newest-post.md │ │ │ ├── older-post.md │ │ │ └── welcome-to-the-xmlui-blog.md │ │ ├── mockServiceWorker.js │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── files │ │ │ │ └── for-download │ │ │ │ └── xmlui │ │ │ │ └── xmlui-standalone.umd.js │ │ │ ├── github.svg │ │ │ ├── llms.txt │ │ │ ├── logo-dark.svg │ │ │ ├── logo.svg │ │ │ ├── pg-popout.svg │ │ │ └── xmlui-logo.svg │ │ ├── serve.json │ │ └── web.config │ ├── scripts │ │ ├── download-latest-xmlui.js │ │ ├── generate-rss.js │ │ ├── get-releases.js │ │ └── utils.js │ ├── src │ │ ├── components │ │ │ ├── BlogOverview.xmlui │ │ │ ├── BlogPage.xmlui │ │ │ └── PageNotFound.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ ├── docs-theme.ts │ │ ├── earthtone.ts │ │ ├── xmlui-gray-on-default.ts │ │ ├── xmlui-green-on-default.ts │ │ └── xmlui-orange-on-default.ts │ └── 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 │ │ │ ├── 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 │ │ │ │ ├── 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 │ │ │ │ ├── make-a-set-of-equal-width-cards.md │ │ │ │ ├── make-a-table-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-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 │ │ │ ├── github.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 │ │ └── web.config │ ├── 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 │ │ │ ├── NameValue.xmlui │ │ │ ├── PageNotFound.xmlui │ │ │ ├── PaletteItem.xmlui │ │ │ ├── Palettes.xmlui │ │ │ ├── SectionHeader.xmlui │ │ │ ├── TBD.xmlui │ │ │ ├── Test.xmlui │ │ │ ├── ThemesIntro.xmlui │ │ │ ├── ThousandThemes.xmlui │ │ │ ├── TubeStops.xmlui │ │ │ ├── TubeStops.xmlui.xs │ │ │ └── TwoColumnCode.xmlui │ │ ├── config.ts │ │ ├── Main.xmlui │ │ └── themes │ │ ├── docs-theme.ts │ │ ├── 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 │ ├── 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 │ │ └── tsconfig.json │ ├── 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 │ │ ├── tsconfig.json │ │ └── 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 │ │ └── tsconfig.json │ ├── 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 │ │ └── tsconfig.json │ ├── 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 │ │ └── tsconfig.json │ ├── 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.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.module.scss │ │ │ │ ├── Preview.tsx │ │ │ │ ├── Select.module.scss │ │ │ │ ├── StandalonePlayground.tsx │ │ │ │ ├── StandalonePlaygroundNative.module.scss │ │ │ │ ├── StandalonePlaygroundNative.tsx │ │ │ │ ├── ThemeSwitcher.module.scss │ │ │ │ ├── ThemeSwitcher.tsx │ │ │ │ ├── ToneSwitcher.tsx │ │ │ │ ├── Tooltip.module.scss │ │ │ │ ├── Tooltip.tsx │ │ │ │ └── utils.ts │ │ │ ├── providers │ │ │ │ ├── Toast.module.scss │ │ │ │ └── ToastProvider.tsx │ │ │ ├── state │ │ │ │ └── store.ts │ │ │ ├── themes │ │ │ │ └── theme.ts │ │ │ └── utils │ │ │ └── helpers.ts │ │ └── tsconfig.json │ ├── 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 │ │ └── tsconfig.json │ ├── xmlui-spreadsheet │ │ ├── .gitignore │ │ ├── demo │ │ │ └── Main.xmlui │ │ ├── index.html │ │ ├── index.ts │ │ ├── meta │ │ │ └── componentsMetadata.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── index.tsx │ │ │ ├── Spreadsheet.tsx │ │ │ └── SpreadsheetNative.tsx │ │ └── tsconfig.json │ └── 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.tsx │ │ │ └── HeroSectionNative.tsx │ │ ├── index.tsx │ │ ├── ScrollToTop │ │ │ ├── ScrollToTop.module.scss │ │ │ ├── ScrollToTop.tsx │ │ │ └── ScrollToTopNative.tsx │ │ └── vite-env.d.ts │ └── tsconfig.json ├── 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.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 │ ├── build-system.md │ ├── build-xmlui.md │ ├── containers.md │ ├── glossary.md │ ├── 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 │ ├── state-management.md │ └── xmlui-repo.md ├── package.json ├── playwright.config.ts ├── scripts │ ├── coverage-only.js │ ├── e2e-test-summary.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 │ ├── get-langserver-metadata.mjs │ ├── 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.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 │ │ ├── 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.spec.ts │ │ │ │ ├── LabelList.tsx │ │ │ │ ├── LabelListNative.module.scss │ │ │ │ └── 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 │ │ ├── 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 │ │ ├── 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 │ │ │ └── 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 │ │ ├── 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 │ │ │ ├── BehaviorContext.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 │ │ │ ├── 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.mjs │ ├── 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 │ │ │ ├── ModalDialogDriver.ts │ │ │ ├── NumberBoxDriver.ts │ │ │ ├── TextBoxDriver.ts │ │ │ ├── TimeInputDriver.ts │ │ │ ├── TimerDriver.ts │ │ │ └── TreeDriver.ts │ │ ├── fixtures.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 │ │ └── themed-app-test-helpers.ts │ └── 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.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 │ ├── 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.bin.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /xmlui/src/components/Table/Table.spec.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Table Component End-to-End Tests 3 | * 4 | * This test suite provides comprehensive coverage for the Table component following 5 | * XMLUI testing conventions. The tests validate all documented properties, events, 6 | * accessibility features, and edge cases. 7 | * 8 | * Test Results Summary: 9 | * - ✅ 25 tests passing 10 | * - ⏭️ 6 tests skipped (require additional implementation investigation) 11 | * 12 | * Key Testing Insights: 13 | * - Use HTML element selectors (th, td, table) rather than role-based selectors 14 | * - Add .first() to avoid strict mode violations when multiple elements match 15 | * - Some features like selection checkboxes exist but are hidden via CSS 16 | * - Loading states, sorting, and pagination may use different implementations than expected 17 | * 18 | * Skipped Tests (for future investigation): 19 | * 1. Loading property visual indicators 20 | * 2. Row selection interaction (checkboxes are hidden) 21 | * 3. Sorting functionality behavior 22 | * 4. Pagination control identification 23 | * 5. Theme variable CSS property testing 24 | */ 25 | 26 | import { expect, test } from "../../testing/fixtures"; 27 | import { SKIP_REASON } from "../../testing/component-test-helpers"; 28 | 29 | // Sample data for testing 30 | const sampleData = [ 31 | { id: 1, name: "Apple", quantity: 5, category: "Fruit" }, 32 | { id: 2, name: "Banana", quantity: 3, category: "Fruit" }, 33 | { id: 3, name: "Carrot", quantity: 10, category: "Vegetable" }, 34 | { id: 4, name: "Spinach", quantity: 2, category: "Vegetable" }, 35 | ]; 36 | 37 | // ============================================================================= 38 | // BASIC FUNCTIONALITY TESTS 39 | // ============================================================================= 40 | 41 | test.describe("Basic Functionality", () => { 42 | test("renders with basic data and columns", async ({ initTestBed, page }) => { 43 | await initTestBed(` 44 | <Table data='{${JSON.stringify(sampleData)}}' testId="table"> 45 | <Column bindTo="name" header="Name"/> 46 | <Column bindTo="quantity" header="Quantity"/> 47 | <Column bindTo="category" header="Category"/> 48 | </Table> 49 | `); 50 | 51 | const table = page.getByTestId("table"); 52 | await expect(table).toBeVisible(); 53 | 54 | // Check for actual HTML table elements 55 | const htmlTable = page.locator("table"); 56 | await expect(htmlTable).toBeVisible(); 57 | 58 | // Check headers are present 59 | const headers = page.locator("th"); 60 | await expect(headers).toHaveCount(3); // Should have 3 headers 61 | 62 | // Check header text content 63 | await expect(headers.nth(0)).toContainText("Name"); 64 | await expect(headers.nth(1)).toContainText("Quantity"); 65 | await expect(headers.nth(2)).toContainText("Category"); 66 | 67 | // Check data content - use first() to avoid strict mode violations 68 | await expect(page.locator("td").filter({ hasText: "Apple" }).first()).toBeVisible(); 69 | await expect(page.locator("td").filter({ hasText: "5" }).first()).toBeVisible(); 70 | await expect(page.locator("td").filter({ hasText: "Fruit" }).first()).toBeVisible(); 71 | }); 72 | 73 | test("renders with empty data array", async ({ initTestBed, page }) => { 74 | await initTestBed(` 75 | <Table data='{[]}' testId="table"> 76 | <Column bindTo="name" header="Name"/> 77 | <Column bindTo="quantity" header="Quantity"/> 78 | </Table> 79 | `); 80 | 81 | const table = page.getByTestId("table"); 82 | await expect(table).toBeVisible(); 83 | 84 | // Headers should still be visible 85 | const headers = page.locator("th"); 86 | await expect(headers.nth(0)).toContainText("Name"); 87 | await expect(headers.nth(1)).toContainText("Quantity"); 88 | }); 89 | 90 | test.describe("data property", () => { 91 | test("displays data with different value types", async ({ initTestBed, page }) => { 92 | const mixedData = [ 93 | { id: 1, name: "Test", number: 42, boolean: true, nullValue: null }, 94 | { id: 2, name: "Another", number: 0, boolean: false, nullValue: undefined }, 95 | ]; 96 | 97 | await initTestBed(` 98 | <Table data='{${JSON.stringify(mixedData)}}' testId="table"> 99 | <Column bindTo="name"/> 100 | <Column bindTo="number"/> 101 | <Column bindTo="boolean"/> 102 | <Column bindTo="nullValue"/> 103 | </Table> 104 | `); 105 | 106 | await expect(page.locator("td").filter({ hasText: "Test" }).first()).toBeVisible(); 107 | await expect(page.locator("td").filter({ hasText: "42" }).first()).toBeVisible(); 108 | await expect(page.locator("td").filter({ hasText: "true" }).first()).toBeVisible(); 109 | await expect(page.locator("td").filter({ hasText: "0" }).first()).toBeVisible(); 110 | await expect(page.locator("td").filter({ hasText: "false" }).first()).toBeVisible(); 111 | }); 112 | 113 | test("handles null data gracefully", async ({ initTestBed, page }) => { 114 | await initTestBed(` 115 | <Table data='{null}' testId="table"> 116 | <Column bindTo="name"/> 117 | </Table> 118 | `); 119 | 120 | const table = page.getByTestId("table"); 121 | await expect(table).toBeVisible(); 122 | }); 123 | 124 | test("handles undefined data gracefully", async ({ initTestBed, page }) => { 125 | await initTestBed(` 126 | <Table data='{undefined}' testId="table"> 127 | <Column bindTo="name"/> 128 | </Table> 129 | `); 130 | 131 | const table = page.getByTestId("table"); 132 | await expect(table).toBeVisible(); 133 | }); 134 | }); 135 | 136 | test.describe("items property", () => { 137 | test("items property works as alias for data", async ({ initTestBed, page }) => { 138 | await initTestBed(` 139 | <Table items='{${JSON.stringify(sampleData)}}' testId="table"> 140 | <Column bindTo="name"/> 141 | <Column bindTo="quantity"/> 142 | </Table> 143 | `); 144 | 145 | await expect(page.locator("td").filter({ hasText: "Apple" }).first()).toBeVisible(); 146 | await expect(page.locator("td").filter({ hasText: "5" }).first()).toBeVisible(); 147 | }); 148 | 149 | test("items takes priority over data when both are provided", async ({ initTestBed, page }) => { 150 | const itemsData = [{ id: 1, name: "Items Data" }]; 151 | const dataProperty = [{ id: 1, name: "Data Property" }]; 152 | 153 | await initTestBed(` 154 | <Table 155 | items='{${JSON.stringify(itemsData)}}' 156 | data='{${JSON.stringify(dataProperty)}}' 157 | testId="table" 158 | > 159 | <Column bindTo="name"/> 160 | </Table> 161 | `); 162 | 163 | await expect(page.locator("td").filter({ hasText: "Items Data" }).first()).toBeVisible(); 164 | await expect(page.locator("td").filter({ hasText: "Data Property" })).toHaveCount(0); 165 | }); 166 | }); 167 | 168 | test.describe("hideHeader property", () => { 169 | test("hides header when hideHeader is true", async ({ initTestBed, page }) => { 170 | await initTestBed(` 171 | <Table data='{${JSON.stringify(sampleData)}}' hideHeader="true" testId="table"> 172 | <Column bindTo="name" header="Name"/> 173 | <Column bindTo="quantity" header="Quantity"/> 174 | </Table> 175 | `); 176 | 177 | // Header should not be visible 178 | await expect(page.locator("th")).toHaveCount(0); 179 | 180 | // Data should still be visible 181 | await expect(page.locator("td").filter({ hasText: "Apple" }).first()).toBeVisible(); 182 | }); 183 | 184 | test("shows header when hideHeader is false", async ({ initTestBed, page }) => { 185 | await initTestBed(` 186 | <Table data='{${JSON.stringify(sampleData)}}' hideHeader="false" testId="table"> 187 | <Column bindTo="name" header="Name"/> 188 | <Column bindTo="quantity" header="Quantity"/> 189 | </Table> 190 | `); 191 | 192 | const headers = page.locator("th"); 193 | await expect(headers.nth(0)).toContainText("Name"); 194 | await expect(headers.nth(1)).toContainText("Quantity"); 195 | }); 196 | }); 197 | 198 | test.describe("noBottomBorder property", () => { 199 | test("removes bottom border when noBottomBorder is true", async ({ initTestBed, page }) => { 200 | await initTestBed(` 201 | <Table data='{${JSON.stringify(sampleData)}}' noBottomBorder="true" testId="table"> 202 | <Column bindTo="name"/> 203 | </Table> 204 | `); 205 | 206 | const table = page.getByTestId("table"); 207 | await expect(table).toBeVisible(); 208 | // Note: Visual border testing would require specific CSS assertions 209 | }); 210 | }); 211 | 212 | test.describe("rowsSelectable property", () => { 213 | test("enables row selection when rowsSelectable is true", async ({ initTestBed, page }) => { 214 | await initTestBed(` 215 | <Table data='{${JSON.stringify(sampleData)}}' rowsSelectable="true" testId="table"> 216 | <Column bindTo="name"/> 217 | </Table> 218 | `); 219 | 220 | // Selection checkboxes should be present - they exist but might be hidden via CSS 221 | const checkboxes = page.locator("input[type='checkbox']"); 222 | await expect(checkboxes).toHaveCount(5); // 4 data rows + 1 header checkbox 223 | }); 224 | 225 | test("disables row selection when rowsSelectable is false", async ({ initTestBed, page }) => { 226 | await initTestBed(` 227 | <Table data='{${JSON.stringify(sampleData)}}' rowsSelectable="false" testId="table"> 228 | <Column bindTo="name"/> 229 | </Table> 230 | `); 231 | 232 | // No selection checkboxes should be present 233 | const checkboxes = page.locator("input[type='checkbox']"); 234 | await expect(checkboxes).toHaveCount(0); 235 | }); 236 | }); 237 | 238 | test.describe("autoFocus property", () => { 239 | test("focuses table when autoFocus is true", async ({ initTestBed, page }) => { 240 | await initTestBed(` 241 | <Table data='{${JSON.stringify(sampleData)}}' autoFocus="true" testId="table"> 242 | <Column bindTo="name"/> 243 | </Table> 244 | `); 245 | 246 | const table = page.getByTestId("table"); 247 | await expect(table).toBeFocused(); 248 | }); 249 | }); 250 | 251 | test.describe("checkboxTolerance property", () => { 252 | test("allows checkbox interaction within compact tolerance boundary", async ({ initTestBed, page }) => { 253 | const { testStateDriver } = await initTestBed(` 254 | <Table data='{${JSON.stringify(sampleData)}}' 255 | rowsSelectable="true" 256 | checkboxTolerance="compact" 257 | onSelectionDidChange="testState = 'selection changed'" 258 | testId="table"> 259 | <Column bindTo="name"/> 260 | <Column bindTo="quantity"/> 261 | </Table> 262 | `); 263 | 264 | // Get first row checkbox - checkboxes exist but are hidden via CSS 265 | const firstRowCheckbox = page.locator("input[type='checkbox']").nth(1); // Skip header checkbox 266 | 267 | // Verify checkbox exists (even if hidden) 268 | await expect(firstRowCheckbox).toBeAttached(); 269 | 270 | // Get the first table row to interact with 271 | const firstDataRow = page.locator("tbody tr").first(); 272 | await expect(firstDataRow).toBeVisible(); 273 | 274 | // Get row bounds for calculation 275 | const rowBounds = await firstDataRow.boundingBox(); 276 | 277 | // Click near the left edge of the row (where checkbox would be with tolerance) 278 | // This simulates clicking within the 8px compact tolerance of the checkbox 279 | const clickX = rowBounds.x + 15; // Slightly to the right of where checkbox would be 280 | const clickY = rowBounds.y + rowBounds.height / 2; 281 | 282 | // Click within tolerance boundary should trigger selection due to checkboxTolerance="compact" 283 | await page.mouse.click(clickX, clickY); 284 | 285 | // Verify checkbox is now checked (using force since it's hidden) 286 | await expect(firstRowCheckbox).toBeChecked(); 287 | 288 | // Verify selection event was fired 289 | await expect.poll(testStateDriver.testState).toEqual('selection changed'); 290 | }); 291 | 292 | test("allows header checkbox interaction within compact tolerance boundary", async ({ initTestBed, page }) => { 293 | const { testStateDriver } = await initTestBed(` 294 | <Table data='{${JSON.stringify(sampleData)}}' 295 | rowsSelectable="true" 296 | enableMultiRowSelection="true" 297 | checkboxTolerance="compact" 298 | onSelectionDidChange="testState = 'header selection changed'" 299 | testId="table"> 300 | <Column bindTo="name"/> 301 | <Column bindTo="quantity"/> 302 | </Table> 303 | `); 304 | 305 | // Get header checkbox (select all checkbox) 306 | const headerCheckbox = page.locator("input[type='checkbox']").first(); // Header checkbox is first 307 | 308 | // Verify checkbox exists (even if hidden) 309 | await expect(headerCheckbox).toBeAttached(); 310 | 311 | // Get the header row to interact with 312 | const headerRow = page.locator("thead tr").first(); 313 | await expect(headerRow).toBeVisible(); 314 | 315 | // Get header row bounds for calculation 316 | const headerBounds = await headerRow.boundingBox(); 317 | 318 | // Click near the left edge of the header row (where checkbox would be with tolerance) 319 | // This simulates clicking within the 8px compact tolerance of the header checkbox 320 | const clickX = headerBounds.x + 15; // Slightly to the right of where checkbox would be 321 | const clickY = headerBounds.y + headerBounds.height / 2; 322 | 323 | // Click within tolerance boundary should trigger "select all" due to checkboxTolerance="compact" 324 | await page.mouse.click(clickX, clickY); 325 | 326 | // Verify header checkbox is now checked (select all) 327 | await expect(headerCheckbox).toBeChecked(); 328 | 329 | // Verify all row checkboxes are also checked (select all behavior) 330 | const allCheckboxes = page.locator("input[type='checkbox']"); 331 | const checkboxCount = await allCheckboxes.count(); 332 | for (let i = 0; i < checkboxCount; i++) { 333 | await expect(allCheckboxes.nth(i)).toBeChecked(); 334 | } 335 | 336 | // Verify selection event was fired 337 | await expect.poll(testStateDriver.testState).toEqual('header selection changed'); 338 | }); 339 | 340 | test("allows checkbox interaction within none tolerance boundary", async ({ initTestBed, page }) => { 341 | const { testStateDriver } = await initTestBed(` 342 | <Table data='{${JSON.stringify(sampleData)}}' 343 | rowsSelectable="true" 344 | checkboxTolerance="none" 345 | onSelectionDidChange="testState = 'none selection changed'" 346 | testId="table"> 347 | <Column bindTo="name"/> 348 | <Column bindTo="quantity"/> 349 | </Table> 350 | `); 351 | 352 | // Get first row checkbox - checkboxes exist but are hidden via CSS 353 | const firstRowCheckbox = page.locator("input[type='checkbox']").nth(1); // Skip header checkbox 354 | 355 | // Verify checkbox exists (even if hidden) 356 | await expect(firstRowCheckbox).toBeAttached(); 357 | 358 | // Get the first table row to interact with 359 | const firstDataRow = page.locator("tbody tr").first(); 360 | await expect(firstDataRow).toBeVisible(); 361 | 362 | // Get row bounds for calculation 363 | const rowBounds = await firstDataRow.boundingBox(); 364 | 365 | // For "none" tolerance, we need to click precisely on the checkbox 366 | // Since checkboxes are hidden, click on their expected position 367 | await firstRowCheckbox.click({ force: true }); 368 | 369 | // Verify checkbox is now checked 370 | await expect(firstRowCheckbox).toBeChecked(); 371 | 372 | // Verify selection event was fired 373 | await expect.poll(testStateDriver.testState).toEqual('none selection changed'); 374 | }); 375 | 376 | test("allows header checkbox interaction within none tolerance boundary", async ({ initTestBed, page }) => { 377 | const { testStateDriver } = await initTestBed(` 378 | <Table data='{${JSON.stringify(sampleData)}}' 379 | rowsSelectable="true" 380 | enableMultiRowSelection="true" 381 | checkboxTolerance="none" 382 | onSelectionDidChange="testState = 'header none selection changed'" 383 | testId="table"> 384 | <Column bindTo="name"/> 385 | <Column bindTo="quantity"/> 386 | </Table> 387 | `); 388 | 389 | // Get header checkbox (select all checkbox) 390 | const headerCheckbox = page.locator("input[type='checkbox']").first(); // Header checkbox is first 391 | 392 | // Verify checkbox exists (even if hidden) 393 | await expect(headerCheckbox).toBeAttached(); 394 | 395 | // Get the header row to interact with 396 | const headerRow = page.locator("thead tr").first(); 397 | await expect(headerRow).toBeVisible(); 398 | 399 | // Get header row bounds for calculation 400 | const headerBounds = await headerRow.boundingBox(); 401 | 402 | // For "none" tolerance, we need to click precisely on the checkbox 403 | // Since checkboxes are hidden, click on their expected position 404 | await headerCheckbox.click({ force: true }); 405 | 406 | // Verify header checkbox is now checked (select all) 407 | await expect(headerCheckbox).toBeChecked(); 408 | 409 | // Verify all row checkboxes are also checked (select all behavior) 410 | const allCheckboxes = page.locator("input[type='checkbox']"); 411 | const checkboxCount = await allCheckboxes.count(); 412 | for (let i = 0; i < checkboxCount; i++) { 413 | await expect(allCheckboxes.nth(i)).toBeChecked(); 414 | } 415 | 416 | // Verify selection event was fired 417 | await expect.poll(testStateDriver.testState).toEqual('header none selection changed'); 418 | }); 419 | 420 | test("allows checkbox interaction within comfortable tolerance boundary", async ({ initTestBed, page }) => { 421 | const { testStateDriver } = await initTestBed(` 422 | <Table data='{${JSON.stringify(sampleData)}}' 423 | rowsSelectable="true" 424 | checkboxTolerance="comfortable" 425 | onSelectionDidChange="testState = 'comfortable selection changed'" 426 | testId="table"> 427 | <Column bindTo="name"/> 428 | <Column bindTo="quantity"/> 429 | </Table> 430 | `); 431 | 432 | // Get first row checkbox - checkboxes exist but are hidden via CSS 433 | const firstRowCheckbox = page.locator("input[type='checkbox']").nth(1); // Skip header checkbox 434 | 435 | // Verify checkbox exists (even if hidden) 436 | await expect(firstRowCheckbox).toBeAttached(); 437 | 438 | // Get the first table row to interact with 439 | const firstDataRow = page.locator("tbody tr").first(); 440 | await expect(firstDataRow).toBeVisible(); 441 | 442 | // Get row bounds for calculation 443 | const rowBounds = await firstDataRow.boundingBox(); 444 | 445 | // Click near the left edge of the row (where checkbox would be with tolerance) 446 | // This simulates clicking within the 12px comfortable tolerance of the checkbox 447 | const clickX = rowBounds.x + 20; // Further right to test 12px tolerance 448 | const clickY = rowBounds.y + rowBounds.height / 2; 449 | 450 | // Click within tolerance boundary should trigger selection due to checkboxTolerance="comfortable" 451 | await page.mouse.click(clickX, clickY); 452 | 453 | // Verify checkbox is now checked 454 | await expect(firstRowCheckbox).toBeChecked(); 455 | 456 | // Verify selection event was fired 457 | await expect.poll(testStateDriver.testState).toEqual('comfortable selection changed'); 458 | }); 459 | 460 | test("allows header checkbox interaction within comfortable tolerance boundary", async ({ initTestBed, page }) => { 461 | const { testStateDriver } = await initTestBed(` 462 | <Table data='{${JSON.stringify(sampleData)}}' 463 | rowsSelectable="true" 464 | enableMultiRowSelection="true" 465 | checkboxTolerance="comfortable" 466 | onSelectionDidChange="testState = 'header comfortable selection changed'" 467 | testId="table"> 468 | <Column bindTo="name"/> 469 | <Column bindTo="quantity"/> 470 | </Table> 471 | `); 472 | 473 | // Get header checkbox (select all checkbox) 474 | const headerCheckbox = page.locator("input[type='checkbox']").first(); // Header checkbox is first 475 | 476 | // Verify checkbox exists (even if hidden) 477 | await expect(headerCheckbox).toBeAttached(); 478 | 479 | // Get the header row to interact with 480 | const headerRow = page.locator("thead tr").first(); 481 | await expect(headerRow).toBeVisible(); 482 | 483 | // Get header row bounds for calculation 484 | const headerBounds = await headerRow.boundingBox(); 485 | 486 | // Click near the left edge of the header row (where checkbox would be with tolerance) 487 | // This simulates clicking within the 12px comfortable tolerance of the header checkbox 488 | const clickX = headerBounds.x + 20; // Further right to test 12px tolerance 489 | const clickY = headerBounds.y + headerBounds.height / 2; 490 | 491 | // Click within tolerance boundary should trigger "select all" due to checkboxTolerance="comfortable" 492 | await page.mouse.click(clickX, clickY); 493 | 494 | // Verify header checkbox is now checked (select all) 495 | await expect(headerCheckbox).toBeChecked(); 496 | 497 | // Verify all row checkboxes are also checked (select all behavior) 498 | const allCheckboxes = page.locator("input[type='checkbox']"); 499 | const checkboxCount = await allCheckboxes.count(); 500 | for (let i = 0; i < checkboxCount; i++) { 501 | await expect(allCheckboxes.nth(i)).toBeChecked(); 502 | } 503 | 504 | // Verify selection event was fired 505 | await expect.poll(testStateDriver.testState).toEqual('header comfortable selection changed'); 506 | }); 507 | 508 | test("allows checkbox interaction within spacious tolerance boundary", async ({ initTestBed, page }) => { 509 | const { testStateDriver } = await initTestBed(` 510 | <Table data='{${JSON.stringify(sampleData)}}' 511 | rowsSelectable="true" 512 | checkboxTolerance="spacious" 513 | onSelectionDidChange="testState = 'spacious selection changed'" 514 | testId="table"> 515 | <Column bindTo="name"/> 516 | <Column bindTo="quantity"/> 517 | </Table> 518 | `); 519 | 520 | // Get first row checkbox - checkboxes exist but are hidden via CSS 521 | const firstRowCheckbox = page.locator("input[type='checkbox']").nth(1); // Skip header checkbox 522 | 523 | // Verify checkbox exists (even if hidden) 524 | await expect(firstRowCheckbox).toBeAttached(); 525 | 526 | // Get the first table row to interact with 527 | const firstDataRow = page.locator("tbody tr").first(); 528 | await expect(firstDataRow).toBeVisible(); 529 | 530 | // Get row bounds for calculation 531 | const rowBounds = await firstDataRow.boundingBox(); 532 | 533 | // Click near the left edge of the row (where checkbox would be with tolerance) 534 | // This simulates clicking within the 16px spacious tolerance of the checkbox 535 | const clickX = rowBounds.x + 24; // Even further right to test 16px tolerance 536 | const clickY = rowBounds.y + rowBounds.height / 2; 537 | 538 | // Click within tolerance boundary should trigger selection due to checkboxTolerance="spacious" 539 | await page.mouse.click(clickX, clickY); 540 | 541 | // Verify checkbox is now checked 542 | await expect(firstRowCheckbox).toBeChecked(); 543 | 544 | // Verify selection event was fired 545 | await expect.poll(testStateDriver.testState).toEqual('spacious selection changed'); 546 | }); 547 | 548 | test("allows header checkbox interaction within spacious tolerance boundary", async ({ initTestBed, page }) => { 549 | const { testStateDriver } = await initTestBed(` 550 | <Table data='{${JSON.stringify(sampleData)}}' 551 | rowsSelectable="true" 552 | enableMultiRowSelection="true" 553 | checkboxTolerance="spacious" 554 | onSelectionDidChange="testState = 'header spacious selection changed'" 555 | testId="table"> 556 | <Column bindTo="name"/> 557 | <Column bindTo="quantity"/> 558 | </Table> 559 | `); 560 | 561 | // Get header checkbox (select all checkbox) 562 | const headerCheckbox = page.locator("input[type='checkbox']").first(); // Header checkbox is first 563 | 564 | // Verify checkbox exists (even if hidden) 565 | await expect(headerCheckbox).toBeAttached(); 566 | 567 | // Get the header row to interact with 568 | const headerRow = page.locator("thead tr").first(); 569 | await expect(headerRow).toBeVisible(); 570 | 571 | // Get header row bounds for calculation 572 | const headerBounds = await headerRow.boundingBox(); 573 | 574 | // Click near the left edge of the header row (where checkbox would be with tolerance) 575 | // This simulates clicking within the 16px spacious tolerance of the header checkbox 576 | const clickX = headerBounds.x + 24; // Even further right to test 16px tolerance 577 | const clickY = headerBounds.y + headerBounds.height / 2; 578 | 579 | // Click within tolerance boundary should trigger "select all" due to checkboxTolerance="spacious" 580 | await page.mouse.click(clickX, clickY); 581 | 582 | // Verify header checkbox is now checked (select all) 583 | await expect(headerCheckbox).toBeChecked(); 584 | 585 | // Verify all row checkboxes are also checked (select all behavior) 586 | const allCheckboxes = page.locator("input[type='checkbox']"); 587 | const checkboxCount = await allCheckboxes.count(); 588 | for (let i = 0; i < checkboxCount; i++) { 589 | await expect(allCheckboxes.nth(i)).toBeChecked(); 590 | } 591 | 592 | // Verify selection event was fired 593 | await expect.poll(testStateDriver.testState).toEqual('header spacious selection changed'); 594 | }); 595 | }); 596 | 597 | test.describe("noDataTemplate property", () => { 598 | test("shows custom no data template when data is empty", async ({ initTestBed, page }) => { 599 | await initTestBed(` 600 | <Table data='{[]}' testId="table"> 601 | <Column bindTo="name"/> 602 | <property name="noDataTemplate"> 603 | <Text>No items found</Text> 604 | </property> 605 | </Table> 606 | `); 607 | 608 | await expect(page.getByText("No items found")).toBeVisible(); 609 | }); 610 | 611 | test("hides no data view when noDataTemplate is empty string", async ({ initTestBed, page }) => { 612 | await initTestBed(` 613 | <Table data='{[]}' noDataTemplate="" testId="table"> 614 | <Column bindTo="name"/> 615 | </Table> 616 | `); 617 | 618 | // Should not show default no data message 619 | await expect(page.getByText(/no data/i)).not.toBeVisible(); 620 | }); 621 | 622 | test("hides no data view when noDataTemplate is null", async ({ initTestBed, page }) => { 623 | await initTestBed(` 624 | <Table data='{[]}' noDataTemplate="{null}" testId="table"> 625 | <Column bindTo="name"/> 626 | </Table> 627 | `); 628 | 629 | // Should not show default no data message 630 | await expect(page.getByText(/no data/i)).not.toBeVisible(); 631 | }); 632 | }); 633 | }); 634 | 635 | // ============================================================================= 636 | // TESTS FOR FEATURES THAT NEED INVESTIGATION 637 | // ============================================================================= 638 | 639 | test.describe("Features Needing Investigation", () => { 640 | test("loading property shows loading state", 641 | async ({ initTestBed, page }) => { 642 | await initTestBed(` 643 | <Table loading="true" testId="table"> 644 | <Column bindTo="name"/> 645 | </Table> 646 | `); 647 | await expect(page.getByRole("status").and(page.getByLabel("Loading"))).toBeVisible(); 648 | } 649 | ); 650 | 651 | test("row selection works with checkboxes", 652 | async ({ initTestBed, page }) => { 653 | await initTestBed(` 654 | <Table 655 | data='{${JSON.stringify(sampleData)}}' 656 | rowsSelectable="true" 657 | enableMultiRowSelection="true" 658 | testId="table" 659 | > 660 | <Column bindTo="name"/> 661 | </Table> 662 | `); 663 | 664 | const checkboxes = page.locator("input[type='checkbox']"); 665 | await checkboxes.nth(1).check({ force: true }); // First data row 666 | await checkboxes.nth(2).check({ force: true }); // Second data row 667 | 668 | await expect(checkboxes.nth(1)).toBeChecked(); 669 | await expect(checkboxes.nth(2)).toBeChecked(); 670 | } 671 | ); 672 | 673 | test("sorting works correctly with descending order", 674 | async ({ initTestBed, page }) => { 675 | await initTestBed(` 676 | <Table 677 | data='{${JSON.stringify(sampleData)}}' 678 | sortBy="name" 679 | sortDirection="descending" 680 | testId="table" 681 | > 682 | <Column bindTo="name" canSort="true"/> 683 | </Table> 684 | `); 685 | 686 | const cells = page.locator("td"); 687 | // Should be sorted reverse alphabetically: Spinach, Carrot, Banana, Apple 688 | await expect(cells.nth(0)).toHaveText("Spinach"); 689 | await expect(cells.nth(1)).toHaveText("Carrot"); 690 | } 691 | ); 692 | 693 | test("sorting works correctly with ascending order", 694 | async ({ initTestBed, page }) => { 695 | await initTestBed(` 696 | <Table 697 | data='{${JSON.stringify(sampleData)}}' 698 | sortBy="quantity" 699 | sortDirection="ascending" 700 | testId="table" 701 | > 702 | <Column bindTo="quantity" canSort="true"/> 703 | </Table> 704 | `); 705 | 706 | const cells = page.locator("td"); 707 | // Should be sorted in ascending order: 2, 3, 5, 10 708 | await expect(cells.nth(0)).toHaveText("2"); 709 | await expect(cells.nth(1)).toHaveText("3"); 710 | await expect(cells.nth(2)).toHaveText("5"); 711 | await expect(cells.nth(3)).toHaveText("10"); 712 | } 713 | ); 714 | 715 | test("pagination works correctly", 716 | async ({ initTestBed, page }) => { 717 | await initTestBed(` 718 | <Table 719 | data='{${JSON.stringify(sampleData)}}' 720 | isPaginated="true" 721 | testId="table" 722 | > 723 | <Column bindTo="name"/> 724 | </Table> 725 | `); 726 | await expect(page.locator("button[aria-label*='Previous page']")).toBeVisible(); 727 | await expect(page.locator("button[aria-label*='Next page']")).toBeVisible(); 728 | } 729 | ); 730 | }); 731 | 732 | // ============================================================================= 733 | // ACCESSIBILITY TESTS 734 | // ============================================================================= 735 | 736 | test.describe("Accessibility", () => { 737 | test("has correct table structure", async ({ initTestBed, page }) => { 738 | await initTestBed(` 739 | <Table data='{${JSON.stringify(sampleData)}}' testId="table"> 740 | <Column bindTo="name" header="Name"/> 741 | <Column bindTo="quantity" header="Quantity"/> 742 | </Table> 743 | `); 744 | 745 | // Check proper table semantics 746 | await expect(page.locator("table")).toBeVisible(); 747 | await expect(page.locator("th")).toHaveCount(2); // 2 headers 748 | await expect(page.locator("tr")).toHaveCount(5); // 1 header + 4 data rows 749 | }); 750 | 751 | test("column headers are focusable and have proper structure", async ({ 752 | initTestBed, 753 | page 754 | }) => { 755 | await initTestBed(` 756 | <Table data='{${JSON.stringify(sampleData)}}' testId="table"> 757 | <Column bindTo="name" header="Name" canSort="true"/> 758 | <Column bindTo="quantity" header="Quantity" canSort="true"/> 759 | </Table> 760 | `); 761 | 762 | const headers = page.locator("th"); 763 | await expect(headers.nth(0)).toContainText("Name"); 764 | await expect(headers.nth(1)).toContainText("Quantity"); 765 | }); 766 | 767 | test("selection checkboxes have proper accessibility when enabled", async ({ initTestBed, page }) => { 768 | await initTestBed(` 769 | <Table data='{${JSON.stringify(sampleData)}}' rowsSelectable="true" testId="table"> 770 | <Column bindTo="name"/> 771 | </Table> 772 | `); 773 | 774 | const checkboxes = page.locator("input[type='checkbox']"); 775 | 776 | // All checkboxes should have proper type 777 | await expect(checkboxes.first()).toHaveAttribute("type", "checkbox"); 778 | 779 | // Should have expected count 780 | await expect(checkboxes).toHaveCount(5); // 4 data rows + 1 header 781 | }); 782 | 783 | test("has proper headers for screen readers", async ({ initTestBed, page }) => { 784 | await initTestBed(` 785 | <Table data='{${JSON.stringify(sampleData)}}' testId="table"> 786 | <Column bindTo="name" header="Product Name"/> 787 | <Column bindTo="quantity" header="Stock Quantity"/> 788 | </Table> 789 | `); 790 | 791 | // Column headers should have descriptive names 792 | await expect(page.locator("th").filter({ hasText: "Product Name" })).toBeVisible(); 793 | await expect(page.locator("th").filter({ hasText: "Stock Quantity" })).toBeVisible(); 794 | }); 795 | }); 796 | 797 | // ============================================================================= 798 | // EDGE CASE TESTS 799 | // ============================================================================= 800 | 801 | test.describe("Edge Cases", () => { 802 | test("handles no props gracefully", async ({ initTestBed, page }) => { 803 | await initTestBed(`<Table testId="table"/>`); 804 | 805 | const table = page.getByTestId("table"); 806 | await expect(table).toBeVisible(); 807 | }); 808 | 809 | test("handles data with missing properties", async ({ initTestBed, page }) => { 810 | const incompleteData = [ 811 | { name: "Apple" }, // missing quantity 812 | { quantity: 5 }, // missing name 813 | {}, // missing both 814 | ]; 815 | 816 | await initTestBed(` 817 | <Table data='{${JSON.stringify(incompleteData)}}' testId="table"> 818 | <Column bindTo="name"/> 819 | <Column bindTo="quantity"/> 820 | </Table> 821 | `); 822 | 823 | await expect(page.getByTestId("table")).toBeVisible(); 824 | await expect(page.locator("td").filter({ hasText: "Apple" }).first()).toBeVisible(); 825 | await expect(page.locator("td").filter({ hasText: "5" }).first()).toBeVisible(); 826 | }); 827 | 828 | test("handles deeply nested object properties", async ({ initTestBed, page }) => { 829 | const nestedData = [ 830 | { 831 | user: { 832 | profile: { 833 | name: "John Doe" 834 | } 835 | } 836 | }, 837 | ]; 838 | 839 | await initTestBed(` 840 | <Table data='{${JSON.stringify(nestedData)}}' testId="table"> 841 | <Column bindTo="user.profile.name" header="Name"/> 842 | </Table> 843 | `); 844 | 845 | await expect(page.locator("td").filter({ hasText: "John Doe" }).first()).toBeVisible(); 846 | }); 847 | 848 | test("handles special characters in data", async ({ initTestBed, page }) => { 849 | const specialData = [ 850 | { name: "Special: 🎉", symbol: "★" }, 851 | { name: "Unicode: 你好", symbol: "⚡" }, 852 | { name: "Emoji: 👨👩👧👦", symbol: "🔥" }, 853 | ]; 854 | 855 | await initTestBed(` 856 | <Table data='{${JSON.stringify(specialData)}}' testId="table"> 857 | <Column bindTo="name"/> 858 | <Column bindTo="symbol"/> 859 | </Table> 860 | `); 861 | 862 | await expect(page.locator("td").filter({ hasText: "Special: 🎉" }).first()).toBeVisible(); 863 | await expect(page.locator("td").filter({ hasText: "Unicode: 你好" }).first()).toBeVisible(); 864 | await expect(page.locator("td").filter({ hasText: "Emoji: 👨👩👧👦" }).first()).toBeVisible(); 865 | }); 866 | 867 | test("handles custom sorting icons", async ({ initTestBed, page }) => { 868 | await initTestBed(` 869 | <Table 870 | data='{${JSON.stringify(sampleData)}}' 871 | iconNoSort="sort" 872 | iconSortAsc="sort-up" 873 | iconSortDesc="sort-down" 874 | testId="table" 875 | > 876 | <Column bindTo="name" canSort="true" header="Name"/> 877 | </Table> 878 | `); 879 | 880 | const headers = page.locator("th"); 881 | await expect(headers.first()).toContainText("Name"); 882 | }); 883 | }); 884 | 885 | // ============================================================================= 886 | // THEME AND STYLING TESTS 887 | // ============================================================================= 888 | 889 | // TODO: Need more theme variable tests! 890 | test.describe("Theme Variables and Styling", () => { 891 | test("applies heading background color theme variable", 892 | async ({ initTestBed, page }) => { 893 | await initTestBed(` 894 | <Table data='{${JSON.stringify(sampleData)}}' testId="table"> 895 | <Column bindTo="name" header="Name"/> 896 | </Table> 897 | `, { 898 | testThemeVars: { "backgroundColor-heading-Table": "rgb(255, 0, 0)" }, 899 | }); 900 | 901 | const header = page.locator("th").first(); 902 | await expect(header).toHaveCSS("background-color", "rgb(255, 0, 0)"); 903 | } 904 | ); 905 | }); 906 | 907 | // ============================================================================= 908 | // CELL VERTICAL ALIGNMENT TESTS 909 | // ============================================================================= 910 | 911 | test.describe("Cell Vertical Alignment", () => { 912 | test("applies center alignment by default", async ({ initTestBed, page }) => { 913 | await initTestBed(` 914 | <Table data='{${JSON.stringify(sampleData)}}' testId="table"> 915 | <Column bindTo="name" header="Name"/> 916 | <Column bindTo="quantity" header="Quantity"/> 917 | </Table> 918 | `); 919 | 920 | // Check header cells have center alignment class 921 | const headerCell = page.locator("th").first(); 922 | await expect(headerCell).toHaveClass(/alignCenter/); 923 | 924 | // Check data cells have center alignment class 925 | const dataCell = page.locator("td").first(); 926 | await expect(dataCell).toHaveClass(/alignCenter/); 927 | }); 928 | 929 | test("applies top alignment when cellVerticalAlign='top'", async ({ initTestBed, page }) => { 930 | await initTestBed(` 931 | <Table data='{${JSON.stringify(sampleData)}}' cellVerticalAlign="top" testId="table"> 932 | <Column bindTo="name" header="Name"/> 933 | <Column bindTo="quantity" header="Quantity"/> 934 | </Table> 935 | `); 936 | 937 | // Check header cells have top alignment class 938 | const headerCell = page.locator("th").first(); 939 | await expect(headerCell).toHaveClass(/alignTop/); 940 | 941 | // Check data cells have top alignment class 942 | const dataCell = page.locator("td").first(); 943 | await expect(dataCell).toHaveClass(/alignTop/); 944 | }); 945 | 946 | test("applies bottom alignment when cellVerticalAlign='bottom'", async ({ initTestBed, page }) => { 947 | await initTestBed(` 948 | <Table data='{${JSON.stringify(sampleData)}}' cellVerticalAlign="bottom" testId="table"> 949 | <Column bindTo="name" header="Name"/> 950 | <Column bindTo="quantity" header="Quantity"/> 951 | </Table> 952 | `); 953 | 954 | // Check header cells have bottom alignment class 955 | const headerCell = page.locator("th").first(); 956 | await expect(headerCell).toHaveClass(/alignBottom/); 957 | 958 | // Check data cells have bottom alignment class 959 | const dataCell = page.locator("td").first(); 960 | await expect(dataCell).toHaveClass(/alignBottom/); 961 | }); 962 | 963 | test("applies center alignment when cellVerticalAlign='center'", async ({ initTestBed, page }) => { 964 | await initTestBed(` 965 | <Table data='{${JSON.stringify(sampleData)}}' cellVerticalAlign="center" testId="table"> 966 | <Column bindTo="name" header="Name"/> 967 | <Column bindTo="quantity" header="Quantity"/> 968 | </Table> 969 | `); 970 | 971 | // Check header cells have center alignment class 972 | const headerCell = page.locator("th").first(); 973 | await expect(headerCell).toHaveClass(/alignCenter/); 974 | 975 | // Check data cells have center alignment class 976 | const dataCell = page.locator("td").first(); 977 | await expect(dataCell).toHaveClass(/alignCenter/); 978 | }); 979 | 980 | test("applies alignment to all cells consistently", async ({ initTestBed, page }) => { 981 | await initTestBed(` 982 | <Table data='{${JSON.stringify(sampleData)}}' cellVerticalAlign="top" testId="table"> 983 | <Column bindTo="name" header="Name"/> 984 | <Column bindTo="quantity" header="Quantity"/> 985 | <Column bindTo="category" header="Category"/> 986 | </Table> 987 | `); 988 | 989 | // Check all header cells have the same alignment 990 | const headerCells = page.locator("th"); 991 | const headerCount = await headerCells.count(); 992 | for (let i = 0; i < headerCount; i++) { 993 | await expect(headerCells.nth(i)).toHaveClass(/alignTop/); 994 | } 995 | 996 | // Check all data cells have the same alignment 997 | const dataCells = page.locator("td"); 998 | const dataCount = await dataCells.count(); 999 | for (let i = 0; i < dataCount; i++) { 1000 | await expect(dataCells.nth(i)).toHaveClass(/alignTop/); 1001 | } 1002 | }); 1003 | }); ``` -------------------------------------------------------------------------------- /xmlui/conventions/copilot-conventions.md: -------------------------------------------------------------------------------- ```markdown 1 | # XMLUI Coding Conventions 2 | 3 | ## Default Values Convention 4 | 5 | ### Overview 6 | 7 | XMLUI components follow a specific pattern for handling default values to ensure consistency between the component metadata and their React Native implementations. The convention ensures that default values are defined in a single location and referenced throughout the codebase, preventing duplication and inconsistencies. 8 | 9 | ### Convention Rules 10 | 11 | 1. **Define Default Values in Native Component**: 12 | - Default values should be defined in a `defaultProps` object in the Native component implementation file (e.g., `ComponentNameNative.tsx`). 13 | - The `defaultProps` object should explicitly type which properties it's defining defaults for using TypeScript's `Pick` type. 14 | 15 | 2. **Reference Default Values in Component Implementation**: 16 | - When destructuring props in the component implementation, reference the `defaultProps` values as defaults. 17 | - Example: `{ propName = defaultProps.propName } = props` 18 | 19 | 3. **Reference Default Values in Component Metadata**: 20 | - In the component metadata file (e.g., `ComponentName.tsx`), import the `defaultProps` from the Native component. 21 | - Use these default values in the component metadata's `defaultValue` fields. 22 | - Example: `defaultValue: defaultProps.propName` 23 | 24 | 4. **Resolution of Inconsistencies**: 25 | - If inconsistencies are found between metadata and implementation, the XMLUI (metadata) default value prevails. 26 | - Update the implementation to match the metadata default value. 27 | 28 | ### Example Implementation 29 | 30 | #### ComponentNameNative.tsx 31 | ```typescript 32 | type Props = { 33 | size?: string; 34 | color?: string; 35 | // other props... 36 | }; 37 | 38 | export const defaultProps: Pick<Props, "size" | "color"> = { 39 | size: "medium", 40 | color: "primary", 41 | }; 42 | 43 | export const ComponentName = forwardRef(function ComponentName( 44 | { 45 | size = defaultProps.size, 46 | color = defaultProps.color, 47 | // other props with defaults... 48 | }: Props, 49 | forwardedRef: ForwardedRef<HTMLDivElement> 50 | ) { 51 | // Component implementation... 52 | }); 53 | ``` 54 | 55 | #### ComponentName.tsx 56 | ```typescript 57 | import { createMetadata } from "../../abstractions/ComponentDefs"; 58 | import { ComponentName, defaultProps } from "./ComponentNameNative"; 59 | 60 | export const ComponentNameMd = createMetadata({ 61 | props: { 62 | size: { 63 | description: "The size of the component", 64 | valueType: "string", 65 | defaultValue: defaultProps.size, 66 | }, 67 | color: { 68 | description: "The color of the component", 69 | valueType: "string", 70 | defaultValue: defaultProps.color, 71 | }, 72 | // other props... 73 | }, 74 | // other metadata... 75 | }); 76 | 77 | export const componentNameComponentRenderer = createComponentRenderer( 78 | "ComponentName", 79 | ComponentNameMd, 80 | ({ node, extractValue, layoutCss }) => { 81 | return ( 82 | <ComponentName 83 | size={extractValue(node.props.size)} 84 | color={extractValue(node.props.color)} 85 | style={layoutCss} 86 | > 87 | {/* component content */} 88 | </ComponentName> 89 | ); 90 | } 91 | ); 92 | ``` 93 | 94 | ### Common Mistakes to Avoid 95 | 96 | 1. **Duplicate Default Definitions**: 97 | - Don't define default values both in `defaultProps` and inline in the component. 98 | 99 | 2. **Inconsistent Types**: 100 | - Ensure the type of default values is the same in both metadata and implementation (e.g., `"0.1"` string vs `0.1` number). 101 | 102 | 3. **Setting `defaultProps` on ComponentMetadata**: 103 | - The `ComponentMetadata` type doesn't have a `defaultProps` property, so avoid lines like: 104 | ```typescript 105 | // Do NOT do this as it causes TypeScript errors 106 | ComponentNameMd.defaultProps = defaultProps; 107 | ``` 108 | 109 | 4. **Incomplete Default Props Coverage**: 110 | - Make sure all props with default values follow this convention, not just some of them. 111 | 112 | By following these conventions, we ensure that default values are consistently defined and referenced throughout the XMLUI codebase, making maintenance easier and reducing the likelihood of bugs related to default values. 113 | 114 | ## GitHub Copilot Conventions 115 | 116 | ### Conversation Context Summary 117 | 118 | When GitHub Copilot summarizes the conversation context for the XMLUI project, it should include the following: 119 | 120 | 1. **Include Conventions File**: 121 | - Always read the content of the `/Users/dotneteer/source/xmlui/xmlui/conventions/copilot-conventions.md` file into the context summary. 122 | - This ensures that all conventions are available to Copilot when continuing work on the project. 123 | 124 | 2. **Maintain Record of Components Processed**: 125 | - Keep track of which components have been analyzed and modified. 126 | - List the component file paths that have been worked on. 127 | 128 | 3. **Record Code Changes**: 129 | - Summarize the nature of changes made to components. 130 | - Include specific details about what default prop patterns were implemented or fixed. 131 | 132 | 4. **Track Inconsistencies Found**: 133 | - Document any inconsistencies found between component metadata and implementations. 134 | - Note how these inconsistencies were resolved. 135 | 136 | 5. **Note Pending Work**: 137 | - Maintain a list of components or tasks still pending review or updates. 138 | 139 | This convention ensures continuity in long-running tasks and helps maintain a consistent approach to code modifications across the XMLUI project. 140 | 141 | ## Layout Properties Convention 142 | 143 | ### Overview 144 | 145 | XMLUI has a comprehensive system for handling layout properties that affect component styling and positioning. These properties need to be consistently defined, documented, and integrated throughout the codebase. This convention ensures that any new layout property is properly added to all relevant parts of the system. 146 | 147 | ### Convention Rules 148 | 149 | 1. **Layout Property Definition**: 150 | - All layout properties must be added to the `layoutOptionKeys` array in `descriptorHelper.ts`. 151 | - Properties should be added in alphabetical order within their logical grouping. 152 | 153 | 2. **Layout Resolver Integration**: 154 | - Add the property to the `LayoutProps` type definition in `layout-resolver.ts`. 155 | - Include the property in the `layoutPatterns` object with appropriate pattern validation. 156 | - Group properties logically (Dimensions, Typography, Animations, etc.) with clear comments. 157 | 158 | 3. **Documentation Requirements**: 159 | - Each layout property requires documentation in two places: 160 | - `layout-props.md`: A concise definition with a link to detailed documentation. 161 | - `common-units.md`: Detailed explanation of allowed values, examples, and visual demos. 162 | 163 | 4. **Theme Keyword Links**: 164 | - Add the property to the `themeKeywordLinks` object in `MetadataProcessor.mjs`. 165 | - Link format: `"propertyName": "[propertyName](../styles-and-themes/common-units/#anchor-name)"`. 166 | - Ensure the anchor name exists in the common-units.md file. 167 | 168 | ### Implementation Process 169 | 170 | When adding a new layout property, follow these steps: 171 | 172 | 1. **Update Type Definitions**: 173 | ```typescript 174 | // In layout-resolver.ts 175 | export type LayoutProps = { 176 | // ...existing properties 177 | 178 | // Add new property in appropriate section with comment 179 | newProperty?: string | number; 180 | }; 181 | ``` 182 | 183 | 2. **Update Layout Patterns**: 184 | ```typescript 185 | // In layout-resolver.ts - layoutPatterns object 186 | const layoutPatterns: Record<keyof LayoutProps, RegExp[]> = { 187 | // ...existing patterns 188 | 189 | // Add new property (with validation patterns if needed) 190 | newProperty: [], 191 | }; 192 | ``` 193 | 194 | 3. **Update Property Keys**: 195 | ```typescript 196 | // In descriptorHelper.ts 197 | export const layoutOptionKeys = [ 198 | // ...existing keys 199 | "newProperty", 200 | ]; 201 | ``` 202 | 203 | 4. **Add Documentation in `layout-props.md`**: 204 | ```markdown 205 | ## `newProperty` 206 | 207 | This property [describes what it does](/styles-and-themes/common-units#anchor-name). 208 | ``` 209 | 210 | 5. **Add Detailed Documentation in `common-units.md`**: 211 | ```markdown 212 | ## New Property Values [#anchor-name] 213 | 214 | This value [detailed description of what it does and how it works]... 215 | 216 | | Value | Description | 217 | | ----- | ----------- | 218 | | `value1` | Description of value1 | 219 | | `value2` | Description of value2 | 220 | 221 | ```xmlui-pg name="Example name" 222 | <App> 223 | // Example showing the property in use 224 | </App> 225 | ``` 226 | ``` 227 | 228 | 6. **Add Theme Keyword Link**: 229 | ```javascript 230 | // In MetadataProcessor.mjs - themeKeywordLinks object 231 | const themeKeywordLinks = { 232 | // ...existing links 233 | newProperty: "[newProperty](../styles-and-themes/common-units/#anchor-name)", 234 | }; 235 | ``` 236 | 237 | ### Common Patterns for Layout Properties 238 | 239 | 1. **Size Properties**: 240 | - Support standard CSS units (`px`, `rem`, `em`, `%`, etc.) 241 | - May support special values like `auto`, `inherit`, etc. 242 | - Anchor: `#size` 243 | 244 | 2. **Color Properties**: 245 | - Support color names, hex values, rgb/rgba, hsl/hsla 246 | - May support theme variables with `$` prefix 247 | - Anchor: `#color` 248 | 249 | 3. **Style Properties**: 250 | - Usually support enumerated string values (`solid`, `dashed`, etc.) 251 | - Document all possible values in a table 252 | - Create property-specific anchor (e.g., `#border-style`) 253 | 254 | 4. **Animation Properties**: 255 | - Document component parts (duration, timing function, etc.) 256 | - Include examples with visual demonstrations 257 | - Anchor: specific to property (e.g., `#transition`) 258 | 259 | 5. **Text and Typography Properties**: 260 | - Group related properties together in documentation 261 | - Include visual examples showing differences 262 | - Anchors: specific to property (e.g., `#text-align`, `#word-spacing`) 263 | 264 | ### Example Implementation 265 | 266 | #### Adding the `transition` Layout Property 267 | 268 | 1. **Update Layout Properties Type**: 269 | ```typescript 270 | // In layout-resolver.ts 271 | export type LayoutProps = { 272 | // ...existing properties 273 | 274 | // --- Animation 275 | transition?: string; 276 | }; 277 | ``` 278 | 279 | 2. **Update Layout Patterns**: 280 | ```typescript 281 | // In layout-resolver.ts - layoutPatterns object 282 | const layoutPatterns: Record<keyof LayoutProps, RegExp[]> = { 283 | // ...existing patterns 284 | 285 | // --- Animation 286 | transition: [], 287 | }; 288 | ``` 289 | 290 | 3. **Update Property Keys**: 291 | ```typescript 292 | // In descriptorHelper.ts 293 | export const layoutOptionKeys = [ 294 | // ...existing keys 295 | "transition", 296 | ]; 297 | ``` 298 | 299 | 4. **Add Property Documentation in `layout-props.md`**: 300 | ```markdown 301 | ## `transition` 302 | 303 | This property is a shorthand for [transition effects](/styles-and-themes/common-units#transition) that specify the CSS property to which a transition effect should be applied, the duration and timing of the transition, and any delay. 304 | ``` 305 | 306 | 5. **Add Detailed Documentation in `common-units.md`** (simplified example): 307 | ```markdown 308 | ## Transition Values [#transition] 309 | 310 | This value specifies the CSS property to animate, the duration, timing function, and delay... 311 | 312 | | Timing Function Values | Description | 313 | | --------------------- | ----------- | 314 | | `ease` | Starts slow, becomes fast, then ends slowly... | 315 | 316 | ```xmlui-pg name="Transition examples" 317 | <App> 318 | // Examples showing transitions 319 | </App> 320 | ``` 321 | ``` 322 | 323 | 6. **Add Theme Keyword Link**: 324 | ```javascript 325 | // In MetadataProcessor.mjs - themeKeywordLinks object 326 | const themeKeywordLinks = { 327 | // ...existing links 328 | transition: "[transition](../styles-and-themes/common-units/#transition)", 329 | }; 330 | ``` 331 | 332 | By following these conventions, we ensure consistent implementation and documentation of layout properties across the XMLUI codebase, making the system more maintainable and easier to extend. 333 | 334 | ## End-to-End Testing Convention 335 | 336 | ### Overview 337 | 338 | XMLUI components require comprehensive end-to-end testing using Playwright to ensure reliability, accessibility, and performance. This convention establishes patterns for creating thorough test suites that cover all aspects of component behavior, from basic functionality to edge cases and performance optimizations. 339 | 340 | ### Component Test Location Map 341 | 342 | | Component Type | Component Location | Test File Location | 343 | | -------------- | ----------------- | ----------------- | 344 | | Standard Components | `/xmlui/src/components/ComponentName/ComponentName.tsx` | `/xmlui/src/components/ComponentName/ComponentName.spec.ts` | 345 | | Container Components | `/xmlui/src/components/Container/Container.tsx` | `/xmlui/src/components/Container/Container.spec.ts` | 346 | | Form Components | `/xmlui/src/components/FormControl/FormControl.tsx` | `/xmlui/src/components/FormControl/FormControl.spec.ts` | 347 | 348 | **Critical Rules for Test Creation:** 349 | 1. ✅ ALL component tests MUST be placed in the same folder as the component itself 350 | 2. ✅ ALL newly created tests MUST use the `.skip` modifier 351 | 3. ✅ Each skipped test MUST include the comment: `// TODO: review these Copilot-created tests` 352 | 4. ❌ NEVER place tests in the global test directory (`/tests/tests/`) 353 | 5. ❌ NEVER create non-skipped tests for new components 354 | 355 | ### Convention Rules 356 | 357 | 1. **Test Structure and Organization**: 358 | - Tests should be organized into logical sections with clear comments and separators. 359 | - Use descriptive test names that clearly indicate what behavior is being tested. 360 | - Group related tests together (e.g., "Size Property Tests", "Edge Cases and Name Processing"). 361 | 362 | 2. **Comprehensive Test Coverage**: 363 | - **Basic Functionality**: Core component behavior and prop handling 364 | - **Accessibility**: ARIA attributes, keyboard navigation, screen reader compatibility 365 | - **Visual States**: Different visual configurations and state transitions 366 | - **Edge Cases**: Null/undefined props, empty values, special characters 367 | - **Performance**: Memoization, rapid prop changes, memory stability 368 | - **Integration**: Component behavior in different layout contexts 369 | 370 | 3. **Test Implementation Patterns**: 371 | - Use `initTestBed` for component setup with proper XMLUI markup 372 | - Create driver instances for component interaction 373 | - Use `expect.poll()` for async state verification 374 | - Implement proper cleanup and isolation between tests 375 | 376 | 4. **Accessibility Testing Requirements**: 377 | - Verify correct ARIA attributes (`aria-label`, `role`) 378 | - Test keyboard navigation and focus management 379 | - Ensure proper alt text for images 380 | - Validate screen reader compatibility 381 | 382 | 5. **Performance Testing Patterns**: 383 | - Test memoization behavior through functional verification 384 | - Verify efficient prop change handling 385 | - Test memory stability with multiple component instances 386 | - Ensure smooth state transitions 387 | 388 | ### Test Categories and Implementation 389 | 390 | #### 1. Basic Functionality Tests 391 | ```typescript 392 | test("component renders with basic props", async ({ initTestBed, createComponentDriver }) => { 393 | await initTestBed(`<ComponentName prop="value"/>`, {}); 394 | const driver = await createComponentDriver(); 395 | 396 | await expect(driver.component).toBeVisible(); 397 | await expect(driver.component).toContainText("Expected Content"); 398 | }); 399 | ``` 400 | 401 | #### 2. Accessibility Tests 402 | ```typescript 403 | test("component has correct accessibility attributes", async ({ initTestBed, createComponentDriver }) => { 404 | await initTestBed(`<ComponentName name="Test User"/>`, {}); 405 | const driver = await createComponentDriver(); 406 | 407 | await expect(driver.component).toHaveAttribute('aria-label', 'Expected Label'); 408 | await expect(driver.component).toHaveAttribute('role', 'expected-role'); 409 | }); 410 | 411 | test("component is keyboard accessible when interactive", async ({ initTestBed, createComponentDriver }) => { 412 | const { testStateDriver } = await initTestBed(` 413 | <ComponentName 414 | name="Test User" 415 | onClick="testState = 'keyboard-activated'" 416 | /> 417 | `, {}); 418 | 419 | const driver = await createComponentDriver(); 420 | 421 | await driver.component.focus(); 422 | await expect(driver.component).toBeFocused(); 423 | 424 | await driver.component.press('Enter'); 425 | await expect.poll(testStateDriver.testState).toEqual('keyboard-activated'); 426 | }); 427 | ``` 428 | 429 | #### 3. Visual State Tests 430 | ```typescript 431 | test("component handles different visual states", async ({ initTestBed, createComponentDriver }) => { 432 | // Test initial state 433 | await initTestBed(`<ComponentName state="initial"/>`, {}); 434 | const driver1 = await createComponentDriver(); 435 | await expect(driver1.component).toHaveClass(/initial-state/); 436 | 437 | // Test changed state 438 | await initTestBed(`<ComponentName state="changed"/>`, {}); 439 | const driver2 = await createComponentDriver(); 440 | await expect(driver2.component).toHaveClass(/changed-state/); 441 | }); 442 | ``` 443 | 444 | #### 4. Edge Case Tests 445 | ```typescript 446 | test("component handles null and undefined props gracefully", async ({ initTestBed, createComponentDriver }) => { 447 | // Test with undefined props 448 | await initTestBed(`<ComponentName/>`, {}); 449 | const driver1 = await createComponentDriver(); 450 | await expect(driver1.component).toBeVisible(); 451 | 452 | // Test with empty string props 453 | await initTestBed(`<ComponentName prop=""/>`, {}); 454 | const driver2 = await createComponentDriver(); 455 | await expect(driver2.component).toBeVisible(); 456 | }); 457 | 458 | test("component handles special characters correctly", async ({ initTestBed, createComponentDriver }) => { 459 | await initTestBed(`<ComponentName name="José María"/>`, {}); 460 | const driver = await createComponentDriver(); 461 | await expect(driver.component).toBeVisible(); 462 | // Test specific behavior with special characters 463 | }); 464 | ``` 465 | 466 | #### 5. Performance Tests 467 | ```typescript 468 | test("component memoization prevents unnecessary re-renders", async ({ initTestBed, createComponentDriver }) => { 469 | const { testStateDriver } = await initTestBed(` 470 | <ComponentName 471 | name="Test User" 472 | onClick="testState = ++testState || 1" 473 | /> 474 | `, {}); 475 | 476 | const driver = await createComponentDriver(); 477 | 478 | // Test that memoization works through stable behavior 479 | await driver.component.click(); 480 | await expect.poll(testStateDriver.testState).toEqual(1); 481 | 482 | await driver.component.click(); 483 | await expect.poll(testStateDriver.testState).toEqual(2); 484 | 485 | // Component should maintain consistent behavior 486 | await expect(driver.component).toBeVisible(); 487 | }); 488 | 489 | test("component handles rapid prop changes efficiently", async ({ initTestBed, createComponentDriver }) => { 490 | // Test multiple rapid prop changes 491 | await initTestBed(`<ComponentName prop="value1"/>`, {}); 492 | const driver1 = await createComponentDriver(); 493 | await expect(driver1.component).toBeVisible(); 494 | 495 | await initTestBed(`<ComponentName prop="value2"/>`, {}); 496 | const driver2 = await createComponentDriver(); 497 | await expect(driver2.component).toBeVisible(); 498 | 499 | // Verify final state is correct 500 | await expect(driver2.component).toContainText("Expected Final Content"); 501 | }); 502 | ``` 503 | 504 | #### 6. Integration Tests 505 | ```typescript 506 | test("component works correctly in different layout contexts", async ({ initTestBed, createComponentDriver }) => { 507 | await initTestBed(`<ComponentName name="Layout Test"/>`, {}); 508 | const driver = await createComponentDriver(); 509 | 510 | // Test basic layout integration 511 | await expect(driver.component).toBeVisible(); 512 | 513 | // Test bounding box and dimensions 514 | const boundingBox = await driver.component.boundingBox(); 515 | expect(boundingBox).not.toBeNull(); 516 | expect(boundingBox!.width).toBeGreaterThan(0); 517 | expect(boundingBox!.height).toBeGreaterThan(0); 518 | }); 519 | ``` 520 | 521 | ### Component-Specific Testing Considerations 522 | 523 | #### Container Components (like Accordion) 524 | 1. **Expansion State Testing**: 525 | ```typescript 526 | test("component expands and collapses correctly", async ({ initTestBed, createAccordionDriver }) => { 527 | await initTestBed(`<Accordion><AccordionItem header="Test">Content</AccordionItem></Accordion>`, {}); 528 | const driver = await createAccordionDriver(); 529 | 530 | // Test initial collapsed state 531 | let content = driver.component.locator(".accordion-content"); 532 | await expect(content).not.toBeVisible(); 533 | 534 | // Click to expand 535 | const header = driver.component.locator(".accordion-header"); 536 | await header.click(); 537 | 538 | // Test expanded state 539 | await expect(content).toBeVisible(); 540 | 541 | // Click to collapse 542 | await header.click(); 543 | 544 | // Test collapsed state again 545 | await expect(content).not.toBeVisible(); 546 | }); 547 | ``` 548 | 549 | 2. **Multiple Item Testing**: 550 | ```typescript 551 | test("multiple items can expand independently", async ({ initTestBed, createAccordionDriver }) => { 552 | await initTestBed(` 553 | <Accordion> 554 | <AccordionItem header="Item 1">Content 1</AccordionItem> 555 | <AccordionItem header="Item 2">Content 2</AccordionItem> 556 | </Accordion> 557 | `, {}); 558 | 559 | const driver = await createAccordionDriver(); 560 | const headers = driver.component.locator(".accordion-header").all(); 561 | 562 | // Expand first item 563 | await (await headers)[0].click(); 564 | 565 | // Verify first expanded, second collapsed 566 | let contents = driver.component.locator(".accordion-content").all(); 567 | await expect((await contents)[0]).toBeVisible(); 568 | await expect((await contents)[1]).not.toBeVisible(); 569 | }); 570 | ``` 571 | 572 | 3. **Component API Testing**: 573 | ```typescript 574 | test("component API methods work correctly", async ({ initTestBed, createAccordionDriver, page }) => { 575 | const { testStateDriver } = await initTestBed(` 576 | <Accordion ref="accordionRef"> 577 | <AccordionItem header="API Test">Content</AccordionItem> 578 | </Accordion> 579 | <Button id="expandBtn" onClick="accordionRef.expand('0'); testState = 'expanded'">Expand</Button> 580 | `, {}); 581 | 582 | const driver = await createAccordionDriver(); 583 | 584 | // Test API method 585 | await page.locator("#expandBtn").click(); 586 | await expect.poll(testStateDriver.testState).toEqual('expanded'); 587 | 588 | // Verify component state changed 589 | const content = driver.component.locator(".accordion-content"); 590 | await expect(content).toBeVisible(); 591 | }); 592 | ``` 593 | 594 | 4. **Icon Rendering Testing**: 595 | ```typescript 596 | test("component displays custom icons", async ({ initTestBed, createAccordionDriver }) => { 597 | await initTestBed(` 598 | <Accordion collapsedIcon="plus" expandedIcon="minus"> 599 | <AccordionItem header="Custom Icons">Content</AccordionItem> 600 | </Accordion> 601 | `, {}); 602 | 603 | const driver = await createAccordionDriver(); 604 | 605 | // Check collapsed icon 606 | const icon = driver.component.locator(".chevron"); 607 | await expect(icon).toHaveAttribute("data-icon-name", "plus"); 608 | 609 | // Expand and check expanded icon 610 | const header = driver.component.locator(".accordion-header"); 611 | await header.click(); 612 | await expect(icon).toHaveAttribute("data-icon-name", "minus"); 613 | }); 614 | ``` 615 | 616 | #### Components With Custom Templates 617 | 618 | 1. **Custom Template Rendering**: 619 | ```typescript 620 | test("component renders custom templates correctly", async ({ initTestBed, createComponentDriver }) => { 621 | await initTestBed(` 622 | <ComponentName headerTemplate={ 623 | <div> 624 | <Icon name="star" /> 625 | <Text fontWeight="bold">Custom Template</Text> 626 | </div> 627 | }> 628 | Content 629 | </ComponentName> 630 | `, {}); 631 | 632 | const driver = await createComponentDriver(); 633 | 634 | // Verify template elements rendered correctly 635 | await expect(driver.component.locator("svg[data-icon-name='star']")).toBeVisible(); 636 | await expect(driver.component.locator("text=Custom Template")).toBeVisible(); 637 | }); 638 | ``` 639 | 640 | #### Testing Theme Variables 641 | 642 | 1. **Theme Variable Application Testing**: 643 | ```typescript 644 | test("applies theme variables to specific element parts", async ({ initTestBed, createComponentDriver }) => { 645 | await initTestBed(`<ComponentName />`, { 646 | testThemeVars: { 647 | "backgroundColor-header-ComponentName": "rgb(255, 0, 0)", 648 | "color-content-ComponentName": "rgb(0, 255, 0)", 649 | }, 650 | }); 651 | 652 | const driver = await createComponentDriver(); 653 | 654 | // Check styles applied to specific component parts 655 | await expect(driver.component.locator(".header")).toHaveCSS("background-color", "rgb(255, 0, 0)"); 656 | await expect(driver.component.locator(".content")).toHaveCSS("color", "rgb(0, 255, 0)"); 657 | }); 658 | ``` 659 | 660 | 2. **Border and Padding Theme Variables**: 661 | ```typescript 662 | test("applies border and padding theme variables correctly", async ({ initTestBed, createComponentDriver }) => { 663 | await initTestBed(`<ComponentName />`, { 664 | testThemeVars: { 665 | "border-ComponentName": "1px solid rgb(255, 0, 0)", 666 | "padding-ComponentName": "10px", 667 | }, 668 | }); 669 | 670 | const driver = await createComponentDriver(); 671 | 672 | // Check border properties 673 | await expect(driver.component).toHaveCSS("border-width", "1px"); 674 | await expect(driver.component).toHaveCSS("border-style", "solid"); 675 | await expect(driver.component).toHaveCSS("border-color", "rgb(255, 0, 0)"); 676 | 677 | // Check padding 678 | await expect(driver.component).toHaveCSS("padding", "10px"); 679 | }); 680 | ``` 681 | 682 | 3. **Border Side-Specific Theme Variables**: 683 | ```typescript 684 | test("prioritizes specific border sides over general border", async ({ initTestBed, createComponentDriver }) => { 685 | await initTestBed(`<ComponentName />`, { 686 | testThemeVars: { 687 | "border-ComponentName": "1px solid rgb(0, 0, 0)", 688 | "borderLeft-ComponentName": "2px dashed rgb(255, 0, 0)", 689 | }, 690 | }); 691 | 692 | const driver = await createComponentDriver(); 693 | 694 | // Check general borders applied to most sides 695 | await expect(driver.component).toHaveCSS("border-top-width", "1px"); 696 | await expect(driver.component).toHaveCSS("border-right-width", "1px"); 697 | await expect(driver.component).toHaveCSS("border-bottom-width", "1px"); 698 | 699 | // Check specific side override 700 | await expect(driver.component).toHaveCSS("border-left-width", "2px"); 701 | await expect(driver.component).toHaveCSS("border-left-style", "dashed"); 702 | await expect(driver.component).toHaveCSS("border-left-color", "rgb(255, 0, 0)"); 703 | }); 704 | ``` 705 | 706 | ### Test Name and Organization Patterns 707 | 708 | 1. **Naming Components Tests by Feature**: 709 | - Avoid: `"test component with value X"` 710 | - Prefer: `"renders with size=small correctly"` 711 | 712 | 2. **Naming Pattern for Behavior Tests**: 713 | - `"{verb} {feature} {when/with} {condition}"` 714 | - Examples: 715 | - `"expands when header is clicked"` 716 | - `"applies red background with danger prop"` 717 | - `"renders placeholder when value is empty"` 718 | 719 | 3. **Naming Pattern for Property Tests**: 720 | - `"prioritizes {specific-prop} over {general-prop}"` 721 | - `"applies {style} to {component-part} with {theme-var}"` 722 | - Examples: 723 | - `"prioritizes borderLeftWidth over borderWidth"` 724 | - `"applies padding to all sides with padding theme var"` 725 | 726 | 4. **Test Organization Patterns**: 727 | ```typescript 728 | // ============================================================================= 729 | // BASIC FUNCTIONALITY TESTS 730 | // ============================================================================= 731 | 732 | test("renders with basic props", async () => {}); 733 | test("updates display when props change", async () => {}); 734 | 735 | // ============================================================================= 736 | // ACCESSIBILITY TESTS 737 | // ============================================================================= 738 | 739 | test("has correct accessibility attributes", async () => {}); 740 | test("is keyboard accessible when interactive", async () => {}); 741 | 742 | // ============================================================================= 743 | // VISUAL STATE TESTS 744 | // ============================================================================= 745 | 746 | test("applies theme variables correctly", async () => {}); 747 | test("handles different visual states", async () => {}); 748 | 749 | // ============================================================================= 750 | // EDGE CASE TESTS 751 | // ============================================================================= 752 | 753 | test("handles null and undefined props gracefully", async () => {}); 754 | test("handles special characters correctly", async () => {}); 755 | 756 | // ============================================================================= 757 | // PERFORMANCE TESTS 758 | // ============================================================================= 759 | 760 | test("memoization prevents unnecessary re-renders", async () => {}); 761 | 762 | // ============================================================================= 763 | // INTEGRATION TESTS 764 | // ============================================================================= 765 | 766 | test("works correctly in different layout contexts", async () => {}); 767 | ``` 768 | 769 | By following these conventions, XMLUI components will have comprehensive, reliable, and maintainable end-to-end test suites that ensure quality and performance across all scenarios. 770 | 771 | ### Skipped Test Creation Convention 772 | 773 | When creating skeleton tests for components, follow these guidelines to ensure consistency and maintainability across the test suite. These tests provide a roadmap for future implementation while maintaining a clear structure. 774 | 775 | #### Convention Rules (CRITICAL) 776 | 777 | 1. **Test Structure Organization**: 778 | - Organize existing tests into appropriate categories (Basic Functionality, Accessibility, etc.) 779 | - Maintain clear section separators with consistent formatting 780 | - Keep related tests grouped together within their sections 781 | 782 | 2. **Test Naming Conventions**: 783 | - Rename existing tests to follow naming conventions (as described in Test Name and Organization Patterns) 784 | - Use descriptive, action-oriented names that clearly indicate what's being tested 785 | - Be specific about the behavior or prop being verified 786 | 787 | 3. **Skipped Test Creation** (MANDATORY): 788 | - ✅ ALL newly created tests MUST be skipped using the `.skip` modifier 789 | - ✅ ALWAYS include the standard TODO comment: `// TODO: review these Copilot-created tests` 790 | - ✅ Implement a complete test body that follows test patterns for that category 791 | - ❌ NEVER create non-skipped tests when adding skeleton tests to components 792 | 793 | 4. **Complete Implementation**: 794 | - Even in skipped tests, provide a complete implementation 795 | - Include proper assertions that verify the expected behavior 796 | - Structure the test to provide a clear example of how it should work when enabled 797 | 798 | #### Implementation Process 799 | 800 | 1. **Organize Tests by Category**: 801 | ```typescript 802 | // ============================================================================= 803 | // BASIC FUNCTIONALITY TESTS 804 | // ============================================================================= 805 | 806 | // (Basic tests here) 807 | 808 | // ============================================================================= 809 | // ACCESSIBILITY TESTS 810 | // ============================================================================= 811 | 812 | // (Accessibility tests here) 813 | ``` 814 | 815 | 2. **Rename Existing Tests with Clear Patterns**: 816 | - Before: `test("component test", async () => {...})` 817 | - After: `test("renders with size={size} correctly", async () => {...})` 818 | 819 | 3. **Add Skipped Tests with TODO Comments**: 820 | ```typescript 821 | test.skip("component applies correct theme variables", async ({ initTestBed, createComponentDriver }) => { 822 | // TODO: review these Copilot-created tests 823 | 824 | await initTestBed(`<ComponentName />`, { 825 | testThemeVars: { 826 | "backgroundColor-ComponentName": "rgb(255, 0, 0)", 827 | }, 828 | }); 829 | 830 | const driver = await createComponentDriver(); 831 | await expect(driver.component).toHaveCSS("background-color", "rgb(255, 0, 0)"); 832 | }); 833 | ``` 834 | 835 | 4. **Implement Complete Test Bodies**: 836 | ```typescript 837 | test.skip("handles keyboard navigation correctly", async ({ initTestBed, createComponentDriver }) => { 838 | // TODO: review these Copilot-created tests 839 | 840 | const { testStateDriver } = await initTestBed(` 841 | <ComponentName 842 | onKeyDown="testState = 'key-pressed'" 843 | /> 844 | `, {}); 845 | 846 | const driver = await createComponentDriver(); 847 | 848 | // Focus the component 849 | await driver.component.focus(); 850 | await expect(driver.component).toBeFocused(); 851 | 852 | // Test keyboard interaction 853 | await driver.component.press('Enter'); 854 | await expect.poll(testStateDriver.testState).toEqual('key-pressed'); 855 | }); 856 | ``` 857 | 858 | #### Example Test Suite with Skipped Tests 859 | 860 | ```typescript 861 | import { test, expect } from '@playwright/test'; 862 | 863 | // ============================================================================= 864 | // BASIC FUNCTIONALITY TESTS 865 | // ============================================================================= 866 | 867 | test("renders with default props", async ({ initTestBed, createComponentDriver }) => { 868 | await initTestBed(`<ComponentName />`, {}); 869 | const driver = await createComponentDriver(); 870 | await expect(driver.component).toBeVisible(); 871 | }); 872 | 873 | test.skip("updates display when prop changes", async ({ initTestBed, createComponentDriver }) => { 874 | // TODO: review these Copilot-created tests 875 | 876 | // Test with initial prop 877 | await initTestBed(`<ComponentName value="initial" />`, {}); 878 | let driver = await createComponentDriver(); 879 | await expect(driver.component).toContainText("initial"); 880 | 881 | // Test with updated prop 882 | await initTestBed(`<ComponentName value="updated" />`, {}); 883 | driver = await createComponentDriver(); 884 | await expect(driver.component).toContainText("updated"); 885 | }); 886 | 887 | // ============================================================================= 888 | // ACCESSIBILITY TESTS 889 | // ============================================================================= 890 | 891 | test.skip("has correct accessibility attributes", async ({ initTestBed, createComponentDriver }) => { 892 | // TODO: review these Copilot-created tests 893 | 894 | await initTestBed(`<ComponentName label="Accessible Component" />`, {}); 895 | const driver = await createComponentDriver(); 896 | 897 | await expect(driver.component).toHaveAttribute('role', 'button'); 898 | await expect(driver.component).toHaveAttribute('aria-label', 'Accessible Component'); 899 | await expect(driver.component).toHaveAttribute('tabindex', '0'); 900 | }); 901 | ``` 902 | 903 | ### Best Practices for Skipped Tests 904 | 905 | 1. **Clear Reasoning**: The skipped test should clearly indicate why it's important to implement later 906 | 2. **Full Implementation**: Implement the complete test body, not just a placeholder 907 | 3. **Readable Examples**: Make the test easy to understand for developers who will enable it later 908 | 4. **Descriptive Names**: Use clear test names that explain exactly what behavior is being tested 909 | 5. **Proper Isolation**: Each skipped test should be independent of other tests 910 | 6. **Comprehensive Coverage**: Create skipped tests for all important functionality, even if it's not implemented yet 911 | 912 | ### Common Mistakes to Avoid 913 | 914 | 1. **Empty Test Bodies**: Don't create skipped tests without implementing the test body 915 | 2. **Missing Comments**: Always include the TODO comment explaining these are Copilot-created tests 916 | 3. **Vague Names**: Avoid generic test names like "test component behavior" 917 | 4. **Untestable Assertions**: Don't make assertions that can't be tested with the current implementation 918 | 5. **Incomplete Categories**: Make sure all required test categories (accessibility, etc.) are represented 919 | 920 | By following these conventions for skipped tests, you create a valuable roadmap for future test implementation while maintaining a clear and organized test structure. This approach helps ensure comprehensive test coverage even when all tests aren't immediately ready to run. 921 | 922 | ### Test File Location Conventions (CRITICAL) 923 | 924 | When creating component tests, follow these location conventions: 925 | 926 | 1. **Component-Level Test Location** (MANDATORY): 927 | - ✅ Test files MUST be located in the same directory as the component being tested 928 | - ✅ Use the naming convention `ComponentName.spec.ts` for test files 929 | - ✅ Example: For a component defined in `/components/Button/Button.tsx`, the test MUST be at `/components/Button/Button.spec.ts` 930 | - ❌ NEVER place test files in the global test directory (`/tests/tests/`) 931 | 932 | 2. **Test Import Paths**: 933 | - Import test utilities from the testing directory: 934 | ```typescript 935 | import { expect, test } from "../../testing/fixtures"; 936 | import { getStyles } from "../../testing/component-test-helpers"; 937 | ``` 938 | - Adjust import paths based on the relative location of the component and testing utilities 939 | 940 | 3. **Component Driver Parameters**: 941 | - Use the appropriate fixture parameters based on component needs: 942 | ```typescript 943 | test.skip("basic test", async ({ initTestBed, createComponentDriver }) => { 944 | // For components with component drivers 945 | }); 946 | ``` 947 | 948 | 4. **Test Execution Context**: 949 | - Tests should be executed from the component's directory 950 | - Run specific component tests with: `npx playwright test ComponentName.spec.ts` 951 | - Test runners will automatically locate tests in the component directories 952 | 953 | This location convention ensures tests are closely associated with their components, making the codebase easier to maintain and navigate. It also simplifies the process of finding and updating tests when component implementations change. 954 | 955 | ### Test Execution and Verification 956 | 957 | ### Proper Test File Structure Example 958 | 959 | Below is an example of a correctly structured skeleton test file for a component. Note that ALL tests are marked with `.skip` and include the required TODO comment: 960 | 961 | ```typescript 962 | import { test, expect } from "../../testing/fixtures"; 963 | import { getStyles } from "../../testing/component-test-helpers"; 964 | 965 | // Constants for testing 966 | const RED = "rgb(255, 0, 0)"; 967 | const GREEN = "rgb(0, 255, 0)"; 968 | 969 | // ============================================================================= 970 | // BASIC FUNCTIONALITY TESTS 971 | // ============================================================================= 972 | 973 | test.skip("component renders with basic props", async ({ initTestBed, createComponentDriver }) => { 974 | // TODO: review these Copilot-created tests 975 | 976 | await initTestBed(`<ComponentName prop="value"/>`); 977 | const driver = await createComponentDriver(); 978 | 979 | await expect(driver.component).toBeVisible(); 980 | await expect(driver.component).toContainText("Expected Content"); 981 | }); 982 | 983 | // ============================================================================= 984 | // ACCESSIBILITY TESTS 985 | // ============================================================================= 986 | 987 | test.skip("component has correct accessibility attributes", async ({ initTestBed, createComponentDriver }) => { 988 | // TODO: review these Copilot-created tests 989 | 990 | await initTestBed(`<ComponentName label="Accessible Label"/>`); 991 | const driver = await createComponentDriver(); 992 | 993 | await expect(driver.component).toHaveAttribute('role', 'button'); 994 | await expect(driver.component).toHaveAttribute('aria-label', 'Accessible Label'); 995 | }); 996 | 997 | // ============================================================================= 998 | // VISUAL STATE TESTS 999 | // ============================================================================= 1000 | 1001 | test.skip("component applies theme variables correctly", async ({ initTestBed, createComponentDriver }) => { 1002 | // TODO: review these Copilot-created tests 1003 | 1004 | await initTestBed(`<ComponentName />`, { 1005 | testThemeVars: { 1006 | "backgroundColor-ComponentName": RED, 1007 | }, 1008 | }); 1009 | 1010 | const driver = await createComponentDriver(); 1011 | await expect(driver.component).toHaveCSS("background-color", RED); 1012 | }); 1013 | 1014 | // All remaining test categories follow the same pattern... 1015 | ``` 1016 | 1017 | This example shows the proper structure with: 1018 | 1. Correct imports from the testing directory 1019 | 2. Section separators for test categories 1020 | 3. ALL tests marked with `.skip` 1021 | 4. Required TODO comment in each test 1022 | 5. Proper test implementation following the patterns 1023 | ```