Skip to content

Commit

Permalink
Merge pull request #17 from hammzj/dev/add-elements-method
Browse files Browse the repository at this point in the history
Dev/add elements method
  • Loading branch information
hammzj authored Apr 25, 2024
2 parents 75d5e3f + 61ca37e commit 06ac465
Show file tree
Hide file tree
Showing 7 changed files with 427 additions and 260 deletions.
108 changes: 71 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,30 @@ import {
} from "@hammzj/cypress-page-object";

class FooterObject extends ComponentObject {
public elements: Elements;

constructor() {
super(() => cy.get(`footer`));
this.addElements = {
copyright: () => this.container().find(`p.MuiTypography-root`),
};
}

elements = {
...this.elements,
copyright: () => this.container().find(`p.MuiTypography-root`),
};
}

class ExamplePageObject extends PageObject {
public elements: Elements;
public components: NestedComponents;

constructor() {
super();
this.addElements = {
appBar: () => cy.get(`.MuiAppBar-root`),
appLink: (label: title) => this.elements.appBar().contains("a.MuiLink-root", label),
};
this.addNestedComponents = {
FooterObject: (fn) => this.performWithin(this.container(), new FooterObject(), fn),
}
}

elements = {
...this.elements,
appBar: () => cy.get(`.MuiAppBar-root`),
appLink: (label: title) => this.elements.appBar().contains("a.MuiLink-root", label),
};

components = {
FooterObject: (fn) => this.performWithin(this.container(), new FooterObject(), fn),
};

}

const examplePageObject = new ExamplePageObject();
Expand All @@ -47,7 +46,7 @@ examplePageObject.components.FooterObject((footerObject) => {

## Installation

This package is hosted on both the NPMJS registry and the GitHub NPM package repository.
This package is hosted on both the npmjs registry and the GitHub npm package repository.

If installing from GitHub, please add or update
your `.npmrc` file with the following:
Expand Down Expand Up @@ -103,13 +102,51 @@ class SearchForm extends ElementCollection {
Base elements are locators for HTML elements on the webpage. They should exist as chained from the base container, or
another element selector in the collection.

These are defined in `this.elements`. You can either extend the original elements
with `this.elements = { ...this.elements, ... }`
or use `Object.assign(this.elements, { ... })` inside the class constructor.
These are defined in `this.elements`. Add new element selectors in the constructor by calling the "set"
method, `this.addElements`. This allows classes to inherit other elements from their base class when extending them.

You can also extend the original elements
with `this.elements = { ...this.elements, ... }` or use `Object.assign(this.elements, { ... })` inside the class
constructor,
but this might produce warnings with using `this.elements` before initialization.

_Note: `this.elements` defaults to being a protected method so that elements are only used in app action functions!
If you want to access elements outside of app action, add `public elements: Elements` to your class._

<details>
<summary>Setting elements</summary>

```js
class NewUserForm extends ElementCollection {
constructor() {
//This is the base container function for the address form
super(() => cy.get("form#new-user"));
this.addElements = {
//An element selector chained from another element selector -- selects the first found "input"
usernameField: () => this.container().find(`input`).first(),
passwordField: () => this.elements.usernameField().next(),
//Some selectors can return many elements at once!
fieldErrors: () => {
//Assumes that multiple field errors can be present on submission, so it has the possiblity to return many elements!
//For example, you can use this.fieldErrors.eq(i) to find a single instance of the error.
//@see https://docs.cypress.io/api/commands/eq

return this.container().find(`div.error`);
},
};
}
}
```

</details>

<details>
<summary>Alternate ways of setting elements</summary>

These might produce warnings!

Using spread syntax

```js
class NewUserForm extends ElementCollection {
constructor() {
Expand All @@ -118,24 +155,23 @@ class NewUserForm extends ElementCollection {
}

public elements = {
...this.elements,
//An element selector chained from another element selector -- selects the first found "input"
usernameField: () => this.container().find(`input`).first(),
passwordField: () => this.elements.usernameField().next(),
//Some selectors can return many elements at once!
fieldErrors: () => { //Assumes that multiple field errors can be present on submission, so it has the possiblity to return many elements!
fieldErrors: () => {
//Assumes that multiple field errors can be present on submission, so it has the possiblity to return many elements!
//For example, you can use this.fieldErrors.eq(i) to find a single instance of the error.
//@see https://docs.cypress.io/api/commands/eq

return this.container().find(`div.error`);
}
}
},
};
}
```

</details>

<details>
<summary>Alternate way of setting elements</summary>
Using `Object.assign()`

```js
class NewUserForm extends ElementCollection {
Expand Down Expand Up @@ -171,12 +207,11 @@ For example, finding a radio button in a list of selections:
class SelectAnShippingOptionObject extends ElementCollection {
constructor() {
super(() => cy.get(`form#select-a-shipping-partner`));
this.addElements = {
//Finds the radio button based on its text
radioButton: (text) => this.container().contains(`button[type="radio"]`, text),
};
}

public elements = {
//Finds the radio button based on its text
radioButton: (text) => this.container().contains(`button[type="radio"]`, text),
};
}
```

Expand All @@ -198,13 +233,12 @@ const { ComponentObject } = require("@hammzj/cypress-page-object");
class SearchForm extends ComponentObject {
constructor() {
super(() => cy.get(`form#location-search-form`));
this.addElements = {
inputField: () => this.container().find(`input[type="text"]`),
submitButton: () => this.container().find(`button[type="submit"]`),
};
}

