This is page 134 of 181. Use http://codebase.md/xmlui-org/xmlui/tools/vscode/resources/%7Bnode.props.src?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-blog.yml
│ ├── 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
│ ├── layout-changes.md
│ ├── 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
│ │ │ ├── rss.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
│ │ └── blog-theme.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
│ │ │ │ ├── make-navpanel-width-responsive.md
│ │ │ │ ├── modify-a-value-reported-in-a-column.md
│ │ │ │ ├── paginate-a-list.md
│ │ │ │ ├── pass-data-to-a-modal-dialog.md
│ │ │ │ ├── react-to-button-click-not-keystrokes.md
│ │ │ │ ├── set-the-initial-value-of-a-select-from-fetched-data.md
│ │ │ │ ├── share-a-modaldialog-across-components.md
│ │ │ │ ├── sync-selections-between-table-and-list-views.md
│ │ │ │ ├── update-ui-optimistically.md
│ │ │ │ ├── use-built-in-form-validation.md
│ │ │ │ └── use-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
│ ├── component-behaviors.md
│ ├── components-with-options.md
│ ├── containers.md
│ ├── data-operations.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
│ ├── ud-components.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
│ │ │ ├── OptionContext.ts
│ │ │ ├── Select.md
│ │ │ ├── Select.module.scss
│ │ │ ├── Select.spec.ts
│ │ │ ├── Select.tsx
│ │ │ ├── SelectContext.tsx
│ │ │ └── SelectNative.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
│ │ │ └── CoreBehaviors.tsx
│ │ ├── component-hooks.ts
│ │ ├── ComponentDecorator.tsx
│ │ ├── ComponentViewer.tsx
│ │ ├── CompoundComponent.tsx
│ │ ├── constants.ts
│ │ ├── DebugViewProvider.tsx
│ │ ├── descriptorHelper.ts
│ │ ├── devtools
│ │ │ ├── InspectorDialog.module.scss
│ │ │ ├── InspectorDialog.tsx
│ │ │ └── InspectorDialogVisibilityContext.tsx
│ │ ├── EngineError.ts
│ │ ├── event-handlers.ts
│ │ ├── InspectorButton.module.scss
│ │ ├── InspectorContext.tsx
│ │ ├── interception
│ │ │ ├── abstractions.ts
│ │ │ ├── ApiInterceptor.ts
│ │ │ ├── ApiInterceptorProvider.tsx
│ │ │ ├── apiInterceptorWorker.ts
│ │ │ ├── Backend.ts
│ │ │ ├── Errors.ts
│ │ │ ├── IndexedDb.ts
│ │ │ ├── initMock.ts
│ │ │ ├── InMemoryDb.ts
│ │ │ ├── ReadonlyCollection.ts
│ │ │ └── useApiInterceptorContext.tsx
│ │ ├── loader
│ │ │ ├── ApiLoader.tsx
│ │ │ ├── DataLoader.tsx
│ │ │ ├── ExternalDataLoader.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MockLoaderRenderer.tsx
│ │ │ └── PageableLoader.tsx
│ │ ├── LoaderComponent.tsx
│ │ ├── markup-check.ts
│ │ ├── parts.ts
│ │ ├── renderers.ts
│ │ ├── rendering
│ │ │ ├── AppContent.tsx
│ │ │ ├── AppRoot.tsx
│ │ │ ├── AppWrapper.tsx
│ │ │ ├── buildProxy.ts
│ │ │ ├── collectFnVarDeps.ts
│ │ │ ├── ComponentAdapter.tsx
│ │ │ ├── ComponentWrapper.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── containers.ts
│ │ │ ├── ContainerWrapper.tsx
│ │ │ ├── ErrorBoundary.module.scss
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── InvalidComponent.module.scss
│ │ │ ├── InvalidComponent.tsx
│ │ │ ├── nodeUtils.ts
│ │ │ ├── reducer.ts
│ │ │ ├── renderChild.tsx
│ │ │ ├── StandaloneComponent.tsx
│ │ │ ├── StateContainer.tsx
│ │ │ ├── UnknownComponent.module.scss
│ │ │ ├── UnknownComponent.tsx
│ │ │ └── valueExtractor.ts
│ │ ├── reportEngineError.ts
│ │ ├── RestApiProxy.ts
│ │ ├── script-runner
│ │ │ ├── asyncProxy.ts
│ │ │ ├── AttributeValueParser.ts
│ │ │ ├── bannedFunctions.ts
│ │ │ ├── BindingTreeEvaluationContext.ts
│ │ │ ├── eval-tree-async.ts
│ │ │ ├── eval-tree-common.ts
│ │ │ ├── eval-tree-sync.ts
│ │ │ ├── ParameterParser.ts
│ │ │ ├── process-statement-async.ts
│ │ │ ├── process-statement-common.ts
│ │ │ ├── process-statement-sync.ts
│ │ │ ├── ScriptingSourceTree.ts
│ │ │ ├── simplify-expression.ts
│ │ │ ├── statement-queue.ts
│ │ │ └── visitors.ts
│ │ ├── StandaloneApp.tsx
│ │ ├── StandaloneExtensionManager.ts
│ │ ├── TableOfContentsContext.tsx
│ │ ├── theming
│ │ │ ├── _themes.scss
│ │ │ ├── component-layout-resolver.ts
│ │ │ ├── extendThemeUtils.ts
│ │ │ ├── hvar.ts
│ │ │ ├── layout-resolver.ts
│ │ │ ├── parse-layout-props.ts
│ │ │ ├── StyleContext.tsx
│ │ │ ├── StyleRegistry.ts
│ │ │ ├── ThemeContext.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── themes
│ │ │ │ ├── base-utils.ts
│ │ │ │ ├── palette.ts
│ │ │ │ ├── root.ts
│ │ │ │ ├── solid.ts
│ │ │ │ ├── theme-colors.ts
│ │ │ │ └── xmlui.ts
│ │ │ ├── themeVars.module.scss
│ │ │ ├── themeVars.ts
│ │ │ ├── transformThemeVars.ts
│ │ │ └── utils.ts
│ │ ├── utils
│ │ │ ├── actionUtils.ts
│ │ │ ├── audio-utils.ts
│ │ │ ├── base64-utils.ts
│ │ │ ├── compound-utils.ts
│ │ │ ├── css-utils.ts
│ │ │ ├── DataLoaderQueryKeyGenerator.ts
│ │ │ ├── date-utils.ts
│ │ │ ├── extractParam.ts
│ │ │ ├── hooks.tsx
│ │ │ ├── LruCache.ts
│ │ │ ├── mergeProps.ts
│ │ │ ├── misc.ts
│ │ │ ├── request-params.ts
│ │ │ ├── statementUtils.ts
│ │ │ └── treeUtils.ts
│ │ └── xmlui-parser.ts
│ ├── index-standalone.ts
│ ├── index.scss
│ ├── index.ts
│ ├── language-server
│ │ ├── server-common.ts
│ │ ├── server-web-worker.ts
│ │ ├── server.ts
│ │ ├── services
│ │ │ ├── common
│ │ │ │ ├── docs-generation.ts
│ │ │ │ ├── lsp-utils.ts
│ │ │ │ ├── metadata-utils.ts
│ │ │ │ └── syntax-node-utilities.ts
│ │ │ ├── completion.ts
│ │ │ ├── diagnostic.ts
│ │ │ ├── format.ts
│ │ │ └── hover.ts
│ │ └── xmlui-metadata-generated.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 |
```