I am having a problem where I set the "headerButtons" and "contractLoaded" component variables in my test but it does not seem to change the values. If I console out or use a debugger in the component as the test runs the variables stay as initially defined (undefined and false).
I have tried lots of different combinations but always the same result.
headerButtons is an #Input
let component: HeaderBannerComponent;
let fixture: ComponentFixture<HeaderBannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({ declarations: [HeaderBannerComponent] });
fixture = TestBed.createComponent(HeaderBannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
...
it('should display the correct amount of header buttons', () => {
const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
fixture.detectChanges();
expect(debugEl.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
fixture.detectChanges();
expect(
debugEl.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});
Component :
import { Component, Input, OnInit } from '#angular/core';
import { KeyValue } from '#angular/common';
import { ContractEventService } from 'src/app/core/services/contract-event.service';
#Component({
selector: 'app-header-banner',
templateUrl: './header-banner.component.html',
styleUrls: ['./header-banner.component.css'],
})
export class HeaderBannerComponent implements OnInit {
menubar: Map<string, string> = new Map<string, string>();
contractLoaded: boolean = false;
#Input() headerTitle: any;
#Input() headerButtons: string;
constructor(private contractEventService: ContractEventService) {}
ngOnInit(): void {
if (this.headerButtons === 'full') {
this.contractEventService.getContractLoaded().subscribe(
(rs) => {
this.contractLoaded = rs;
this.setButtons();
},
(err) => {
console.warn('failed to get contractLoaded status', err);
}
);
}
this.setButtons();
}
setButtons(): void {
this.menubar = new Map<string, string>();
this.menubar.set('Home', '/iforis/main.do');
if (this.headerButtons === 'full' && this.contractLoaded) {
this.menubar.set('Contacts', '/iforis/s/contacts/');
this.menubar.set('Notes', '/iforis/s/notes/');
}
if (this.headerButtons === 'full' && !this.contractLoaded) {
this.menubar.delete('Contacts');
this.menubar.delete('Notes');
}
this.menubar.set('Exit', '/iforis/exit.do');
}
originalOrder = (
a: KeyValue<string, string>,
b: KeyValue<string, string>
): number => {
return 0;
};
}
Template:
<div class="header-banner box-shadow">
<div class="header-banner__title">
<span id="headerTitleText" class="header-banner__text">
{{ headerTitle }}
</span>
</div>
<div class="header-banner__logo">
<img src="assets/DAFM_Logo_2018.png" />
</div>
<div class="header-banner__btn-container">
<div
*ngFor="
let button of menubar | keyvalue: originalOrder;
let first = first;
let last = last
"
[ngClass]="[
'header-banner__btn',
first ? 'header-banner__btn--first' : '',
last ? 'header-banner__btn--last' : ''
]"
>
<a href="{{ button.value }}" class="header-banner__btn-link">
{{ button.key }}
</a>
</div>
</div>
</div>
That is strange. Can you show the HTML and component typescript as well?
I think I have an idea though about the issue you could be facing.
it('should display the correct amount of header buttons', () => {
// get rid of this variable
// const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(
fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});
The issue is the debugEl. It becomes stale after you change component variables so you always need a new debugEl after changing variables. I think this is most likely the issue.
====== Edit ======
Maybe we should mock ContractEventService instead of providing the real one.
Try this:
let component: HeaderBannerComponent;
let fixture: ComponentFixture<HeaderBannerComponent>;
let mockContractEventService: jasmine.SpyObj<ContractEventService>;
beforeEach(() => {
// the first string argument is just an identifier and is optional
// the second array of strings are public methods that we need to mock
mockContractEventService = jasmine.createSpyObj<ContractEventService>('ContractEventService', ['getContractLoaded']);
TestBed.configureTestingModule({
declarations: [HeaderBannerComponent],
// provide the mock when the component asks for the real one
providers: [{ provide: ContractEventService, useValue: mockContractService }]
});
fixture = TestBed.createComponent(HeaderBannerComponent);
component = fixture.componentInstance;
// formatting is off but return a fake value for getContractLoaded
mockContractEventService.getContractLoaded.and.returnValue(of(true));
// the first fixture.detectChanges() is when ngOnInit is called
fixture.detectChanges();
});
it('should display the correct amount of header buttons', () => {
// get rid of this variable
// const debugEl: DebugElement = fixture.debugElement;
component.headerButtons = 'basic';
// The issue was that we are not calling setButtons
// manually call setButtons
component.setButtons();
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length).toEqual(
2
);
component.headerButtons = 'full';
component.contractLoaded = true;
// manually call setButtons
component.setButtons();
fixture.detectChanges();
// Change this line to be fixture.debugElement and not debugEl
expect(
fixture.debugElement.queryAll(By.css('.header-banner__btn-link')).length
).toBeGreaterThan(2);
});
Related
I'm new to Angular and I'm trying to perform a test... I want to test my function in component.ts that receives an event through click, and this value passes to an output for the component dad.
The test code looks like this...
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { QuadradosComponent } from './quadrados.component';
describe('QuadradosComponent', () => {
let component: QuadradosComponent;
let fixture: ComponentFixture<QuadradosComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ QuadradosComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(QuadradosComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('Deve emitir mensagem quando clicar na div', () => {
const emitMessageSpy = spyOn(component.eventoOutput, 'emit')
let divClick = fixture.debugElement.query(By.css('.casa')).nativeElement
divClick.click()
expect(emitMessageSpy).toHaveBeenCalled
})
});
The component.ts
import { Component, EventEmitter, Input, Output, OnInit} from '#angular/core';
import { JogadasService } from 'src/app/services/jogadas.service';
#Component({
selector: 'app-quadrados',
templateUrl: './quadrados.component.html',
styleUrls: ['./quadrados.component.css']
})
export class QuadradosComponent implements OnInit{
constructor(public service: JogadasService) {}
ngOnInit(): void {
this.service.emitirVitoria.subscribe(
array => this.vitoria.push(...array)
)
console.log(document.getElementsByClassName('casa'))
}
vitoria: number[] =[];
posicoes: number = 0;
#Input() quadrados?: Array<number>;
#Output() eventoOutput = new EventEmitter();
#Output() index = new EventEmitter();
eventoClick(evento: MouseEvent) {
this.eventoOutput.emit(evento);
console.log(document.getElementsByClassName('casa'))
}
pegaIndex(index: number) {
this.posicoes = index;
this.index.emit(this.posicoes);
}
vitoriaArray(modelo: number) {
for(let i = 0; i< this.vitoria.length; i++) {
if(this.vitoria[i] == modelo) {
return this.vitoria[i]
}
}
}
}
the component HTML:
<div class="centraliza">
<div class="jogo-da-velha">
<div class="linha">
<div class="casa" *ngFor="let quadrado of quadrados, let i = index"
(click)="pegaIndex(i)"
[ngClass]="i == vitoriaArray(i) ? 'casa-vencedor' : null"
(click)="eventoClick($event)" ></div>
</div>
</div>
</div>
I'm using Angular/Cli in version 15, I'm not getting a good understanding of the tests in Angular, thanks for the help.
div.casa is being rendered with an *ngFor and you have to make sure quadrados is a non empty array and not undefined.
Try this:
it('Deve emitir mensagem quando clicar na div', () => {
const emitMessageSpy = spyOn(component.eventoOutput, 'emit')
// Mock quadrados here
component.quadrados = [{ } as any];
// detect the changes here since the view model changed
fixture.detectChanges();
// the div should be there now
let divClick = fixture.debugElement.query(By.css('.casa')).nativeElement
divClick.click()
expect(emitMessageSpy).toHaveBeenCalled
})
Here is a good resource on learning testing with Angular: https://testing-angular.com/.
I have a simple Angular component with text input which uses ngModel binding. the component works fine, but not the unit-test.
helloworld.component.html:
<div>
<input [(ngModel)]="name" >
<p>{{msg}} {{name}}</p>
</div>
helloworld.component.ts:
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'hello-world',
templateUrl: './helloworld.component.html',
styleUrls: ['./helloworld.component.css']
})
export class HelloWorldComponent {
#Input() name: string = "World";
#Input() msg: string = "Hello";
}
helloworld.component.spec.ts:
import { async, ComponentFixture, TestBed, fakeAsync, tick } from
'#angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '#angular/core';
import { HelloWorldComponent } from './helloworld.component';
describe('HelloWorldComponent', () => {
let component: HelloWorldComponent;
let fixture: ComponentFixture<HelloWorldComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HelloWorldComponent ],
schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HelloWorldComponent);
component = fixture.componentInstance;
});
it('should print the name on input', ()=>{
component.msg = "Hi";
fixture.detectChanges();
var nameInput: HTMLInputElement = fixture.nativeElement.querySelector('input');
var label: HTMLElement = fixture.nativeElement.querySelector('p');
nameInput.value = 'John';
nameInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(component.msg).toBe('Hi');
expect(component.name).toBe('John');
expect(label.textContent).toBe('Hi John');
});
});
The test fails because name is not set to 'John' but still has the default value 'World'. The provided code is in my eyes equivalent to the example in Angular documentation (https://angular.io/guide/testing#component-binding)
There are some articles that explain to perform the test in async() and use fixture.whenStable().then() after changing the input value.
I also found the hint to run the test inside fakeAsync() and use tick() to wait for the input value to be bound to name. Both didn't help.
What am I missing?
I am trying to implement the tab view component of Prime NG. but my tabs are dynamic in nature ie.
So when the container is loaded it sends multiple AJAX requests for data inside the component.(Maybe the component is initialized multiple times?)
Another thing, in one of the components, moving mouse gives Thousands of errors on the console.
ERROR Error: Error trying to diff '[object Object]'. Only arrays and iterables are allowed
ERROR CONTEXT [object Object]
Not sure why. Used the same component in another place and there was no issue.
Even if I remove the dynamic nature of the components and just place 4 static tabs, everything works perfectly.(Right now the same 4 components are coming from server).
Html Template:
<div class="col-md-12 padding0">
<div class="tabViewWrapper">
<p-tabView (onChange)="handleChange($event)">
<p-tabPanel header="{{tab.tabName}}" *ngFor="let tab of tabs" >
<dynamic-component [componentData]="componentData"></dynamic-component>
</p-tabPanel>
</p-tabView>
<div>
</div>
Component:
#Component({
selector: 'tab-view',
templateUrl: './tab-view.component.html',
styleUrls: ['./tab-view.component.scss'],
encapsulation: ViewEncapsulation.None,
entryComponents: [GenericDataTableComponent, SingleEditCategoryExplorerComponent, AssetsDataTableComponent]
})
export class TabViewComponent implements OnInit {
private ngUnsubscribe: Subject<void> = new Subject<void>();
private componentData = null;
private tabs: Array<any>;
private index:number;
private disabledTabs:Array<any>;
private disabledTabsWhenMetaDataClicked:Array<any>;
versionConfig = {
url: AppSettingProperties.DATA_TABLE_VALUES.VERSIONS_URL,
dateLocale: AppSettingProperties.DATA_TABLE_VALUES.LOCALE,
header: AppSettingProperties.DATA_TABLE_VALUES.VERSIONS_HEADER
};
relatedConfig = {
url: AppSettingProperties.BASEURL + AppSettingProperties.DATA_TABLE_VALUES.RELATED_ENDPOINT,
header: AppSettingProperties.DATA_TABLE_VALUES.RELATED_HEADER
};
constructor(private assetDataLoadedService: AssetDataLoadedService, private assetDetailsService: AssetDetailsService, private assetDetailDataModel:AssetDetailDataModel) { }
#ViewChildren(DynamicContainerComponent) dynamicContainers: QueryList<DynamicContainerComponent>;
ngOnInit() {
this.disabledTabs = [];
//Set items to be disabled when Metadata button is clicked
this.disabledTabsWhenMetaDataClicked = [AppSettingProperties.TAB_RELATEDITEMS, AppSettingProperties.TAB_VERSIONS];
//Disable the tabs as per the condistions
this.disableTabsAsPerRequirement();
//Assigning tabs
this.tabs = this.assetDetailsService.systemTabs;
}
getInitialSelected(tab){
return this.selectedTab == this.tabs.indexOf(tab);
}
get selectedTab():number{
return this.index;
}
set selectedTab(val:number){
this.index = val;
var defaultTab = this.tabs[this.index]['tabName'];
if(!this.assetDetailDataModel.catalogId){
this.assetDataLoadedService.assetDetailPublisher.subscribe(data=>{
this.loadComponentByTab(defaultTab);
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
});
}
else{
this.loadComponentByTab(defaultTab);
}
}
handleChange(e) {
let tabName: string = e.originalEvent.currentTarget.innerText;
this.selectedTab = e.index;
//this.loadComponentByTab(tabName);
}
loadComponentByTab(tabName:string){
switch (tabName) {
case AppSettingProperties.TAB_METADATA:
this.componentData = { component: AssetsDataTableComponent, inputs: {} }
break;
case AppSettingProperties.TAB_CATEGORY:
let categoryConfig: object = {"catalog_id":this.assetDetailDataModel.catalogId,"item_id":this.assetDetailDataModel.assetId};
console.log(categoryConfig);
this.componentData = { component: SingleEditCategoryExplorerComponent, inputs: { tabConfig: categoryConfig } }
break;
case AppSettingProperties.TAB_RELATEDITEMS:
this.componentData = { component: GenericDataTableComponent, inputs: { tabConfig: this.relatedConfig } }
break;
case AppSettingProperties.TAB_VERSIONS:
this.componentData = { component: GenericDataTableComponent, inputs: { tabConfig: this.versionConfig } }
break;
}
}
}
Dynamic Component:
import { Component, Input, ViewContainerRef, ViewChild, ReflectiveInjector, ComponentFactoryResolver } from '#angular/core';
#Component({
selector: 'dynamic-component',
template: `<div #dynamicComponentContainer></div>`,
})
export class DynamicComponent {
private currentComponent = null;
#ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) { }
// component: Class for the component you want to create
// inputs: An object with key/value pairs mapped to input name/input value
#Input() set componentData(data: { component: any, inputs: any }) {
console.log("Building Component Start");
if (!data) {
return;
}
// Inputs need to be in the following format to be resolved properly
let inputProviders = Object.keys(data.inputs).map((inputName) => { return { provide: inputName, useValue: data.inputs[inputName] }; });
let resolvedInputs = ReflectiveInjector.resolve(inputProviders);
// We create an injector out of the data we want to pass down and this components injector
let injector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.dynamicComponentContainer.parentInjector);
// We create a factory out of the component we want to create
let factory = this.resolver.resolveComponentFactory(data.component);
// We create the component using the factory and the injector
let component = factory.create(injector);
// We insert the component into the dom container
this.dynamicComponentContainer.insert(component.hostView);
// We can destroy the old component is we like by calling destroy
if (this.currentComponent) {
this.currentComponent.destroy();
}
this.currentComponent = component;
console.log("Building Component Finish");
}
}
Another thing is that the console start in dynamic component is shown 8 times.
While console finish is shown 4-5 times.
Seems really weird behavior.
As #echonax wrote in comment.
This is because you are trying to iterate something that is not an array.
Most probably this.tabs.
You can try and write out {{tabs|json}} in a div instead of the *ngFor
Since your response takes sometime to load your DOM will have tabs variable as undefined array.
To solve this initialize the variable to an empty array as below
tabs:Array<any> = []
or inside the constructor as
constructor(){
this.tabs = [];
}
I started looking into unit testing angular 2 applications, but I'm stuck even in the simplest examples. I just want to run a simple test to see if it even works, basically what I want is to compare a value from the title page to the one in the test.
This is the error I'm getting, but I don't see where the error is coming from since everything looks to be synchronous to me.
Error: Error: Cannot call Promise.then from within a sync test.
Unit test:
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { DebugElement, Input} from '#angular/core';
import { ToDoComponent } from './todo.component';
import { FormsModule } from '#angular/forms';
describe(("test input "),() => {
let comp: ToDoComponent;
let fixture: ComponentFixture<ToDoComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ ToDoComponent ],
imports: [ FormsModule ]
})
.compileComponents();
});
fixture = TestBed.createComponent(ToDoComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css("h1"));
el = de.nativeElement;
it('should display a different test title', () => {
comp.pageTitle = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toBe('Test Title423');
});
});
My component:
import {Component} from "#angular/core";
import {Note} from "app/note";
#Component({
selector : "toDoArea",
templateUrl : "todo.component.html"
})
export class ToDoComponent{
pageTitle : string = "Test";
noteText : string ="";
noteArray : Note[] = [];
counter : number = 1;
removeCount : number = 1;
addNote() : void {
if (this.noteText.length > 0){
var a = this.noteText;
var n1 : Note = new Note();
n1.noteText = a;
n1.noteId = this.counter;
this.counter = this.counter + 1;
this.noteText = "";
this.noteArray.push(n1);
}
}
removeNote(selectedNote : Note) :void{
this.noteArray.splice(this.noteArray.indexOf(selectedNote),this.removeCount);
}
}
Move your variable initialization inside a beforeEach.
You shouldn't be getting things out of the TestBed or managing the fixture or component in the describe scope. You should only do these things within the scope of a test run: inside a beforeEach/beforeAll, afterEach/afterAll, or inside an it.
describe(("test input "), () => {
let comp: ToDoComponent;
let fixture: ComponentFixture<ToDoComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ToDoComponent],
imports: [FormsModule]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ToDoComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css("h1"));
el = de.nativeElement;
})
it('should display a different test title', () => {
comp.pageTitle = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toBe('Test Title423');
});
});
See also
https://angular.io/docs/ts/latest/guide/testing.html#!#waiting-compile-components
I got the same error for a different reason. I put a TestBed.get(Dependency) call within a describe block. The fix was moving it to the it block.
Wrong:
describe('someFunction', () => {
const dependency = TestBed.get(Dependency); // this was causing the error
it('should not fail', () => {
someFunction(dependency);
});
});
Fixed:
describe('someFunction', () => {
it('should not fail', () => {
const dependency = TestBed.get(Dependency); // putting it here fixed the issue
someFunction(dependency);
});
});
I'm working on my first Angular2 project (building a Pokemon web app), and I keep getting the following error messages when trying to load the page:
failed to compile.
/home/mattlayton1986/workspace/pokedex/src/app/pokedex.service.ts (26,20): Cannot find name 'p'.
/home/mattlayton1986/workspace/pokedex/src/app/pokedex.service.ts (26,23): Cannot find name 'i'.
The error is occurring inside my ./pokedex-service.ts' file, which loads the data from the API and gets injected into the component. Here is all the code in myPokedexService` file:
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
#Injectable()
export class PokedexService {
private baseUrl: string = 'https://pokeapi.co/api/v2/pokemon/';
private baseSpriteUrl: string = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/';
constructor(private http: Http) { }
getPokemon(offset: number, limit: number) {
return this.http.get(
`${this.baseUrl}?offset=${offset}&limit=${limit}`
).toPromise().then(
response => response.json().results
).then(items => items.map(
(poke, idx) => {
const id: number = idx + offset + 1;
return {
id,
name: poke.name,
sprite: `${this.baseSpriteUrl}${id}.png`
};
}
).map(getTypes(p, i))); // <-- error occurs here
}
}
function getTypes(pokemon, id) {
return this.http.get(
`${this.baseUrl}${id}`
).toPromise().then(
response => response.json().results
).then(item => item.map(
poke => {
return {
poke,
type1: poke.types.type[0].name,
type2: poke.types.type[1].name
}
}
));
}
For reference, in case it helps, here is my main component and its template, which makes use of the service to load Pokemon data:
app.component.html
<h1>Angular 2 Pokedex</h1>
<span>This is a sample app using Angular 2 RC5. Check out the source code here</span>
<hr />
<div class="pokedex">
<div class="pokedex-pokemon" *ngFor="let p of pokemon" [pokemonTypes]="p">
<div class="pokedex-pokemon-id">
{{p.id}}
</div>
<img [ngClass]="{'hidden': !p.imageLoaded}" class="pokedex-pokemon-sprite" (load)="p.imageLoaded = true" [attr.src]="p.sprite" />
<div class="pokedex-pokemon-name">
{{ p.name | capitalize }}
</div>
<div class="pokedex-pokemon-type1">
{{ p.types.type1 }}
</div>
<div calss="pokedex-pokemon-type2">
{{ p.types.type2 }}
</div>
</div>
</div>
<button class="load-button" (click)="loadMore()" [disabled]="isLoading">
<span *ngIf="!error">
<span *ngIf="isLoading">Loading...</span>
<span *ngIf="!isLoading">Load more</span>
</span>
<span *ngIf="error">
Loading failed
</span>
</button>
app.component.ts
import { Component } from '#angular/core';
import { OnInit } from '#angular/core';
import { PokedexService } from './pokedex.service';
import { Pokemon } from './pokemon';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
pokemon: Pokemon[] = [];
isLoading: boolean = false;
error: boolean = false;
constructor(private pokedexService: PokedexService) {}
ngOnInit() {
// Loads the initial data.
this.loadMore();
}
loadMore() {
this.isLoading = true;
// User the Pokedex service
// to load the next 9 Pokemon.
this.pokedexService.getPokemon(this.pokemon.length, 9)
.then(pokemon => {
pokemon = pokemon.map(p => {
p.imageLoaded = false;
return p;
});
this.pokemon = this.pokemon.concat(pokemon);
this.isLoading = false;
this.error = false;
})
.catch( () => {
this.error = true;
this.isLoading = false;
});
}
}
I've called my map function's callback with a parameter for the current item in the array I'm mapping and the index of that item, and then included formal parameters for both in the function definition of getTypes, so I'm not sure where the error is coming from or how to resolve it. Any help in clearing this up is greatly appreciated.
You aren't defining the parameters p or i.
What you want to do is:
.map((p, i) => getTypes(p, i));
passing a function to map that has both p and i defined in it's scope.
In your case you're mapping over the result of another map (which returns an array) so you would want to destructure the array as below:
.map(([p, i]) => getTypes(p, i));
Which will take the array and split the variable assignments out for you. The same as doing:
.map(arr => {
const p = arr.p;
const i = arr.i;
return getTypes(p, i);
});
but a lot more succinct.
I think it should be
).map(v => getTypes(v.name, v.id))); // <-- error occurs here