public elements = {
inputField: () => this.container().find(`input[type="text"]`),
submitButton: () => this.container().find(`button[type="submit"]`),
};

//An app action to search for text using the form
search(text, submit = true) {
this.elements.inputField().type(text);
Expand Down
65 changes: 39 additions & 26 deletions docs/WORKING_WITH_NESTED_OBJECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ function fn(nestedComponent) {
};
```

### Adding nested components

These are defined in `this.components`. Add new component objects in the constructor by calling the "set"
method, `this.addNestedComponents` or its alias of `this.addComponents`. This allows classes to inherit other elements from their base class when extending them.

You can also extend the original elements
with `this.components = { ...this.components, ... }` or use `Object.assign(this.components, { ... })` inside the class
constructor,
but this might produce warnings with using `this.components` before initialization.

_Note: `this.components` defaults to being a protected method so they are only used in app action functions!
If you want to access elements outside of app action, add `public components: NestedComponents` to your class._

### Example 1: A simple nested component object

```js
Expand All @@ -50,10 +63,11 @@ import AccountFormObject from "../components/account.form.object";
class CreateAccountPage extends PageObject {
contructor() {
super({ path: `/create-account` });
}

AccountFormObject(fn) {
this.performWithin(this.container(), new AccountFormObject(), fn);
this.addNestedComponents = {
AccountFormObject(fn) {
this.performWithin(this.container(), new AccountFormObject(), fn);
},
};
}
}
```
Expand Down Expand Up @@ -91,17 +105,19 @@ class ToggleButton extends ComponentObject {
}

class SettingsObject extends ComponentObject {
constructor() {
super(() => cy.get("div[id=\"settings\"]"));
}
public elements: Elements;
public components: NestedComponents;

public elements = {
displaySettingsForm: () => this.container().find(`form#mode-selectors`),

};

ToggleButton(fn, buttonText) {
this.performWithin(this.elements.displaySettingsForm(), new ToggleButton(buttonText), fn);
constructor() {
super(() => cy.get(`div[id="settings"]`));
this.addElements = {
displaySettingsForm: () => this.container().find(`form#mode-selectors`),
};
this.addNestedComponents = {
ToggleButton(fn, buttonText) {
this.performWithin(this.elements.displaySettingsForm(), new ToggleButton(buttonText), fn);
},
};
}
}
```
Expand Down Expand Up @@ -145,11 +161,10 @@ import AccountFormObject from "../components/account.form.object";
class CreateAccountPage extends PageObject {
constructor() {
super({ path: `/create-account` });
this.addNestedComponents = {
AccountFormObject: (fn) => this.container().within(() => fn(new AccountFormObject())),
};
}

public components = {
AccountFormObject: (fn) => this.container().within(() => fn(new AccountFormObject()))
};
}
```

