I have a TitleComponent that uses ActivedRoute.snapshot.data and Router to update data from app-routing.
This is my app-routing.module
export const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: 'home',
component: MainHomeComponent,
data: {
title: 'Home',
}
},
];
#NgModule({
imports: [RouterModule.forRoot(routes, {
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
scrollOffset: [0, 64] // [x, y]
} )],
exports: [RouterModule]
})
export class AppRoutingModule { }
and this is my TitleComponent.ts
export class TitleComponent implements OnInit {
titleTab: string;
subTitle: string;
isTablet$: Subscribable<boolean> = this.breakpointsService.isTablet$;
constructor(private router: Router, private route: ActivatedRoute, private breakpointsService: BreakpointsService) {
this.router.events.subscribe((data) => {
if (data instanceof NavigationEnd) {
console.log('title is= ',this.route.snapshot.data['title']);
// console.log(this.route.root.firstChild.snapshot.data.subTitle);
this.titleTab = this.route.snapshot.data['title'];
if (this.route.snapshot.data['subTitle']) {
this.subTitle = this.route.snapshot.data['subTitle'];
} else {
this.subTitle = '';
}
}
});
}
ngOnInit() {
}
}
Now i want to test my TitleComponent. For example, when i navigate to home, i want to have titleTab === Home or Actived.snapshot.data['title'] = Home
How do it? I tried many ways buti always have errors.
My spec.ts
fdescribe('TitleComponent', () => {
let component: TitleComponent;
let fixture: ComponentFixture<TitleComponent>;
let location: Location;
let router: Router;
// let route: ActivatedRoute;
const mockActivedRoute = jasmine.createSpyObj('ActivatedRoute', ['get']);
const testRouter = jasmine.createSpyObj('Router', ['get'])
const route = ({ data: of({ title: 'home' }) } as any) as ActivatedRoute;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
TitleComponent,
HomeComponent,
SearchComponent,
AppComponent,
MainHomeComponent,
],
providers: [
{
provide: ActivatedRoute,
useValue: {
snapshot: {
data: { title: 'home' }
}
},
}
],
imports: [RouterTestingModule.withRoutes(routes)],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
router = TestBed.get(Router);
location = TestBed.get(Location);
router.initialNavigation();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TitleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// It doesn't works with mock
it('test home data', fakeAsync(() => {
router.navigate(['/home']);
tick();
expect(route.snapshot.data.title).toBe('title');
}));
// It doesn't works with component
//it('test ', fakeAsync(() => {
//router.navigate(['/home']);
//tick();
//expect(component.titleTab).toBe('home');
// }));
Help me please.
Router navigation is asynchronous, so my guess is that you should wait for router event to complete. Here is what I would use:
it('should....', (done) => {
router.navigate(['/home'])
.then(() => {
fixture.detectChanges();
expect(....);
done();
})
.catch(err => fail(err));
});
Related
I'm building a small e-commerce store with an admin panel for myself.
I use Firebase firestore as my backend to store all the user's data.
I have a root 'users' collection with a document for every single registered user and everything else each user has is branching out of the user doc.
Here are firestore commands i perform so you understand the structure better.
db.collection('users').doc(userId).collection('categories').doc(subCategoryId)...
db.collection('users').doc(userId).collection('subcategories').doc(subCategoryId)...
I use Vuex so every time i need to change something on my firestore (update a product category, remove a category etc.), i dispatch an appropriate action.
The first thing any of those actions does is to go ahead and dispatch another action from auth.js that gets the userId.
The problem is that if the action in question should run in a mounted() lifecycle hook, then it fails to grab the userId.
In EditCategory.vue updateCategory action works perfectly well because SubmitHandler() is triggered on click event but in Categories.vue the fetchCategories does not work and spit out an error:
[Vue warn]: Error in mounted hook (Promise/async): "FirebaseError: [code=invalid-argument]: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null"
Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null
Which, as far as i understand it, basically tells me that fetchCategories() action's firestore query could not be performed because the userId was not recieved.
After two days of moving stuff around i noticed that errors only occur if i refresh the page. If i switch to other tab and back on without refreshing, then fetchCategories() from Categories.vue mounted() hook works. Placing the code in to created() hook gives the same result.
I think that there is some fundamental thing i am missing about asynchronous code and lifecycle hooks.
Categories.vue component
<template>
<div class="category-main">
<section>
<div class="section-cols" v-if="!loading">
<EditCategory
v-on:updatedCategory="updatedCategory"
v-bind:categories="categories"
v-bind:key="categories.length + updateCount"
/>
</div>
</section>
</div>
</template>
<script>
import EditCategory from '#/components/admin/EditCategory.vue'
export default {
name: 'AdminCategories',
components: {
EditCategory,
},
data: () => ({
updateCount: 0,
loading: true,
categories: [],
}),
async mounted() {
this.categories = await this.$store.dispatch('fetchCategories');// FAILS!
this.loading = false;
},
methods: {
addNewCategory(category) {
this.categories.push(category);
},
updatedCategory(category) {
const catIndex = this.categories.findIndex(c => c.id === category.id);
this.categories[catIndex].title = category.title;
this.categories[catIndex].path = category.path;
this.updateCount++;
}
}
}
</script>
category.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
export default {
state: {
test: 10,
categories: [],
subCategories: [],
currentCategory: '',
},
mutations: {
setCategories(state, payload){
state.categories = payload;
},
},
actions: {
async fetchCategories({commit, dispatch}) {
try {
const userId = await dispatch('getUserId');
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},
async updateCategory({commit, dispatch}, {title, path, id}) {
try {
const userId = await dispatch('getUserId');
console.log('[category.js] updateCategory', userId);
await db.collection('users').doc(userId).collection('categories').doc(id).update({
title,
path
})
commit('rememberCurrentCategory', id);
return;
} catch (err) {throw err;}
}
},
}
auth.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
...async login(), async register(), async logout()
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
},
}
index.js store file
import Vue from 'vue'
import Vuex from 'vuex'
import auth from './auth'
import products from './products'
import info from './info'
import category from './category'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
auth, products, info, category,
}
})
EditCategory.vue
export default {
name: 'EditCategory',
data: () => ({
select: null,
title: '',
path: '',
current: null
}),
props: {
categories: {
type: Array,
required: true
}
},
methods: {
async submitHandler() {
if (this.$v.invalid){
this.$v.$touch()
return;
}
try {
const categoryData = {
id : this.current,
title: this.title,
path: this.path
};
await this.$store.dispatch('updateCategory', categoryData);// WORKS!
this.$emit('updatedCategory', categoryData);
} catch (err) { throw err; }
},
},
//takes current category id from store getter
computed: {
categoryFromState() {
return this.$store.state.currentCategory;
}
},
created() {
console.log('[EditCategory.vue'], currentCategory);
},
mounted(){
this.select = M.FormSelect.init(this.$refs.select);
M.updateTextFields();
},
destroyed() {
if (this.select && this.select.destroy) {
this.select.destroy;
}
}
}
</script>
First of all, it's just a small detail, but you don't need need to make your 'getUserId' action async, since it does not use the 'await' keyword. So can simplify this :
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = await dispatch('getUserId')
into this :
getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = dispatch('getUserId')
Coming back to your id that seems to be undefined, the problem here is that your 'mounted' event is probably triggered before the 'login' can be completed.
How to solve this case ? Actually, there are a lot of different ways to approch this. What I suggest in your case is to use a middleware (or a 'route guard'). This guard can make you are verified user before accessing some routes (and eventually restrict the access or redirect depending on the user privileges). In this way, you can make sure that your user is defined before accessing the route.
This video is 4 years old so it is not up to date with the last versions of Firebas. But I suggest The Net Ninja tutorial about Vue Route Guards with Firebase if you want to learn more about this topic.
Accepted answer actually pointed me to the correct direction.
In my case i had to make a route guard for child routes.
router.vue
import Vue from 'vue'
import Router from 'vue-router'
import firebase from 'firebase/app';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Home.vue')
},
{
path: '/bouquets',
name: 'bouquets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Bouquets.vue')
},
{
path: '/sets',
name: 'sets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Sets.vue')
},
{
path: '/cart',
name: 'cart',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Cart.vue')
},
{
path: '/login',
name: 'login',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Login.vue')
},
{
path: '/register',
name: 'register',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Register.vue')
},
{
path: '/admin',
name: 'admin',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Home.vue'),
children: [
{
path: 'categories',
name: 'adminCategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Categories'),
},
{
path: 'subcategories',
name: 'adminSubcategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/SubCategories'),
},
{
path: 'products',
name: 'adminProducts',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Products'),
},
]
},
{
path: '/checkout',
name: 'checkout',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Checkout.vue')
},
{
path: '/:subcategory',
name: 'subcategory',
meta: {layout: 'main-layout'},
props: true,
params: true,
component: () => import('./views/main/Subcategory.vue')
},
]
})
router.beforeEach((to, from, next) => {
//if currentUser exists then user is logged in
const currentUser = firebase.auth().currentUser;
//does a route has auth == true
const requireAuth = to.matched.some(record => record.meta.auth);
//if auth is required but user is not authentificated than redirect to login
if (requireAuth && !currentUser) {
// next('/login?message=login');
next('login')
} else {
next();
}
})
export default router;
category.js fetchCategories() action
async fetchCategories({commit, dispatch}) {
const userId = await dispatch('getUserId')
try {
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},
I am experiencing the following issue - once the user is logged in, and onMounted event is finished, SocketIO client side should join the room. But this doesn't happen for some reason. I have to manually refresh browser in order to join the room. What am I doing wrong here?
I have the following code for the SocketIO on the client side:
import { io } from "socket.io-client";
const token = window.localStorage.getItem('TOKEN') || window.sessionStorage.getItem('TOKEN')
console.log(token);
const ioSocket = io('https://dolphin-app-e4ozn.ondigitalocean.app', {
withCredentials: true,
transportOptions: {
polling: {
extraHeaders: {
'authorization': `${token}`,
},
},
},
});
export const socket = ioSocket
The vue 3 router logic:
import { createRouter, createWebHistory } from 'vue-router'
import Landing from '#/views/Landing.vue'
import Login from '#/views/login/Login.vue'
import ResetPassword from '#/views/login/ResetPassword.vue'
import ForgotPassword from '#/views/login/ForgotPassword.vue'
const routes = [
{
path: '/login',
name: 'login',
component: Login,
meta: {
isGuest: true,
title: 'Servant | Login'
}
},
{
path: '/resetPassword',
name: 'resetPassword',
component: ResetPassword,
meta: {
isGuest: true,
title: 'Servant | Reset Password'
}
},
{
path: '/forgotPassword',
name: 'forgotPassword',
component: ForgotPassword,
meta: {
isGuest: true,
title: 'Servant | Forgot Password'
}
},
{
path: '/',
name: 'landing',
component: Landing,
meta: {
requiresAuth: true,
title: 'Servant',
role: 'waiter'
}
},
{
path: '/:pathMatch(.*)*',
component: Landing
},
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior() {
// always scroll to top
return { top: 0 }
},
})
router.beforeEach((to, from, next) => {
document.title = to.meta.title || "Servant"
let token = window.localStorage.getItem('TOKEN') || window.sessionStorage.getItem('TOKEN')
if(to.meta.requiresAuth && !token)
{
next({name: 'login'})
}
if (token && to.meta.isGuest )
{
next({ name: 'landing' })
}
next();
});
export default router
Login component logic:
function login() {
loading.value = true
formClass = ''
if (user.remember)
{
window.localStorage.setItem('remember', user.remember)
}
mainStore
.login(user)
.then((response) => {
loading.value = false
router.push({
name: 'landing',
})
})
.catch((err) => {
loading.value = false
errorMsg.value = err.response.data.messages.error
formClass = 'was-validated'
})
}
Once the component is mounter I have following logic:
onMounted(() => {
socket.emit("join", import.meta.env.VITE_SOCKET_ROOM, (message) => {
console.log(message);
});
})
On the SocketIO server side I have following logic:
io.use((socket, next) => {
const header = socket.handshake.headers["authorization"];
if(header !== 'null')
{
jwtVerify(header, secret).then((res) => {
if (res === true) {
const jwt = jwtDecode(header);
servantID = jwt.payload.iss;
return next();
}
return next(new Error("authentication error"));
});
}
});
In my Angular 13 Ionic 6 I am attempting to navigate to the "home" page.
Running on Android.
Here's the component code (signup) that is calling the navigation (abbreviated):
import { Component, OnInit, NgZone } from '#angular/core';
import { Router } from '#angular/router';
constructor(
private route: Router,
private zone: NgZone,
) { }
routeBasedOnRole(role: number) {
console.log('Routing based on role: ', role);
this.route
.routeReuseStrategy
.shouldReuseRoute = function () {
return false;
};
if (role==2) {
//Helper
this.zone.run(() => {
this.route.navigate(['/my-invitations']);
})
} else {
//Learner or both
console.log('Routing to home now');
this.zone.run(() => {
this.route.navigate(['/home'])
.then(nav => {
console.log(nav); // Always returns true
}, err => {
console.log(err)
});
})
}
}
In the app-routing.module.ts routes array, for the 'home' path, when I route to the:
component: HomePage
The routing is executed OK.
However, when I lazy load the route:
loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)
...the screen does not route - it stays in the calling component.
The route.navigate() promise always returns true regardless.
Here's the app-routing.module.ts:
import { NgModule, Component } from '#angular/core';
import { CommonModule } from '#angular/common';
import { PreloadAllModules, RouterModule, Routes } from '#angular/router';
import { HomePage } from './home/home.page';
import { SignupPage } from './signup/signup.page';
import { HelpersPage } from './helpers/helpers.page';
import { AddHelperPage } from './helpers/add-helper/add-helper.page';
import { MyInvitationsPage } from './my-invitations/my-invitations.page';
import { InvitationsResponsesPage } from './invitations-responses/invitations-responses.page';
import { ChatPage } from './chat/chat.page';
import { SettingsPage } from './settings/settings.page';
import { FeedbackPage } from './feedback/feedback.page';
const routes: Routes = [
{
path: 'home',
//loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)
component: HomePage
},
{
path: 'signup',
loadChildren: () => import('./signup/signup.module').then( m => m.SignupPageModule)
//component: SignupPage
},
{
path: 'helpers',
loadChildren: () => import('./helpers/helpers.module').then( m => m.HelpersPageModule)
},
{
path: 'add-helper',
loadChildren: () => import('./helpers/add-helper/add-helper.module').then( m => m.AddHelperPageModule)
},
{
path: 'my-invitations',
loadChildren: () => import('./my-invitations/my-invitations.module').then( m => m.MyInvitationsPageModule)
},
{
path: 'chat',
loadChildren: () => import('./chat/chat.module').then( m => m.ChatPageModule)
},
{
path: 'invitations-responses',
loadChildren: () => import('./invitations-responses/invitations-responses.module').then( m => m.InvitationsResponsesPageModule)
},
{
path: 'settings',
loadChildren: () => import('./settings/settings.module').then( m => m.SettingsPageModule)
},
{
path: 'feedback',
loadChildren: () => import('./feedback/feedback.module').then( m => m.FeedbackPageModule)
},
];
#NgModule({
declarations: [
HomePage,
SignupPage,
HelpersPage,
AddHelperPage,
MyInvitationsPage,
InvitationsResponsesPage,
ChatPage,
SettingsPage
],
imports: [
CommonModule,
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule { }
Btw, the signup page that is calling for the route, is itself lazy-loaded OK from the app.component using the same route.navigate() call. It seems that maybe subsequent routings to lazy-loaded pages are not executing?
Have looked through many similar questions - to no avail.
What am I missing?
I am writing the angular (Karma-Jasmine) test cases and I want to navigate between the pages. How to navigate between pages using karma-Jasmine.
1) Test a component in which navigation is used: navigate method should be called when you do an action (assertion like toHaveBeenCalled OR toHaveBeenCalledWith)
it('should redirect the user to the users page after saving', () => {
let router = TestBed.get(Router);
let spy = spyOn(router, 'navigate');
component.save();
expect(spy).toHaveBeenCalledWith(['users'])
});
2) Also test your routes that proper component will be used
app.routes.spec.ts
import { routes } from './app.routes'
it('should contain a route for users', () => {
expect(routes).toContain({path: 'users', component: UserComponent})
});
3) You can use useValue for testing different activatedRouteParams (just configure then and pass to providers, see example).
Component ts file example:
ngOnInit() {
this.contextType = this.route.snapshot.paramMap.get('contextType');
this.contextId = this.route.snapshot.paramMap.get('contextId');
this.contextFriendlyId = this.route.snapshot.paramMap.get('contextFriendlyId');
}
Spec file (configureTestData is a function that allows you to pass different configurable values, in my case activatedRouteParams)
export function configureTestData(activatedRouteParams) {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SetUpComponent],
imports: [RouterTestingModule],
providers: [
{
provide: ActivatedRoute, useValue: activatedRouteParams
}
]
});
}));
}
describe('SetUp Component:Folder ', () => {
let component: SetUpComponent;
let fixture: ComponentFixture<SetUpComponent>;
configureTestData({
snapshot: {
paramMap: convertToParamMap({
contextType: 'Folder',
contextId: 'FX6C3F2EDE-DB25-BC3D-0F16-D984753C9D2E',
contextFriendlyId: 'FX17373'
})
}
});
beforeEach(() => {
fixture = TestBed.createComponent(SetUpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create set up component for folder', () => {
expect(component).toBeTruthy();
});
it('should create alert with required properties', () => {
expect(component.contextType).toEqual('Folder);
.... etc
});
});
4) router-outlet and routerLink
Template file:
<nav>
<a routerLink="todos"></a>
</nav>
<router-outlet></router-outlet>
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([])],
declarations: [AppComponent]
});
});
it('should have router outlet', () => {
let de = fixture.debugElement.query(By.directive(RouterOutlet));
expect(de).not.toBeNull();
});
it('should have a link to todos page', () => {
let debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
let index = debugElements.findIndex(de => de.properties['href'] === '/todos');
expect(index).toBeGreaterThan(-1);
});
5) Stub for ActivatedRoute where we can push params
component.ts
ngOnInit() {
this.route.params.subscribe(p => {
if (p['id'] === 0) {
this.router.navigate(['not-found']);
}
});
}
Spec file:
class RouterStub {
navigate(params) {}
}
class ActivatedRouteStub {
private subject = new Subject();
get params () {
return this.subject.asObservable();
}
push(value) {
this.subject.next(value);
}
}
describe('Navigation Testing', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
{provide: Router, useClass: RouterStub},
{provide: ActivatedRoute, useClass: ActivatedRouteStub}
]
});
});
it('should navigate to invalid page when invalid params passed', () => {
let router = TestBed.get(Router);
let spy = spyOn(router, 'navigate');
let route: ActivatedRouteStub = TestBed.get(ActivatedRoute);
route.push({id: 0});
expect(spy).toHaveBeenCalledWith(['not-found'])
});
});
SO I'm trying to implement a user role/permission implementation in React using react-boilerplate. However, the routing used in here is somehow dynamic, in contrast to the standard
<Router>
<Route><Route>
</Router>
format.
Now I have seen this function onEnter of react-router and my colleague advised me that this could be of use for user roles.
So I was wondering if there's a way to access it via dynamic routing? From what I could see from the routing code snippet of react-boilerplate, there's no way I could declare it.
app/routes.js
....
return [
{
path: '/',
name: 'home',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('pages/Login'),
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
}, {
path: '/pt',
name: 'home-pt',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('pages/Home'),
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
},
childRoutes: [
{
path: '/pt/info/share',
name: 'pt-share-info',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('pages/Share'),
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
}
}, {
path: '/pt/info/request',
name: 'home-pt',
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('pages/Request'),
]);
const renderRoute = loadModule(cb);
importModules.then(([component]) => {
renderRoute(component);
});
importModules.catch(errorLoading);
}
}
]
}, {
path: '*',
name: 'notfound',
getComponent(nextState, cb) {
System.import('containers/NotFoundPage')
.then(loadModule(cb))
.catch(errorLoading);
},
},
];
PS. I have also seen this react-router-role-authorization, and is wondering if I could integrate it with the current setup of react-boilerplate?