{"action":"created","issue":{"url":"https://api.github.com/repos/nightwatchjs/nightwatch/issues/729","labels_url":"https://api.github.com/repos/nightwatchjs/nightwatch/issues/729/labels{/name}","comments_url":"https://api.github.com/repos/nightwatchjs/nightwatch/issues/729/comments","events_url":"https://api.github.com/repos/nightwatchjs/nightwatch/issues/729/events","html_url":"https://github.com/nightwatchjs/nightwatch/issues/729","id":115160274,"number":729,"title":"Pages vs Repeated UI Elements","user":{"login":"iabw","id":1054569,"avatar_url":"https://avatars.githubusercontent.com/u/1054569?v=3","gravatar_id":"","url":"https://api.github.com/users/iabw","html_url":"https://github.com/iabw","followers_url":"https://api.github.com/users/iabw/followers","following_url":"https://api.github.com/users/iabw/following{/other_user}","gists_url":"https://api.github.com/users/iabw/gists{/gist_id}","starred_url":"https://api.github.com/users/iabw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/iabw/subscriptions","organizations_url":"https://api.github.com/users/iabw/orgs","repos_url":"https://api.github.com/users/iabw/repos","events_url":"https://api.github.com/users/iabw/events{/privacy}","received_events_url":"https://api.github.com/users/iabw/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":3,"created_at":"2015-11-04T22:07:34Z","updated_at":"2015-11-05T14:18:37Z","closed_at":null,"body":"##### Overview\r\nThere is not an optimal place to put UI elements used across multiple pages. Common examples are header, footer, login form, cart, ad popups, etc.\r\n\r\nI don't think that this is directly comparable to the current implementation of Sections & Elements, because these widgets will generally have their own commands, possibly URLs, and are not just nested selectors.\r\n\r\n##### Current Problems\r\nPages don't require a url property, but if navigate is called without one, an error is thrown. For certain UI elements, they SHOULD have a URL that can be navigated to, but the URL would only be a hash, such as `{ url: '#login' }` (tabs or content sections would be the primary example). Calling `.navigate()` on a page with such a url does not currently work, and requires custom implementation.\r\n\r\nIt becomes easy to propagate copy/pasted widget code across pages, since there's not a baked in solution for them, and you end up with something like `client.pages[ {any page} ].login()`\r\n\r\nOr, you lump such widgets together, and they're not guaranteed any organization - `client.pages.utility().login()`\r\n\r\nOr, you *do* separate them into separate pages based on widget, and the name is still slightly confusing. When seeing something like `client.pages.login()`. It is very unclear if there is a separate login page with its own url that is missing a `navigate()` call, or if the login form is expected to be a UI element in the currently displayed page. You would need to rename the login \"page\" as `widget_login` or `ui_login` or some other arbitrary and ugly name - `client.pages.ui_login()`.\r\n\r\n##### Proposal\r\nI propose adding a new type of page object - `widgets`, `interfaces`, `ui`, or some name like that. These would be defined and instantiated by Nightwatch almost identically to pages with a few key differences:\r\n- They would be expected to have their own folder separate from pages, and defined in the config the same way that `page_objects_path` is\r\n- They would be located under `client.widgets` (or interfaces, or ui, or whatever) instead of `client.pages`: like `client.widgets.login()`\r\n- Their url should either not throw, throw a different error, and/or handle hash values correctly. A widget with its own 'real' url is just a page, and should probably be filed under 'pages'.\r\n- Probably pages should be able to extend themselves so that instead of having to call `client.widgets.login().someMethod()`, you could have a page list widgets it contains, and then call `client.pages.myPage().widgets.login().someMethod()`. Without this, all of a page's knowledge of the widgets it contains would be in the commands or tests themselves, and very hard to reflect on (as opposed to current Sections / Element, which are easily iterable)."},"comment":{"url":"https://api.github.com/repos/nightwatchjs/nightwatch/issues/comments/154070167","html_url":"https://github.com/nightwatchjs/nightwatch/issues/729#issuecomment-154070167","issue_url":"https://api.github.com/repos/nightwatchjs/nightwatch/issues/729","id":154070167,"user":{"login":"iabw","id":1054569,"avatar_url":"https://avatars.githubusercontent.com/u/1054569?v=3","gravatar_id":"","url":"https://api.github.com/users/iabw","html_url":"https://github.com/iabw","followers_url":"https://api.github.com/users/iabw/followers","following_url":"https://api.github.com/users/iabw/following{/other_user}","gists_url":"https://api.github.com/users/iabw/gists{/gist_id}","starred_url":"https://api.github.com/users/iabw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/iabw/subscriptions","organizations_url":"https://api.github.com/users/iabw/orgs","repos_url":"https://api.github.com/users/iabw/repos","events_url":"https://api.github.com/users/iabw/events{/privacy}","received_events_url":"https://api.github.com/users/iabw/received_events","type":"User","site_admin":false},"created_at":"2015-11-05T14:18:37Z","updated_at":"2015-11-05T14:18:37Z","body":"After thinking about it some more, I think these components are more analogous to sections than pages. Sections are just components that haven't appeared on other pages yet. I could even see an argument for building all sections as components, so that they can be easily added and removed from page objects.\r\n\r\nAnd thinking of how I've built my pages, they seem to follow this rule, it's just that there are multiple components \"hard-wired\" into a single page object. Most page commands deal with only a single \"component\", and the ones that don't are messy, probably because I'm not separating concerns well enough.\r\n\r\nHere's a somewhat contrived but real example of an actual page I have. The billing section and fillBillingFields method are currently repeated across multiple pages.\r\n```\r\nmodule.exports = {\r\n    url: function(){ return this.api.globals.domain + '/checkout' },\r\n\r\n    elements: {\r\n        shippingIsBilling: '.js-shippingIsBilling',\r\n    },\r\n\r\n    sections: {\r\n        shipping: {\r\n            selector: '.js-shippingFieldset',\r\n            elements: {\r\n                firstName: '#shippingAddress\\\\.firstName',\r\n                lastName: '#shippingAddress\\\\.lastName',\r\n                street1: '#shippingAddress\\\\.street1',\r\n                street2: '#shippingAddress\\\\.street2',\r\n                city: '#shippingAddress\\\\.city',\r\n                country: '#shippingAddress\\\\.country',\r\n                subCountry: '#shippingAddress\\\\.subCountry',\r\n                zipOrPostalCode: '#shippingAddress\\\\.zipOrPostalCode',\r\n                phoneNumber: '#shippingAddress\\\\.phoneNumber'\r\n            }\r\n        },\r\n        billing: {\r\n            selector: '.js-billingFieldset',\r\n            elements: {\r\n                firstName: '#billingAddress\\\\.firstName',\r\n                lastName: '#billingAddress\\\\.lastName',\r\n                country: '#billingAddress\\\\.country',\r\n                zipOrPostalCode: '#billingAddress\\\\.zipOrPostalCode',\r\n                phoneNumber: '#billingAddress\\\\.phoneNumber'\r\n            }\r\n        }\r\n    },\r\n\r\n    commands: [{\r\n        fillShippingFields: function() {\r\n            this.section.shipping\r\n                .assert.visible('@firstName')\r\n                .setValue('@firstName', 'Sherbet')\r\n                .setValue('@lastName', 'Jackson')\r\n                .setValue('@street1', '200000 Main Street')\r\n                .setValue('@street2', '')\r\n                .setValue('@city', 'New York')\r\n                .setValue('@subCountry', 'NY')\r\n                .setValue('@zipOrPostalCode', '80085')\r\n                .setValue('@phoneNumber', '555-123-4567')\r\n\r\n            return this\r\n        },\r\n\r\n        setShippingIsBilling: function(value) {\r\n            return this\r\n                .assert.visible('@shippingIsBilling')\r\n                .setValue('@shippingIsBilling', value || true)\r\n        },\r\n\r\n        fillBillingFields: function() {\r\n            this.section.billing\r\n                .assert.visible('@firstName')\r\n                .setValue('@firstName', 'Sherbet')\r\n                .setValue('@lastName', 'Jackson')\r\n                .setValue('@country', 'United States')\r\n                .setValue('@zipOrPostalCode', '80085')\r\n                .setValue('@phoneNumber', '555-123-4567')\r\n\r\n            return this\r\n        }\r\n    }]\r\n};\r\n```\r\n\r\nThis could be refactored into components and written instead like this.\r\n```\r\n// shippingComponent.js\r\nmodule.exports = {\r\n    selector: '.js-shippingFieldset',\r\n\r\n    elements: {\r\n        firstName: '#shippingAddress\\\\.firstName',\r\n        lastName: '#shippingAddress\\\\.lastName',\r\n        street1: '#shippingAddress\\\\.street1',\r\n        street2: '#shippingAddress\\\\.street2',\r\n        city: '#shippingAddress\\\\.city',\r\n        country: '#shippingAddress\\\\.country',\r\n        subCountry: '#shippingAddress\\\\.subCountry',\r\n        zipOrPostalCode: '#shippingAddress\\\\.zipOrPostalCode',\r\n        phoneNumber: '#shippingAddress\\\\.phoneNumber'\r\n    },\r\n\r\n    commands: [{\r\n        fillFields: function() {\r\n            this.\r\n                .assert.visible('@firstName')\r\n                .setValue('@firstName', 'Sherbet')\r\n                .setValue('@lastName', 'Jackson')\r\n                .setValue('@street1', '200000 Main Street')\r\n                .setValue('@street2', '')\r\n                .setValue('@city', 'New York')\r\n                .setValue('@subCountry', 'NY')\r\n                .setValue('@zipOrPostalCode', '80085')\r\n                .setValue('@phoneNumber', '555-123-4567')\r\n\r\n            return this\r\n        }\r\n    }]\r\n};\r\n\r\n// billingComponent.js\r\nmodule.exports = {\r\n    selector: '.js-billingFieldset',\r\n\r\n    elements: {\r\n        firstName: '#billingAddress\\\\.firstName',\r\n        lastName: '#billingAddress\\\\.lastName',\r\n        country: '#billingAddress\\\\.country',\r\n        zipOrPostalCode: '#billingAddress\\\\.zipOrPostalCode',\r\n        phoneNumber: '#billingAddress\\\\.phoneNumber'\r\n    },\r\n\r\n    commands: [{\r\n        fillFields: function() {\r\n            this.\r\n                .assert.visible('@firstName')\r\n                .setValue('@firstName', 'Sherbet')\r\n                .setValue('@lastName', 'Jackson')\r\n                .setValue('@country', 'United States')\r\n                .setValue('@zipOrPostalCode', '80085')\r\n                .setValue('@phoneNumber', '555-123-4567')\r\n\r\n            return this\r\n        }\r\n    }]\r\n};\r\n\r\n// page.js\r\nmodule.exports = {\r\n    url: function(){ return this.api.globals.domain + '/checkout' },\r\n\r\n    elements: {\r\n        shippingIsBilling: '.js-shippingIsBilling',\r\n    },\r\n\r\n    sections: {\r\n        shipping: client.components.shipping\r\n        billing: client.components.billing\r\n    },\r\n\r\n    commands: [{\r\n        fillShippingFields: function() {\r\n            this.section.shipping.fillFields()\r\n\r\n            return this\r\n        },\r\n\r\n        setShippingIsBilling: function(value) {\r\n            return this\r\n                .assert.visible('@shippingIsBilling')\r\n                .setValue('@shippingIsBilling', value || true)\r\n        },\r\n\r\n        fillBillingFields: function() {\r\n            this.section.billing.fillFIelds()\r\n\r\n            return this\r\n        }\r\n    }]\r\n};\r\n```"}}