Expand All @@ -164,15 +179,13 @@ class ToggleButton extends ComponentObject {
class SettingsObject extends ComponentObject {
constructor() {
super(() => cy.get(`div[id="settings"]`));
this.addElements = {
displaySettingsForm: () => this.container.find(`form#mode-selectors`),
};
this.addNestedComponents = {
ToggleButton: (fn) => this.elements.displaySettingsForm().within(() => fn(new ToggleButton(buttonText))),
};
}

public elements = {
displaySettingsForm: () => this.container.find(`form#mode-selectors`),
};

public components = {
ToggleButton: (fn) => this.elements.displaySettingsForm().within(() => fn(new ToggleButton(buttonText))),
};
}
```

Expand Down
6 changes: 1 addition & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hammzj/cypress-page-object",
"version": "2.1.1",
"version": "2.1.2",
"description": "A set of template classes and guides to help with developing component and page objects in Cypress.",
"author": "Zachary Hamm <[email protected]>",
"license": "MIT",
Expand Down Expand Up @@ -37,13 +37,9 @@
"test:cypress:open:e2e": "cypress open --e2e --browser electron",
"test:cypress:run:e2e": "cypress run --e2e --browser electron"
},
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-private-property-in-object": "^7.23.3",
"@types/lodash": "^4.17.0",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
Expand Down
6 changes: 3 additions & 3 deletions src/component.object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { gt, isEqual } from "lodash";
import { BaseContainerFunction } from "./types";
import ElementCollection from "./element.collection";

Expand All @@ -22,8 +21,9 @@ export default class ComponentObject extends ElementCollection {
}

assertExists(expectation: boolean = true): void {
if (isEqual(expectation, false)) {
if (gt(this._scopedIndex, 0)) {
if (!expectation) {
//@ts-ignore
if (this._scopedIndex > 0) {
/*
The scoped index is set above 0 (i.e., it is not the first-found instance).
Make sure at least a base container exists first,
Expand Down
41 changes: 36 additions & 5 deletions src/element.collection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { BaseContainerFunction, ComponentObjectFunction, Elements, NestedComponents, IMetadata } from "./types";
import { isNil } from "lodash";

/**
* Base class for describing page objects and components, which have a collection of element selectors
Expand All @@ -8,9 +7,7 @@ export default abstract class ElementCollection {
protected _baseContainerFn: BaseContainerFunction;
protected _scopedIndex?: number;
protected metadata: Partial<IMetadata>;
protected elements: Elements = {
container: () => this.container(),
};
protected elements: Elements;
protected components?: NestedComponents = {};

/**
Expand All @@ -22,6 +19,9 @@ export default abstract class ElementCollection {
constructor(baseContainerFn: BaseContainerFunction = () => cy.root()) {
this._baseContainerFn = baseContainerFn;
this.metadata = {};
this.elements = {
container: () => this.container(),
};
}

/**
Expand Down Expand Up @@ -61,6 +61,36 @@ export default abstract class ElementCollection {
this.scopedIndex = i;
}

/**
* Adds new element selectors to the base group of existing elements
* WARNING: you will set `elements` as public in order to access them outside of app action functions!
* Just add `public elements: Elements` before your constructor.
* @param elements
* @protected
*/
protected set addElements(elements: Elements) {
this.elements = Object.assign(this.elements || {}, elements);
}

/**
* Adds new element selectors to the base group of existing elements
* WARNING: you will set `components` as public in order to access them outside of app action functions!
* Just add `public components: NestedComponentd` before your constructor.
* @param components
* @protected
*/
protected set addNestedComponents(components: NestedComponents) {
this.components = Object.assign(this.components || {}, components);
}

/**
* @alias addNestedComponents
* @protected
*/
protected set addComponents(components: NestedComponents) {
this.addNestedComponents = components;
}

/**
* This allows the `_baseContainerFn` to be chained, allowing to scope the instances of the base containers returned to a smaller set
* of instances or even just an individual element. Useful for finding a single element from a larger collection of repeating elements, like a button in a list using its text or radio choices
Expand Down Expand Up @@ -104,7 +134,7 @@ export default abstract class ElementCollection {
* when expecting multiple elements to be located.
*/
container(): Cypress.Chainable<Cypress.JQueryWithSelector> {
return !isNil(this._scopedIndex) ?
return this._scopedIndex != null ?
this.getAllContainers().eq(this._scopedIndex)
: this.getAllContainers().first();
}
Expand Down Expand Up @@ -158,6 +188,7 @@ export default abstract class ElementCollection {
* this.elements.form().within(() => fn(new RadioButtonObject(buttonText)));
* }
*/
//@ts-ignore
performWithin(
baseElement: Cypress.Chainable<Cypress.JQueryWithSelector>,
nestedComponent: ElementCollection,
Expand Down
Loading

0 comments on commit 06ac465

Please sign in to comment.