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 -->
|
||||
<router-link
|
||||
v-if="showClusterTools"
|
||||
tag="div"
|
||||
class="tools"
|
||||
v-slot="{ href, navigate }"
|
||||
custom
|
||||
:to="{name: 'c-cluster-explorer-tools', params: {cluster: clusterId}}"
|
||||
>
|
||||
<a
|
||||
class="tools-button"
|
||||
@click="collapseAll()"
|
||||
<div
|
||||
class="tools"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
>
|
||||
<i class="icon icon-gear" />
|
||||
<span>{{ t('nav.clusterTools') }}</span>
|
||||
</a>
|
||||
<a
|
||||
class="tools-button"
|
||||
:href="href"
|
||||
@click="collapseAll()"
|
||||
>
|
||||
<i class="icon icon-gear" />
|
||||
<span>{{ t('nav.clusterTools') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</router-link>
|
||||
<!-- SideNav footer area (seems to be tied to harvester) -->
|
||||
<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>
|
||||
<router-link
|
||||
v-if="showPreferencesLink"
|
||||
tag="li"
|
||||
v-slot="{ href, navigate }"
|
||||
custom
|
||||
: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
|
||||
v-if="showAccountAndApiKeyLink"
|
||||
tag="li"
|
||||
v-slot="{ href, navigate }"
|
||||
custom
|
||||
: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
|
||||
v-if="authEnabled"
|
||||
tag="li"
|
||||
v-slot="{ href, navigate }"
|
||||
custom
|
||||
: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>
|
||||
</ul>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -28,65 +28,10 @@ export default {
|
|||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
near: false,
|
||||
over: false,
|
||||
menuPath: this.type.route ? this.$router.resolve(this.type.route)?.route?.path : undefined,
|
||||
};
|
||||
return { near: false };
|
||||
},
|
||||
|
||||
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() {
|
||||
return ( this.type.mode && this.near && showFavoritesFor.includes(this.type.mode) );
|
||||
},
|
||||
|
|
@ -116,14 +61,6 @@ export default {
|
|||
this.near = val;
|
||||
},
|
||||
|
||||
setOver(val) {
|
||||
this.over = val;
|
||||
},
|
||||
|
||||
removeFavorite() {
|
||||
this.$store.dispatch('type-map/removeFavorite', this.type.name);
|
||||
},
|
||||
|
||||
selectType() {
|
||||
// Prevent issues if custom NavLink is used #5047
|
||||
if (this.type?.route) {
|
||||
|
|
@ -142,63 +79,70 @@ export default {
|
|||
<router-link
|
||||
v-if="type.route"
|
||||
:key="type.name"
|
||||
v-slot="{ href, navigate, isActive, isExactActive }"
|
||||
custom
|
||||
:to="type.route"
|
||||
tag="li"
|
||||
class="child nav-type"
|
||||
:class="{'root': isRoot, [`depth-${depth}`]: true, 'router-link-active': isCurrent}"
|
||||
:exact="type.exact"
|
||||
>
|
||||
<TabTitle
|
||||
v-if="$router.resolve(type.route).route.path === $route.path"
|
||||
:show-child="false"
|
||||
<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"
|
||||
>
|
||||
{{ type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label) }}
|
||||
</TabTitle>
|
||||
<a
|
||||
@click="selectType"
|
||||
@mouseenter="setNear(true)"
|
||||
@mouseleave="setNear(false)"
|
||||
>
|
||||
<span
|
||||
v-if="type.labelKey"
|
||||
class="label"
|
||||
><t :k="type.labelKey" /></span>
|
||||
<span
|
||||
v-else
|
||||
v-clean-html="type.labelDisplay || type.label"
|
||||
class="label"
|
||||
:class="{'no-icon': !type.icon}"
|
||||
/>
|
||||
<span
|
||||
v-if="showFavorite || namespaceIcon || showCount"
|
||||
class="count"
|
||||
<TabTitle
|
||||
v-if="isExactActive"
|
||||
:show-child="false"
|
||||
>
|
||||
<Favorite
|
||||
v-if="showFavorite"
|
||||
:resource="type.name"
|
||||
/>
|
||||
<i
|
||||
v-if="namespaceIcon"
|
||||
class="icon icon-namespace"
|
||||
:class="{'ns-and-icon': showCount}"
|
||||
{{ type.labelKey ? t(type.labelKey) : (type.labelDisplay || type.label) }}
|
||||
</TabTitle>
|
||||
<a
|
||||
:href="href"
|
||||
@click="selectType"
|
||||
@mouseenter="setNear(true)"
|
||||
@mouseleave="setNear(false)"
|
||||
>
|
||||
<span
|
||||
v-if="type.labelKey"
|
||||
class="label"
|
||||
><t :k="type.labelKey" /></span>
|
||||
<span
|
||||
v-else
|
||||
v-clean-html="type.labelDisplay || type.label"
|
||||
class="label"
|
||||
:class="{'no-icon': !type.icon}"
|
||||
/>
|
||||
<span
|
||||
v-if="showCount"
|
||||
>{{ count }}</span>
|
||||
</span>
|
||||
</a>
|
||||
v-if="showFavorite || namespaceIcon || showCount"
|
||||
class="count"
|
||||
>
|
||||
<Favorite
|
||||
v-if="showFavorite"
|
||||
:resource="type.name"
|
||||
/>
|
||||
<i
|
||||
v-if="namespaceIcon"
|
||||
class="icon icon-namespace"
|
||||
:class="{'ns-and-icon': showCount}"
|
||||
data-testid="type-namespaced"
|
||||
/>
|
||||
<span
|
||||
v-if="showCount"
|
||||
data-testid="type-count"
|
||||
>{{ count }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
<li
|
||||
v-else-if="type.link"
|
||||
class="child nav-type"
|
||||
data-testid="link-type"
|
||||
>
|
||||
<a
|
||||
:href="type.link"
|
||||
:target="type.target"
|
||||
rel="noopener noreferrer nofollow"
|
||||
@click="selectType"
|
||||
@mouseenter="setNear(true)"
|
||||
@mouseleave="setNear(false)"
|
||||
>
|
||||
<span class="label">{{ type.label }} <i class="icon icon-external-link" /></span>
|
||||
</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 { createChildRenderingRouterLinkStub } from '@shell/utils/unit-tests/ChildRenderingRouterLinkStub';
|
||||
import { TYPE_MODES } from '@shell/store/type-map';
|
||||
|
||||
// Mandatory to mock vue-router in this test
|
||||
jest.mock('vue-router');
|
||||
|
||||
// Configuration text
|
||||
const className = 'router-link-active';
|
||||
// Configuration
|
||||
const activeClass = 'router-link-active';
|
||||
const exactActiveClass = 'router-link-exact-active';
|
||||
const rootClass = 'root';
|
||||
const favoriteClass = 'favorite';
|
||||
|
||||
describe('component: Type', () => {
|
||||
describe('should not use highlight class', () => {
|
||||
it('given no hash', () => {
|
||||
const wrapper = mount(Type, {
|
||||
propsData: { type: { route: 'something else' } },
|
||||
stubs: { nLink: RouterLinkStub },
|
||||
mocks: {
|
||||
$route: { path: 'whatever' },
|
||||
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
||||
$store: {
|
||||
getters: {
|
||||
currentStore: () => 'cluster',
|
||||
'cluster/count': () => 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
describe('testing router-link type', () => {
|
||||
const localVue = createLocalVue();
|
||||
|
||||
localVue.directive('cleanHtml', (identity) => identity);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
const highlight = wrapper.find(`.${ className }`);
|
||||
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 }
|
||||
});
|
||||
|
||||
expect(highlight.exists()).toBe(false);
|
||||
});
|
||||
const elementWithSelector = wrapper.find(`a[href='${ fakeHref }']`);
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
},
|
||||
expect(elementWithSelector.exists()).toBe(true);
|
||||
});
|
||||
|
||||
const highlight = wrapper.find(`.${ className }`);
|
||||
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 }
|
||||
});
|
||||
|
||||
expect(highlight.exists()).toBe(false);
|
||||
const elementWithSelector = wrapper.find(`.${ activeClass }`);
|
||||
|
||||
elementWithSelector.trigger('click');
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('given no matching values', () => {
|
||||
const wrapper = mount(Type, {
|
||||
propsData: { type: {} },
|
||||
stubs: { nLink: RouterLinkStub },
|
||||
mocks: {
|
||||
$route: {
|
||||
hash: 'hash',
|
||||
path: 'path',
|
||||
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 } />` }
|
||||
},
|
||||
$router: { resolve: () => ({ route: { path: 'whatever' } }) },
|
||||
},
|
||||
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);
|
||||
});
|
||||
|
||||
const highlight = wrapper.find(`.${ className }`);
|
||||
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 }
|
||||
});
|
||||
|
||||
expect(highlight.exists()).toBe(false);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
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 } />` }
|
||||
},
|
||||
$router: { resolve: () => ({ route: { path: 'many/parts' } }) },
|
||||
$store: {
|
||||
getters: {
|
||||
currentStore: () => 'cluster',
|
||||
'cluster/count': () => 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
mocks: { $store: storeMock }
|
||||
});
|
||||
|
||||
const typeCount = wrapper.find(`span[data-testid="type-count"]`);
|
||||
|
||||
expect(Number.parseInt(typeCount.text())).toBe(count);
|
||||
});
|
||||
|
||||
const highlight = wrapper.find(`.${ className }`);
|
||||
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 }
|
||||
});
|
||||
|
||||
expect(highlight.exists()).toBe(false);
|
||||
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: {
|
||||
|
||||
$store: {
|
||||
getters: {
|
||||
currentStore: () => 'cluster',
|
||||
'cluster/count': () => null,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const typeCount = wrapper.find(`span[data-testid="type-count"]`);
|
||||
|
||||
expect(typeCount.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,
|
||||
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 } />` }
|
||||
},
|
||||
$router: { resolve: () => ({ route: { path: menuPath } }) },
|
||||
$store: {
|
||||
getters: {
|
||||
currentStore: () => 'cluster',
|
||||
'cluster/count': () => 1,
|
||||
}
|
||||
}
|
||||
},
|
||||
mocks: { $store: storeMock }
|
||||
});
|
||||
|
||||
const namespaced = wrapper.find(`i[data-testid="type-namespaced"]`);
|
||||
|
||||
expect(namespaced.exists()).toBe(true);
|
||||
});
|
||||
|
||||
const highlight = wrapper.find(`.${ className }`);
|
||||
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 }
|
||||
});
|
||||
|
||||
expect(highlight.exists()).toBe(false);
|
||||
const namespaced = wrapper.find(`i[data-testid="type-namespaced"]`);
|
||||
|
||||
expect(namespaced.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,
|
||||
}
|
||||
}
|
||||
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(true);
|
||||
expect(link.text()).toBe(defaultLinkTypeProp.label);
|
||||
expect(link.find('a').attributes('target')).toBe(defaultLinkTypeProp.target);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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