Skip to content

Conversation

@Sqrcz
Copy link
Collaborator

@Sqrcz Sqrcz commented Dec 4, 2025

Summary by CodeRabbit

  • New Features

    • Button now supports Vue Router navigation via a to prop, with improved handling of href and disabled
    • Smarter selection of link/button rendering with graceful fallback when routing is unavailable
  • Tests

    • Expanded coverage for router navigation, link rendering, button states, loading, slots, variants, and custom classes

✏️ Tip: You can customize this high-level summary in your review settings.

@Sqrcz Sqrcz self-assigned this Dec 4, 2025
@netlify
Copy link

netlify bot commented Dec 4, 2025

Deploy Preview for sensational-seahorse-8635f8 ready!

Name Link
🔨 Latest commit 51814b1
🔍 Latest deploy log https://app.netlify.com/projects/sensational-seahorse-8635f8/deploys/6931ef03475bda00086d42b2
😎 Deploy Preview https://deploy-preview-417--sensational-seahorse-8635f8.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

Walkthrough

FwbButton now supports Vue Router navigation via computed properties that choose between router-link, anchor, or button and dynamically bind the appropriate navigation attribute. The test suite is reorganized and expanded to cover router behavior, link/button states, variants, slots, loading, and custom classes.

Changes

Cohort / File(s) Summary
Component Enhancement
src/components/FwbButton/FwbButton.vue
Adds computed properties buttonComponent, linkAttr, and linkValue to select router-link / anchor / button and dynamically bind navigation attributes. Adds to prop (string
Test Restructuring & Expansion
src/components/FwbButton/tests/Button.spec.ts
Replaces previous tests with structured describe blocks (Basic Rendering, Router Navigation, Link Functionality, Button States, Loading States, Slots, Button Variants, Custom Classes). Introduces RouterLinkStub and extensive assertions for router integration, attribute precedence (to over href), disabled/loading states, and variant classes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify computed logic in buttonComponent, linkAttr, and linkValue for correct precedence (to → href → none) and tag selection.
  • Confirm fallback behavior and console warning when vue-router / router-link is unavailable.
  • Review template v-bind usage to ensure attributes are applied only when expected (especially disabled and loading states).
  • Check tests for accurate stubbing of router-link and correctness of assertions around navigation props.

Poem

🐰
I hopped from prop to prop with care,
Chose routes or links depending where,
If router sleeps I fall back light,
Tests line up to check my flight,
A tiny hop — the button's right. ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly describes the main change: adding Vue Router navigation support to the FwbButton component, which aligns with the substantial modifications to props, computed properties, and template bindings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/components/FwbButton/FwbButton.vue (2)

133-144: resolveComponent('router-link') try/catch likely never exercises the fallback

In buttonComponent:

if (props.to) {
  try {
    return resolveComponent('router-link')
  } catch {
    console.warn('router-link component not found...')
    return 'a'
  }
}

In Vue 3, resolveComponent typically warns and returns a fallback rather than throwing on unresolved names, so this catch block is unlikely to ever run. That means:

  • In an app without vue-router, you’ll probably still get a <router-link> tag instead of the intended <a> fallback.
  • The warning you emit is redundant with Vue’s own unresolved‑component warning but the behavior is unchanged.

If you want a real, tested fallback:

  • Either rely on the string 'router-link' directly (and document that to requires vue-router), or
  • Explicitly check the app context for a registered router-link component and fall back to 'a' when it’s missing, and add a test that mounts without a router-link stub and asserts an <a> is rendered.

133-149: Clarify interaction between tag and to (especially for nuxt-link)

When props.to is set, buttonComponent always resolves 'router-link', ignoring the tag prop, while linkAttr still special‑cases props.tag === 'router-link' || props.tag === 'nuxt-link'.

This leads to slightly surprising behavior:

  • tag="nuxt-link", to="/foo" → component still resolves router-link, not nuxt-link.
  • tag="router-link", href="/foo", no to → you correctly render a router-link with to="/foo" via linkAttr/linkValue.

If supporting Nuxt’s NuxtLink explicitly is a goal, you may want to:

  • Respect tag === 'nuxt-link' when to is provided (e.g. resolve that instead of hard‑coding 'router-link'), or
  • Document that to always uses vue-router’s router-link and that Nuxt users should rely on the RouterLink alias, not NuxtLink, when using this prop.

Right now the behavior is workable but a bit implicit; tightening this contract would reduce surprises for Nuxt users.

src/components/FwbButton/tests/Button.spec.ts (3)

6-12: Strengthen router fallback coverage and expectations

The Router Navigation tests nicely cover:

  • Rendering as router-link when to is provided (string and object).
  • Prioritizing to over href.

However, the behavior you comment about here:

// In a real environment without vue-router, this would fallback to 'a'

is never actually tested—every router test provides a 'router-link' stub.

Given the buttonComponent logic aims to fall back to <a> when router-link is unavailable, consider adding a dedicated test that:

  • Mounts FwbButton with a to prop but without stubbing router-link.
  • Asserts that the root element is an <a> with the correct href/to resolution (once the component logic is adjusted to actually fall back).

This will protect the intended non‑router environment behavior from regressions and verify the try/catch (or alternative detection) actually works.

Also applies to: 56-122


161-184: Disabled link test codifies Vue’s internal behavior rather than the desired contract

This test:

// For links, disabled should not be an attribute, but the component might still render it as 'false'
const disabledAttr = wrapper.find('a').attributes('disabled')
expect(disabledAttr).toBe('false')

asserts that <a> carries disabled="false", which is really an artifact of how Vue binds attrs on non‑boolean‑aware elements, not a desirable API surface.

If the intended contract is “links are never disabled via a disabled attribute”, it would be clearer to assert that the attribute is absent instead:

  • Either expect(disabledAttr).toBeUndefined(), or
  • Update the component to avoid binding disabled for non‑button elements and then adjust this test.

Right now the comment and assertion disagree on what “correct” looks like; aligning the expectation with the intended behavior will make future refactors to the component less brittle.


186-220: Loading state tests rely on brittle positional selectors

For the loading tests you use:

const firstChild = wrapper.find('div:first-child')

const lastChild = wrapper.find('div:last-child')

This depends heavily on the exact DOM structure (which div comes first/last under the root), so minor template changes (e.g., wrapping content, adding an extra container) will break these tests without changing behavior.

You can make them more robust by targeting the known containers instead, e.g.:

  • For prefix: wrapper.find('div.mr-2')
  • For suffix: wrapper.find('div.ml-2')

or by scoping the search under the relevant wrapper element.

That way the tests assert semantic placement (spinner in the prefix/suffix container) rather than raw child indices.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b330dc4 and 872ca60.

📒 Files selected for processing (2)
  • src/components/FwbButton/FwbButton.vue (3 hunks)
  • src/components/FwbButton/tests/Button.spec.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-01-11T13:26:22.855Z
Learnt from: Sqrcz
Repo: themesberg/flowbite-vue PR: 338
File: docs/components/dropdown.md:243-261
Timestamp: 2025-01-11T13:26:22.855Z
Learning: The `disabled` prop in the FwbDropdown component is a boolean prop that defaults to `false` and controls whether the dropdown trigger button is disabled.

Applied to files:

  • src/components/FwbButton/FwbButton.vue

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/components/FwbButton/tests/Button.spec.ts (2)

96-113: “router-link availability” test doesn’t hit the fallback path and duplicates the happy path

it('should handle router-link availability gracefully', ...) currently mounts with a router-link stub registered, so it executes the same code path as should render as router-link when to prop is provided and never exercises the catch/fallback branch in buttonComponent.

Either:

  • Remove this test as redundant, or
  • Rework it to simulate the “no vue-router installed” scenario (e.g., mount without a router-link stub and assert that an <a> is rendered with the expected navigation attribute, once the fallback logic is fixed).

This will better validate the new fallback behavior rather than duplicating the primary happy-path test.


195-229: Loading spinner position tests are slightly brittle to markup changes

The loading tests use selectors like div:first-child / div:last-child to infer prefix/suffix placement. This works with the current structure but could break if additional wrapper <div> elements are introduced.

If you want more resilient tests, consider targeting the known wrapper around the spinner (e.g., by class like .mr-2 / .ml-2) or asserting relative order in the rendered text/DOM rather than relying on “first-child/last-child” selectors.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 872ca60 and 51814b1.

📒 Files selected for processing (2)
  • src/components/FwbButton/FwbButton.vue (3 hunks)
  • src/components/FwbButton/tests/Button.spec.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-01-11T13:26:22.708Z
Learnt from: Sqrcz
Repo: themesberg/flowbite-vue PR: 338
File: src/components/FwbDropdown/FwbDropdown.vue:9-9
Timestamp: 2025-01-11T13:26:22.708Z
Learning: In Vue components with disabled states, ensure click handlers respect the disabled prop by adding early returns (e.g., `if (props.disabled) return`). The disabled attribute alone on elements isn't sufficient as the click handler can still execute.

Applied to files:

  • src/components/FwbButton/FwbButton.vue
📚 Learning: 2025-01-11T13:26:22.855Z
Learnt from: Sqrcz
Repo: themesberg/flowbite-vue PR: 338
File: docs/components/dropdown.md:243-261
Timestamp: 2025-01-11T13:26:22.855Z
Learning: The `disabled` prop in the FwbDropdown component is a boolean prop that defaults to `false` and controls whether the dropdown trigger button is disabled.

Applied to files:

  • src/components/FwbButton/FwbButton.vue
🔇 Additional comments (6)
src/components/FwbButton/tests/Button.spec.ts (4)

6-12: RouterLink stubbing and navigation tests look solid

Using a simple RouterLinkStub with a to prop and slot-only template is a clean way to test the to/router-link resolution. The router navigation tests (string to, route-object to, and to vs href precedence) exercise the new computed logic well and should guard most regressions around routing behavior.

Also applies to: 65-131


231-359: Slots and variant tests provide good coverage of styling behavior

The slot tests (prefix/suffix with and without outline gradients) and variant tests (colors, sizes, pill/square, outline, gradient, shadow) give broad coverage of the visual branches driven by the new props. Assertions are intentionally loose (e.g., checking presence of some classes instead of exact lists), which should keep them stable while still catching regressions in the basic styling behavior.


361-385: Custom class handling is well-covered

The tests for class as both a string and an object confirm that user-supplied classes are merged correctly with internal button classes and that conditional object entries behave as expected. This should prevent regressions around the new class prop behavior and its interaction with useMergeClasses.


170-193: Update disabled-link expectation to match component behavior and avoid relying on disabled="false"

it('should handle disabled state for link elements correctly', ...) currently expects:

const disabledAttr = wrapper.find('a').attributes('disabled')
expect(disabledAttr).toBe('false')

But the component now only binds disabled when buttonComponent === 'button', so anchors will not have a disabled attribute at all. Relying on disabled="false" for links is also semantically odd.

Consider changing this test to assert that no disabled attribute is present and, if you add any visual/ARIA treatment for disabled links, assert that instead:

- const disabledAttr = wrapper.find('a').attributes('disabled')
- expect(disabledAttr).toBe('false')
+ const link = wrapper.find('a')
+ expect(link.attributes('disabled')).toBeUndefined()
+ // Optionally: expect(link.attributes('aria-disabled')).toBe('true') or similar, if implemented.

This will bring the test suite back in sync with the component and align with standard HTML semantics.

src/components/FwbButton/FwbButton.vue (2)

133-157: Router fallback leaves <a> without href when vue-router is missing; adjust linkAttr to keep navigation working

When props.to is provided but router-link is unavailable:

  • buttonComponent falls back to 'a' in the catch branch, but
  • linkAttr still returns 'to' and linkValue returns props.to.

This produces markup like <a to="/dashboard">...</a>, which won't navigate anywhere even though you attempted a graceful fallback. The warning helps, but the user-visible behavior is a non-functional link.

Consider basing linkAttr on whether router-link could actually be resolved, so that in the fallback case you emit href instead of to:

-const buttonComponent = computed(() => {
-  if (props.to) {
-    try {
-      return resolveComponent('router-link')
-    } catch {
-      console.warn('router-link component not found. Make sure vue-router is installed and properly configured.')
-      return 'a'
-    }
-  }
-  return props.href ? linkComponent : 'button'
-})
+let resolvedRouterLink: any | null
+let routerLinkResolved = false
+
+const resolveRouterLink = () => {
+  if (!routerLinkResolved) {
+    routerLinkResolved = true
+    try {
+      resolvedRouterLink = resolveComponent('router-link')
+    } catch {
+      resolvedRouterLink = null
+    }
+  }
+  return resolvedRouterLink
+}
+
+const buttonComponent = computed(() => {
+  if (props.to) {
+    const routerLink = resolveRouterLink()
+    if (routerLink) return routerLink
+
+    console.warn('router-link component not found. Make sure vue-router is installed and properly configured.')
+    return 'a'
+  }
+  return props.href ? linkComponent : 'button'
+})
 
-const linkAttr = computed(() => {
-  if (props.to) return 'to'
+const linkAttr = computed(() => {
+  if (props.to) {
+    // Use `to` only when router-link is actually available; otherwise fall back to `href`.
+    return resolveRouterLink() ? 'to' : 'href'
+  }
   if (props.href) return props.tag === 'router-link' || props.tag === 'nuxt-link' ? 'to' : 'href'
   return undefined
 })

linkValue can stay as-is (it already returns props.to/props.href), and this keeps the existing precedence (to over href) while making the non-router fallback actually render a working <a href="...">.


2-7: Disabled behavior only applies to <button> elements; update tests and consider adding disabled enforcement for interactive links

The binding :disabled="buttonComponent === 'button' && disabled" correctly avoids invalid disabled attributes on anchors/router-links. However:

  • Non-button components (<a>, <router-link>) will never receive a disabled attribute even when disabled is true, making disabled semantics invisible to users and assistive technologies.
  • Tests expecting disabled='false' on link elements should be updated to reflect this implementation.

For truly disabled link variants, add an accessible pattern: aria-disabled="true" + pointer-events-none/cursor-not-allowed class + early-return click handlers (e.g., if (props.disabled) return). This ensures disabled state is enforced even when consumers attach click handlers, aligning with how other disabled components behave.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant