Cypress get attribute value and assign it to variable in function - cypress

I am trying to get attribute value and return it from a function.
Here is the code that is working and can be used in a normal test class (into the integration folder).
describe('Example shows how to get attribute value.', () => {
// 'it' is used to create test case. You can add a name of the test case. You can have multiple test cases in one JS class.
it('Get attribute value.', () => {
// Cypress is not able to work with new tabs. It is not possible to switch between tabs. Cypress can manipulate the DOM tree, so we can change the element attributes and open the hyperlink in the same browser tab.
// 'visit()' method is used for navigating to URL address.
cy.visit('https://demoqa.com/links')
cy.xpath('//*[#id="simpleLink"]').then(function (element) {
// 'prop()' method is used to get the attribute value.
const url = element.prop('href')
cy.visit(url)
})
// Assert URL.
cy.url().should('include', 'demoqa.com').should('eq', 'https://demoqa.com/');
})
If I use the code that way - everything is working as expected.
But if I want to re-use the code and create a function like this:
// Give a value of the variable to use it for next function.
functionName = 'addAttribute';
// Declare a Cypress child custom command.
Cypress.Commands.add(functionName, { prevSubject: 'element' }, (subject: any, attributeName: string, attributeValue: string) => {
// Create a try-catch statement. If the function fails - we will recieve the error message.
try {
// Create the function steps after this comment.
cy
.wrap(subject)
.invoke('attr', attributeName, attributeValue)
.should('have.attr', attributeName, attributeValue)
} catch (error) {
// Create the error log and show it to the UI. Show the function name, the class where the function is located and catched error.
let errorMessage = `----------ERROR! It seems that we have an error. Please review the "${functionName}" function from "${__filename.split(__dirname + "/").pop()}" . The error is: ${error}`;
cy.log(errorMessage);
console.log(errorMessage);
}
})
The result is 'object' and I am not sure how to process it.
Here is the rest of the code:
describe("'getAttribute' custom child command example.", () => {
it("example shows how to use 'getAttribute' custom child command.", () => {
cy.visit('https://demoqa.com/buttons');
let attributeValue = cy.element('xpath','(//*[contains(text(),"Click Me")])[3]').getAttribute('class');
cy.log(`The attribute values is: ${attributeValue}`)
});
});

You have to do as below to have a return value:
let attributeValue = '';
cy.element('xpath', '(//*[contains(text(),"Click Me")])[3]')
.getAttribute('class')
.then((attr) => {
attributeValue = attr;
});
cy.log('The attribute values is:' + attributeValue)

Related

How to override 'data-testid' in the 'findByTestId function from Cypress Testing Library

Most of my existing codebase uses a 'id' only in few places 'data-testId' attribute present.
tried this code
import { configure } from '#testing-library/cypress';
configure({ testIdAttribute: ['data-testId','id'] });
But, still its not working.
Is there any way to use 'id' value in any of the testing-library functions.
My HTML code is something like:
<div class="some random class name" id="userprofile-open" role="button">SB</div>
I want click that element with this code:
cy.findByTestId("userprofile-open", { timeout: 120000 }).click();
I don't think you can configure testing-library with an array of ids, ref API configuration,
import { configure } from '#testing-library/cypress'
configure({ testIdAttribute: 'id' })
But even this fails. Instead you have to use the Cypress command to change the attribute name (only one name is allowed).
cy.configureCypressTestingLibrary({ testIdAttribute: 'id' })
To use either/or attribute name you can change the attribute name on the fly, wrapping it in a custom command (based on Custom Queries)
Cypress.Commands.add('findByTestIdOrId', (idToFind) => {
let result;
const { queryHelpers } = require('#testing-library/dom');
let queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'data-testId');
result = queryAllByTestId(Cypress.$('body')[0], idToFind)
if (result.length) return result;
queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'id');
result = queryAllByTestId(Cypress.$('body')[0], idToFind);
if (result.length) return result;
throw `Unable to find an element by: [data-test-id="${idToFind}"] or [id="${idToFind}"]`
})
cy.findByTestIdOrId('my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
Note this custom command works only for synchronous DOM.
If you need to have Cypress retry and search for either/or attribute, don't use testing-library in the custom command.
Instead use Cypress .should() to enable retry
Cypress.Commands.add('findByTestIdOrId', (selector, idToFind) => {
cy.get(selector)
.should('satisfy', $els => {
const attrs = [...$els].reduce((acc, el) => {
const id = el.id || el.getAttribute('data-test-id') // either/or attribute
if (id) {
acc.push(id)
}
return acc
}, [])
return attrs.some(attr => attr === idToFind); // retries when false
})
.first(); // may be more than one
})
cy.findByTestIdOrId('div', 'my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
The usual cypress way - which has an inherent check on the element visibility and existence as well as included retries for a period of time is using cy.get()
If you want to select element using property like data-id you need this sintax: cy.get('[propertyName="propertyValue"]')
If you want select an element by CSS selector you just pass CSS selector like this:
cy.get('#id')

check store for object before calling api

You know how they say you don't need state management until you know you need it. Well turns out my project needs it. So I need some help wit best practice as I am adding ngxs to an existing angular project.
I have an action called getServiceDetail and my statemodel has a list of objects called DriverListsStopInfoViewModel. each of these objects have a unique ID. The html template of the consuming component uses a selector for the property currentStopDetail, which is a state property that gets set in my action.
GOAL:
in my action I want to check the list of objects in my store to see if an object with the same id exists and return that object, and if it does not exist, call and api to get it.
EXAMPLE:
The following code works, but I would like to hear if this is the right way to do it. do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
export interface SignServiceStateModel {
searchResults: ServiceSearchModel[];
driverStopsDetails: DriverListsStopInfoViewModel[];
driverStopsList: DriverListsStopsViewModel[];
driverStopsMarkers: DriverStopsMarkerViewModel[];
currentStopDetail: DriverListsStopInfoViewModel;
}
const SIGNSERVICE_STATE_TOKEN = new StateToken<SignServiceStateModel>(
'signservice'
);
#State<SignServiceStateModel>({
name: SIGNSERVICE_STATE_TOKEN,
defaults: {
searchResults: [],
driverStopsDetails: [],
driverStopsList: [],
driverStopsMarkers: [],
currentStopDetail: null
},
})
#Injectable()
export class SignServiceState {
constructor(private driverListsService: DriverListsService) {}
#Action(DriverList.GetServiceDetail)
getServiceDetail(
ctx: StateContext<SignServiceStateModel>,
action: DriverList.GetServiceDetail
) {
if (action.serviceId === undefined || action.serviceId <= 0) {
return;
}
// check if record already in list and return
const currentState = ctx.getState();
const existingStopDetail = currentState.driverStopsDetails.find(s => s.SignServiceId === action.serviceId);
if (existingStopDetail !== undefined) {
const currentStopDetail = existingStopDetail;
ctx.patchState({ currentStopDetail });
return currentStopDetail;
}
// else get new record, add it to list and return
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((currentStopDetail) => {
ctx.patchState({ currentStopDetail });
ctx.setState(
patch({
driverStopsDetails: append([currentStopDetail])
})
);
})
);
}
#Selector()
static currentStopDetail(state: SignServiceStateModel) {
return state.currentStopDetail;
}
}
I only included the relevant code from my state class
QUESTION:
is this the best way to check the store for an item and call api if it does not exist?
Thanks in advance
Short answer is yes, what you have done here is a typical way of handling this scenario (in my experience). There's a couple of improvements you could make:
do I even need to return the object from the action function if its found, or can I just use patch state to assign it to the currentStopDetail
No, you don't return anything from these action handlers, other than possibly an Observable that NGXS will handle (so in your case if there is no matching item found, you return the Observable that fetchs it from the API and patches the state).
Also when you do make the API call, you should only need a single update to the state:
return this.driverListsService.getDriverListsInfo(action.serviceId).pipe(
tap((result) => {
ctx.setState(
patch({
currentStopDetails: result
driverStopsDetails: append([result]),
})
);
})
);

How to test the code logic of ngOnInit in Jasmine and Angular

My component looks for presence of a parameter in the route in ngOnInit. If the parameter is not present, it shows error. I want to test this logic.
ngOnInit() {
this.id = this.route.snapshot.paramMap.get("id");
if(this.id != null) {
... } else{
console.log("didn't get id from the route");
this.showDialog(...);
}
}
I wrote the following spec. In the spec, the parameter is not passed in the route
beforeEach(async() => {
fixture = TestBed.createComponent(QuestionDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
fit('should show error if question details of a question can\'t be retrieved', () => {
spyOn(component,'showDialog');
expect(componetn.showDialog).toHaveBeenCalled();
});
but my test case fails with reason Expected spy showDialog to have been called.
I suppose the issue is that showDialog gets called when the component gets created before the it is called.
How do I test the logic in ngOnInit? I need the component before I can test it (i.e. call it) and I want to test the logic which gets executed while the component is being created.
To test the ngOnInit method, you just need to call it:
component.ngOnInit();
And the route value can be spied:
spyOn(component.route.snapshot.paramMap,"get").and.returnValue("some_id");
Also, you can change the returned value. For example:
fit("should ...", () => {
let mock_id = null;
spyOn(component,"showDialog");
spyOn(component.route.snapshot.paramMap,"get").and.callFake(() => {
return mock_id;
});
component.ngOnInit();
expect(componetn.showDialog).toHaveBeenCalled();
expect(componetn.showDialog).toHaveBeenCalledTimes(1);
mock_id = "some_value";
component.ngOnInit();
expect(...).to..
...
expect(componetn.showDialog).toHaveBeenCalledTimes(1);
mock_id = "another_value";
component.ngOnInit();
expect(...).to..
...
expect(componetn.showDialog).toHaveBeenCalledTimes(1);
});

Passing text from HTML as variable using protractor

I have a table with unknown values (which I need to log in later) and I would like to save one of it with class currentWeek to a variable. HTML is as following:
<tr _ngcontent-c21="" class="currentWeek">
<th _ngcontent-c21="" class="text-center">Value1Foo</th>
(...)
</tr>
In proctracor I created in Helper.ts:
static getfooOfTheWeek() {
let child = element(by.css('.currentWeek')).$('.text-center');
describe('Get foo', function () {
it('get foo', function () {
browser.driver.get('tablepage');
browser.sleep(3000);
return ((child).getText()).toString();
})
})
}
and in file maintest.ts:
describe('Get FOO', function () {
var FOO=Helper.getfooOfTheWeek();
browser.sleep(2000);
//use the value set in Helper.getfooOfTheWeek();
NegativeTest.SomeTest(FOO);
});
but it fails ususally with - Failed: each key must be a number of string; got undefined - therefore I think the FOO is save as an Object, not as string.
I thought also using JSON (to use JSON.parse()), but developers can't gives the values to the table from JSON
Any path what I can try?
Got it - found this issue about saving as string instead of object: Protractor: element.getText() returns an object and not String where I need to resolve the promise
and instead of passing it as variable with var FOO=Helper.getfooOfTheWeek(); I saved it as browser.params.Foo
static getfooOfTheWeek() {
let child = element(by.css('.currentWeek')).$('.text-center');
browser.driver.get('tablepage');
browser.sleep(3000);
child.getText().then(function(text) {
browser.params.Foo=text;
});
}
and I can run NegativeTest.SomeTest(); where the variable is passed in SomeTest() as browser.params.Foo (instead of writting there FOO)

Jasmine testcase for covering link parameter of the directive

I have been working with the jasmine test cases for the directives but with the template parameter where I would directly check the output of the directive however I do not know how to cover the directive with no template section defined. Like the one given below:
appDirective.directive('linkscriptContainer', [ 'config', function(config) {
return {
restrict : 'A',
scope : {
'value' : '#'
},
link : function(scope, elem, attrs) {
//creating custom script tag
var customScript = document.createElement("script");
customScript.type = "text/javascript";
//checking if property value is available in Config object
//then reading the attribute value and constructing the src tag
if (config[attrs.value] != undefined) {
customScript.src = config[attrs.value];
}
//appending the script tag in linkScriptContainer
elem.append(customScript);
}
};
}]);
You don't need to do anything special for testing the link function. Once, you $compile your element and $scope.$digest() (just like you do for any other directive), link function will be executed by itself.
If your code var customScript = document.createElement("script"); is not getting covered, you might need to mock that function like this,
spyOn(document, "createElement").and.returnValue({});

Resources