mirror of https://github.com/rancher/dashboard.git
Updating <router-link> to no longer use the deprecated :tag property
This commit is contained in:
parent
9cc17f1338
commit
e205b568ee
|
|
@ -423,17 +423,24 @@ export default {
|
||||||
<!-- Cluster tools -->
|
<!-- Cluster tools -->
|
||||||
<router-link
|
<router-link
|
||||||
v-if="showClusterTools"
|
v-if="showClusterTools"
|
||||||
tag="div"
|
v-slot="{ href, navigate }"
|
||||||
class="tools"
|
custom
|
||||||
:to="{name: 'c-cluster-explorer-tools', params: {cluster: clusterId}}"
|
:to="{name: 'c-cluster-explorer-tools', params: {cluster: clusterId}}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="tools"
|
||||||
|
@click="navigate"
|
||||||
|
@keypress.enter="navigate"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="tools-button"
|
class="tools-button"
|
||||||
|
:href="href"
|
||||||
@click="collapseAll()"
|
@click="collapseAll()"
|
||||||
>
|
>
|
||||||
<i class="icon icon-gear" />
|
<i class="icon icon-gear" />
|
||||||
<span>{{ t('nav.clusterTools') }}</span>
|
<span>{{ t('nav.clusterTools') }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<!-- SideNav footer area (seems to be tied to harvester) -->
|
<!-- SideNav footer area (seems to be tied to harvester) -->
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'TabbedLinks',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* Default tab to display from the list
|
|
||||||
*/
|
|
||||||
defaultTab: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The list of tabs to display
|
|
||||||
* @model
|
|
||||||
*/
|
|
||||||
tabList: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
default: () => [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
const { tabList } = this;
|
|
||||||
|
|
||||||
return { tabs: tabList };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<ul
|
|
||||||
v-if="tabs !== null && tabs.length > 0"
|
|
||||||
role="tablist"
|
|
||||||
class="tabs clearfix"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.name"
|
|
||||||
:to="tab.route"
|
|
||||||
tag="li"
|
|
||||||
role="presentation"
|
|
||||||
class="tab"
|
|
||||||
exact-active-class="active"
|
|
||||||
exact
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
{{ tab.label }}
|
|
||||||
</a>
|
|
||||||
</router-link>
|
|
||||||
</ul>
|
|
||||||
<div class="tab-container">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tabs {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
float: left;
|
|
||||||
border-radius: 3px 3px 0 0;
|
|
||||||
margin: 0 8px 0 0;
|
|
||||||
|
|
||||||
A {
|
|
||||||
display: block;
|
|
||||||
padding: 10px 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: var(--tabbed-container-bg);
|
|
||||||
border-bottom-color: var(--tabbed-container-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-container {
|
|
||||||
padding: 20px;
|
|
||||||
background-color: var(--tabbed-container-bg);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -690,27 +690,48 @@ export default {
|
||||||
</li>
|
</li>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="showPreferencesLink"
|
v-if="showPreferencesLink"
|
||||||
tag="li"
|
v-slot="{ href, navigate }"
|
||||||
|
custom
|
||||||
:to="{name: 'prefs'}"
|
:to="{name: 'prefs'}"
|
||||||
class="user-menu-item"
|
|
||||||
>
|
>
|
||||||
<a>{{ t('nav.userMenu.preferences') }}</a>
|
<li
|
||||||
|
class="user-menu-item"
|
||||||
|
@click="navigate"
|
||||||
|
@keypress.enter="navigate"
|
||||||
|
>
|
||||||
|
<a :href="href">{{ t('nav.userMenu.preferences') }}</a>
|
||||||
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="showAccountAndApiKeyLink"
|
v-if="showAccountAndApiKeyLink"
|
||||||
tag="li"
|
v-slot="{ href, navigate }"
|
||||||
|
custom
|
||||||
:to="{name: 'account'}"
|
:to="{name: 'account'}"
|
||||||
class="user-menu-item"
|
|
||||||
>
|
>
|
||||||
<a>{{ t('nav.userMenu.accountAndKeys', {}, true) }}</a>
|
<li
|
||||||
|
class="user-menu-item"
|
||||||
|
@click="navigate"
|
||||||
|
@keypress.enter="navigate"
|
||||||
|
>
|
||||||
|
<a :href="href">{{ t('nav.userMenu.accountAndKeys', {}, true) }}</a>
|
||||||
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="authEnabled"
|
v-if="authEnabled"
|
||||||
tag="li"
|
v-slot="{ href, navigate }"
|
||||||
|
custom
|
||||||
:to="generateLogoutRoute"
|
:to="generateLogoutRoute"
|
||||||
class="user-menu-item"
|
|
||||||
>
|
>
|
||||||
<a @blur="showMenu(false)">{{ t('nav.userMenu.logOut') }}</a>
|
<li
|
||||||
|
class="user-menu-item"
|
||||||
|
@click="navigate"
|
||||||
|
@keypress.enter="navigate"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="href"
|
||||||
|
@blur="showMenu(false)"
|
||||||
|
>{{ t('nav.userMenu.logOut') }}</a>
|
||||||
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -28,65 +28,10 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return { near: false };
|
||||||
near: false,
|
|
||||||
over: false,
|
|
||||||
menuPath: this.type.route ? this.$router.resolve(this.type.route)?.route?.path : undefined,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isCurrent() {
|
|
||||||
// This is required to avoid scenarios where fragments break vue routers location matching
|
|
||||||
// For example, the following fails
|
|
||||||
// Curruent Path /c/c-m-hzqf4tqt/explorer/members#project-membership
|
|
||||||
// Menu Path /c/c-m-hzqf4tqt/explorer/members
|
|
||||||
// vue-router exact-path="true" fixes this (https://v3.router.vuejs.org/api/#exact-path),
|
|
||||||
// but fails when the the current path is a child (for instance a resource detail page)
|
|
||||||
|
|
||||||
// Scenarios to consider
|
|
||||||
// - Fragement world
|
|
||||||
// Curruent Path /c/c-m-hzqf4tqt/explorer/members#project-membership
|
|
||||||
// Menu Path /c/c-m-hzqf4tqt/explorer/members
|
|
||||||
// - Similar current paths
|
|
||||||
// /c/c-m-hzqf4tqt/fleet/fleet.cattle.io.bundlenamespacemapping
|
|
||||||
// /c/c-m-hzqf4tqt/fleet/fleet.cattle.io.bundle
|
|
||||||
// - Other menu items that appear in current menu item
|
|
||||||
// /c/c-m-hzqf4tqt/fleet
|
|
||||||
// /c/c-m-hzqf4tqt/fleet/management.cattle.io.fleetworkspace
|
|
||||||
|
|
||||||
// If there's no hash the n-link will determine it's linkActiveClass correctly, so avoid this faff
|
|
||||||
const invalidHash = !this.$route.hash;
|
|
||||||
// Lets be super safe
|
|
||||||
const invalidProps = !this.menuPath || !this.$route.path;
|
|
||||||
|
|
||||||
if (invalidHash || invalidProps) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're kind of, but in a fixing way, copying n-link --> vue-router link see vue-router/src/components/link.js & vue-router/src/util/route.js
|
|
||||||
// We're only going to compare the path and ignore query and fragment
|
|
||||||
|
|
||||||
if (this.type.exact) {
|
|
||||||
return this.$route.path === this.menuPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPath = this.$route.path.split('/');
|
|
||||||
const menuPath = this.menuPath.split('/');
|
|
||||||
|
|
||||||
if (menuPath.length > currentPath.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < menuPath.length; i++) {
|
|
||||||
if (menuPath[i] !== currentPath[i]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
showFavorite() {
|
showFavorite() {
|
||||||
return ( this.type.mode && this.near && showFavoritesFor.includes(this.type.mode) );
|
return ( this.type.mode && this.near && showFavoritesFor.includes(this.type.mode) );
|
||||||
},
|
},
|
||||||
|
|
@ -116,14 +61,6 @@ export default {
|
||||||
this.near = val;
|
this.near = val;
|
||||||
},
|
},
|
||||||
|
|
||||||
setOver(val) {
|
|
||||||
this.over = val;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeFavorite() {
|
|
||||||
this.$store.dispatch('type-map/removeFavorite', this.type.name);
|
|
||||||
},
|
|
||||||
|
|
||||||
selectType() {
|
selectType() {
|
||||||
// Prevent issues if custom NavLink is used #5047
|
// Prevent issues if custom NavLink is used #5047
|
||||||
if (this.type?.route) {
|
if (this.type?.route) {
|
||||||
|
|
@ -142,19 +79,25 @@ export default {
|
||||||
<router-link
|
<router-link
|
||||||
v-if="type.route"
|
v-if="type.route"
|
||||||
:key="type.name"
|
:key="type.name"
|
||||||
|
v-slot="{ href, navigate, isActive, isExactActive }"
|
||||||
|
custom
|
||||||
:to="type.route"
|
:to="type.route"
|
||||||
tag="li"
|
|
||||||
class="child nav-type"
|
|
||||||
:class="{'root': isRoot, [`depth-${depth}`]: true, 'router-link-active': isCurrent}"
|
|
||||||
:exact="type.exact"
|
:exact="type.exact"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="child nav-type"
|
||||||
|
:class="{'root': isRoot, [`depth-${depth}`]: true, 'router-link-active': isActive, 'router-link-exact-active': isExactActive}"
|
||||||
|
@click="navigate"
|
||||||
|
@keypress.enter="navigate"
|
||||||
>
|
>
|
||||||
<TabTitle
|
<TabTitle
|
||||||
v-if="$router.resolve(type.route).route.path === $route.path"
|
v-if="isExactActive"
|
||||||
:show-child="false"
|
:show-child="false"
|
||||||
>
|
>
|
||||||
{{ type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label) }}
|
{{ type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label) }}
|
||||||
</TabTitle>
|
</TabTitle>
|
||||||
<a
|
<a
|
||||||
|
:href="href"
|
||||||
@click="selectType"
|
@click="selectType"
|
||||||
@mouseenter="setNear(true)"
|
@mouseenter="setNear(true)"
|
||||||
@mouseleave="setNear(false)"
|
@mouseleave="setNear(false)"
|
||||||
|
|
@ -181,24 +124,25 @@ export default {
|
||||||
v-if="namespaceIcon"
|
v-if="namespaceIcon"
|
||||||
class="icon icon-namespace"
|
class="icon icon-namespace"
|
||||||
:class="{'ns-and-icon': showCount}"
|
:class="{'ns-and-icon': showCount}"
|
||||||
|
data-testid="type-namespaced"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="showCount"
|
v-if="showCount"
|
||||||
|
data-testid="type-count"
|
||||||
>{{ count }}</span>
|
>{{ count }}</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
<li
|
<li
|
||||||
v-else-if="type.link"
|
v-else-if="type.link"
|
||||||
class="child nav-type"
|
class="child nav-type"
|
||||||
|
data-testid="link-type"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:href="type.link"
|
:href="type.link"
|
||||||
:target="type.target"
|
:target="type.target"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
@click="selectType"
|
|
||||||
@mouseenter="setNear(true)"
|
|
||||||
@mouseleave="setNear(false)"
|
|
||||||
>
|
>
|
||||||
<span class="label">{{ type.label }} <i class="icon icon-external-link" /></span>
|
<span class="label">{{ type.label }} <i class="icon icon-external-link" /></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,167 +1,362 @@
|
||||||
import { mount, RouterLinkStub } from '@vue/test-utils';
|
import { shallowMount, RouterLinkStub, createLocalVue } from '@vue/test-utils';
|
||||||
import Type from '@shell/components/nav/Type.vue';
|
import Type from '@shell/components/nav/Type.vue';
|
||||||
|
import { createChildRenderingRouterLinkStub } from '@shell/utils/unit-tests/ChildRenderingRouterLinkStub';
|
||||||
|
import { TYPE_MODES } from '@shell/store/type-map';
|
||||||
|
|
||||||
// Mandatory to mock vue-router in this test
|
// Configuration
|
||||||
jest.mock('vue-router');
|
const activeClass = 'router-link-active';
|
||||||
|
const exactActiveClass = 'router-link-exact-active';
|
||||||
// Configuration text
|
const rootClass = 'root';
|
||||||
const className = 'router-link-active';
|
const favoriteClass = 'favorite';
|
||||||
|
|
||||||
describe('component: Type', () => {
|
describe('component: Type', () => {
|
||||||
describe('should not use highlight class', () => {
|
describe('testing router-link type', () => {
|
||||||
it('given no hash', () => {
|
const localVue = createLocalVue();
|
||||||
const wrapper = mount(Type, {
|
|
||||||
propsData: { type: { route: 'something else' } },
|
localVue.directive('cleanHtml', (identity) => identity);
|
||||||
stubs: { nLink: RouterLinkStub },
|
|
||||||
|
const defaultRouteTypeProp = {
|
||||||
|
name: 'route-type',
|
||||||
|
route: 'route',
|
||||||
|
exact: true,
|
||||||
|
mode: TYPE_MODES.FAVORITE
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultCount = 1;
|
||||||
|
const storeMock = {
|
||||||
|
getters: {
|
||||||
|
currentStore: () => 'cluster',
|
||||||
|
'cluster/count': () => defaultCount,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('should pass props correctly', () => {
|
||||||
|
it('should forward Type props to router-link', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: RouterLinkStub },
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkStub = wrapper.findComponent(RouterLinkStub);
|
||||||
|
|
||||||
|
expect(linkStub.props().to).toBe(defaultRouteTypeProp.route);
|
||||||
|
expect(linkStub.props().exact).toBe(defaultRouteTypeProp.exact);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use router-link-slot href prop', () => {
|
||||||
|
const fakeHref = 'fake-href';
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub({ href: fakeHref }) },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`a[href='${ fakeHref }']`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use router-link-slot navigate prop', () => {
|
||||||
|
const navigate = jest.fn();
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub({ isActive: true, navigate }) },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.${ activeClass }`);
|
||||||
|
|
||||||
|
elementWithSelector.trigger('click');
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should not use classes if preconditions are not met', () => {
|
||||||
|
it('should not use active class if the link is not active', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub({ isActive: false }) },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.${ activeClass }`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use exact active class if the link is not active', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub({ isExactActive: false }) },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.${ exactActiveClass }`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not use root class if the isRoot prop is false', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp, isRoot: false },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub() },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.${ rootClass }`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should use classes if preconditions are met', () => {
|
||||||
|
it('should use active class if the link is active', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub({ isActive: true }) },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.${ activeClass }`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use exact active class if the link is active', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub({ isExactActive: true }) },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.${ exactActiveClass }`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use root class if the isRoot prop is true', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp, isRoot: true },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub() },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.${ rootClass }`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show depth-0 class if depth prop is not defined', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub() },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.depth-0`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show depth-1 class if depth prop is defined as 1', () => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp, depth: 1 },
|
||||||
|
stubs: { routerLink: createChildRenderingRouterLinkStub() },
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementWithSelector = wrapper.find(`.depth-1`);
|
||||||
|
|
||||||
|
expect(elementWithSelector.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle the favorite icon appropriately', () => {
|
||||||
|
it('should show favorite icon if mouse is over and type is favorite', async() => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const aElement = wrapper.find(`a`);
|
||||||
|
|
||||||
|
aElement.trigger('mouseenter');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
const favoriteElement = wrapper.find(`.${ favoriteClass }`);
|
||||||
|
|
||||||
|
expect(favoriteElement.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show favorite icon if mouse is not over and type is favorite', async() => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const favoriteElement = wrapper.find(`.${ favoriteClass }`);
|
||||||
|
|
||||||
|
expect(favoriteElement.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show favorite icon if mouse is over and type is not favorite', async() => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: { ...defaultRouteTypeProp, mode: null } },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const aElement = wrapper.find(`a`);
|
||||||
|
|
||||||
|
aElement.trigger('mouseenter');
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
const favoriteElement = wrapper.find(`.${ favoriteClass }`);
|
||||||
|
|
||||||
|
expect(favoriteElement.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle count appropriately', () => {
|
||||||
|
it('should show count if on type', async() => {
|
||||||
|
const count = 2;
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: { ...defaultRouteTypeProp, count } },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeCount = wrapper.find(`span[data-testid="type-count"]`);
|
||||||
|
|
||||||
|
expect(Number.parseInt(typeCount.text())).toBe(count);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show count if in store', async() => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeCount = wrapper.find(`span[data-testid="type-count"]`);
|
||||||
|
|
||||||
|
expect(Number.parseInt(typeCount.text())).toBe(defaultCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show count if not in type or store', async() => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: defaultRouteTypeProp },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
mocks: {
|
mocks: {
|
||||||
$route: { path: 'whatever' },
|
|
||||||
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
|
||||||
$store: {
|
$store: {
|
||||||
getters: {
|
getters: {
|
||||||
currentStore: () => 'cluster',
|
currentStore: () => 'cluster',
|
||||||
'cluster/count': () => 1,
|
'cluster/count': () => null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeCount = wrapper.find(`span[data-testid="type-count"]`);
|
||||||
|
|
||||||
|
expect(typeCount.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle namespace appropriately', () => {
|
||||||
|
it('should show namespace if on type', async() => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: { ...defaultRouteTypeProp, namespaced: true } },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const namespaced = wrapper.find(`i[data-testid="type-namespaced"]`);
|
||||||
|
|
||||||
|
expect(namespaced.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show namespace if not on type', async() => {
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
localVue,
|
||||||
|
propsData: { type: { ...defaultRouteTypeProp, namespaced: false } },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
|
},
|
||||||
|
mocks: { $store: storeMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
const namespaced = wrapper.find(`i[data-testid="type-namespaced"]`);
|
||||||
|
|
||||||
|
expect(namespaced.exists()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('testing link type', () => {
|
||||||
|
it('should show the link type element if link is present on type with the specified label and target', () => {
|
||||||
|
const defaultLinkTypeProp = {
|
||||||
|
link: 'link-type-link',
|
||||||
|
label: 'link-type-label',
|
||||||
|
target: 'link-type-target'
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = shallowMount(Type as any, {
|
||||||
|
propsData: { type: defaultLinkTypeProp },
|
||||||
|
stubs: {
|
||||||
|
routerLink: createChildRenderingRouterLinkStub(),
|
||||||
|
Favorite: { template: `<div class=${ favoriteClass } />` }
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const highlight = wrapper.find(`.${ className }`);
|
const link = wrapper.find(`li[data-testid="link-type"]`);
|
||||||
|
|
||||||
expect(highlight.exists()).toBe(false);
|
expect(link.text()).toBe(defaultLinkTypeProp.label);
|
||||||
});
|
expect(link.find('a').attributes('target')).toBe(defaultLinkTypeProp.target);
|
||||||
|
|
||||||
it('given no path', () => {
|
|
||||||
const wrapper = mount(Type, {
|
|
||||||
propsData: { type: { route: 'something else' } },
|
|
||||||
stubs: { nLink: RouterLinkStub },
|
|
||||||
mocks: {
|
|
||||||
$route: { hash: 'whatever' },
|
|
||||||
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
|
||||||
$store: {
|
|
||||||
getters: {
|
|
||||||
currentStore: () => 'cluster',
|
|
||||||
'cluster/count': () => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const highlight = wrapper.find(`.${ className }`);
|
|
||||||
|
|
||||||
expect(highlight.exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('given no matching values', () => {
|
|
||||||
const wrapper = mount(Type, {
|
|
||||||
propsData: { type: {} },
|
|
||||||
stubs: { nLink: RouterLinkStub },
|
|
||||||
mocks: {
|
|
||||||
$route: {
|
|
||||||
hash: 'hash',
|
|
||||||
path: 'path',
|
|
||||||
},
|
|
||||||
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const highlight = wrapper.find(`.${ className }`);
|
|
||||||
|
|
||||||
expect(highlight.exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('given navigation path is bigger than current page route path', () => {
|
|
||||||
const wrapper = mount(Type, {
|
|
||||||
propsData: { type: { route: 'not empty' } },
|
|
||||||
stubs: { nLink: RouterLinkStub },
|
|
||||||
mocks: {
|
|
||||||
$route: {
|
|
||||||
hash: 'not empty',
|
|
||||||
path: 'whatever',
|
|
||||||
},
|
|
||||||
$router: { resolve: () => ({ route: { path: 'many/parts' } }) },
|
|
||||||
$store: {
|
|
||||||
getters: {
|
|
||||||
currentStore: () => 'cluster',
|
|
||||||
'cluster/count': () => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const highlight = wrapper.find(`.${ className }`);
|
|
||||||
|
|
||||||
expect(highlight.exists()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
// URL with fragments like anchors
|
|
||||||
[
|
|
||||||
'/c/c-m-hzqf4tqt/explorer/members#project-membership',
|
|
||||||
'/c/c-m-hzqf4tqt/explorer/members'
|
|
||||||
],
|
|
||||||
// Similar paths
|
|
||||||
[
|
|
||||||
'/c/c-m-hzqf4tqt/fleet/fleet.cattle.io.bundlenamespacemapping',
|
|
||||||
'/c/c-m-hzqf4tqt/fleet/fleet.cattle.io.bundle'
|
|
||||||
],
|
|
||||||
// paths with same parts, e.g. parents
|
|
||||||
[
|
|
||||||
'/c/c-m-hzqf4tqt/fleet',
|
|
||||||
'/c/c-m-hzqf4tqt/fleet/management.cattle.io.fleetworkspace'
|
|
||||||
],
|
|
||||||
])('given different current path %p and menu path %p', (currentPath, menuPath) => {
|
|
||||||
const wrapper = mount(Type, {
|
|
||||||
propsData: { type: { route: 'not empty' } },
|
|
||||||
stubs: { nLink: RouterLinkStub },
|
|
||||||
mocks: {
|
|
||||||
$route: {
|
|
||||||
hash: 'not empty',
|
|
||||||
path: currentPath,
|
|
||||||
},
|
|
||||||
$router: { resolve: () => ({ route: { path: menuPath } }) },
|
|
||||||
$store: {
|
|
||||||
getters: {
|
|
||||||
currentStore: () => 'cluster',
|
|
||||||
'cluster/count': () => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const highlight = wrapper.find(`.${ className }`);
|
|
||||||
|
|
||||||
expect(highlight.exists()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should use highlight class', () => {
|
|
||||||
it.each([
|
|
||||||
[
|
|
||||||
'same',
|
|
||||||
'same'
|
|
||||||
],
|
|
||||||
])('given same current path %p and menu path %p (on first load)', (currentPath, menuPath) => {
|
|
||||||
const wrapper = mount(Type, {
|
|
||||||
propsData: { type: { route: 'not empty' } },
|
|
||||||
stubs: { nLink: RouterLinkStub },
|
|
||||||
mocks: {
|
|
||||||
$route: {
|
|
||||||
hash: 'not empty',
|
|
||||||
path: currentPath,
|
|
||||||
},
|
|
||||||
$router: { resolve: () => ({ route: { path: menuPath } }) },
|
|
||||||
$store: {
|
|
||||||
getters: {
|
|
||||||
currentStore: () => 'cluster',
|
|
||||||
'cluster/count': () => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const highlight = wrapper.find(`.${ className }`);
|
|
||||||
|
|
||||||
expect(highlight.exists()).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { RouterLinkStub } from '@vue/test-utils';
|
||||||
|
import { NavigationFailure, Route } from 'vue-router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link RouterLinkSlotArgument} in vue-router
|
||||||
|
*/
|
||||||
|
export interface RouterLinkSlotArgumentOptional {
|
||||||
|
href?: string;
|
||||||
|
route?: Route;
|
||||||
|
navigate?: (e?: MouseEvent) => Promise<undefined | NavigationFailure>;
|
||||||
|
isActive?: boolean;
|
||||||
|
isExactActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a workaround because VueUtils RouterLinkStub doesn't currently support the slot api.
|
||||||
|
*
|
||||||
|
* See @link https://github.com/vuejs/vue-test-utils/issues/1803#issuecomment-940884170
|
||||||
|
*
|
||||||
|
* @param slotProps Provide arguments that you want passed to the child rendered by router-link
|
||||||
|
* @returns A stub
|
||||||
|
*/
|
||||||
|
export function createChildRenderingRouterLinkStub(slotProps?: RouterLinkSlotArgumentOptional): typeof RouterLinkStub | any {
|
||||||
|
return {
|
||||||
|
...RouterLinkStub,
|
||||||
|
render() {
|
||||||
|
return this.$scopedSlots.default({
|
||||||
|
href: slotProps?.href || '',
|
||||||
|
route: slotProps?.route || ({} as any),
|
||||||
|
navigate: slotProps?.navigate || (() => {}),
|
||||||
|
isActive: slotProps?.isActive || false,
|
||||||
|
isExactActive: slotProps?.isExactActive || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue