├── .gitignore ├── .prettierrc.json ├── dist ├── pipe.d.ts ├── map.d.ts.map ├── pipe.d.ts.map ├── filter.d.ts.map ├── really.d.ts.map ├── filter.d.ts ├── invoke.d.ts.map ├── pipe.js ├── map.d.ts ├── really.d.ts ├── invoke.d.ts ├── filter.js ├── index.d.ts.map ├── map.js ├── invoke.js ├── really.js ├── index.d.ts └── index.js ├── src ├── pipe.ts ├── filter.ts ├── map.ts ├── invoke.ts ├── really.ts └── index.ts ├── cypress └── e2e │ ├── json-attribute │ ├── index.html │ └── spec.js │ ├── filter-by-value │ ├── index.html │ └── spec.js │ ├── duplicates │ ├── index.html │ └── spec.js │ ├── children │ ├── index.html │ └── children-spec.js │ ├── object-spec.js │ ├── parse-search-spec.js │ ├── tap-spec.js │ ├── table-spec.js │ ├── table │ ├── app.js │ ├── app.css │ ├── index.html │ └── tableManager.js │ └── invoke-spec.js ├── cypress.config.js ├── renovate.json ├── .github └── workflows │ ├── ci.yml │ └── badges.yml ├── LICENSE ├── package.json ├── README.md └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /dist/pipe.d.ts: -------------------------------------------------------------------------------- 1 | export declare function pipe(...fns: Function[]): (value: unknown) => unknown; 2 | //# sourceMappingURL=pipe.d.ts.map -------------------------------------------------------------------------------- /src/pipe.ts: -------------------------------------------------------------------------------- 1 | export function pipe(...fns: Function[]) { 2 | return function (value: unknown) { 3 | return fns.reduce((acc, fn) => fn(acc), value) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /cypress/e2e/json-attribute/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
First person
4 | 5 | 6 | -------------------------------------------------------------------------------- /dist/map.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../src/map.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,EAAE,EAAE,QAAQ,UACP,OAAO,SAQ/B"} -------------------------------------------------------------------------------- /dist/pipe.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"pipe.d.ts","sourceRoot":"","sources":["../src/pipe.ts"],"names":[],"mappings":"AAAA,wBAAgB,IAAI,CAAC,GAAG,GAAG,EAAE,QAAQ,EAAE,WACb,OAAO,aAGhC"} -------------------------------------------------------------------------------- /dist/filter.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,MAAM,CAAC,SAAS,EAAE,QAAQ,UACjB,OAAO,SAW/B"} -------------------------------------------------------------------------------- /dist/really.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"really.d.ts","sourceRoot":"","sources":["../src/really.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,GAAG,qBAAqB,EAAE,OAAO,EAAE,WAkBhC,OAAO,SAahC"} -------------------------------------------------------------------------------- /cypress/e2e/filter-by-value/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /dist/filter.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter the values by the given predicate function. 3 | * @param {Function} predicate 4 | */ 5 | export declare function filter(predicate: Function): (list: unknown) => any; 6 | //# sourceMappingURL=filter.d.ts.map -------------------------------------------------------------------------------- /dist/invoke.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../src/invoke.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,UACpC,OAAO,GAAG,OAAO,EAAE,SAmB3C"} -------------------------------------------------------------------------------- /cypress/e2e/duplicates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /dist/pipe.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.pipe = void 0; 4 | function pipe(...fns) { 5 | return function (value) { 6 | return fns.reduce((acc, fn) => fn(acc), value); 7 | }; 8 | } 9 | exports.pipe = pipe; 10 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: false, 5 | viewportHeight: 1100, 6 | e2e: { 7 | setupNodeEvents(on, config) {}, 8 | supportFile: false, 9 | specPattern: 'cypress/e2e/**/*spec.js', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "prHourlyLimit": 2, 5 | "updateNotScheduled": false, 6 | "timezone": "America/New_York", 7 | "schedule": ["after 10pm and before 5am on every weekday", "every weekend"], 8 | "masterIssue": true, 9 | "labels": ["type: dependencies", "renovate"] 10 | } 11 | -------------------------------------------------------------------------------- /dist/map.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms an object or a list of objects using the supplied function or name of the property. 3 | * @param {Function} fn Function to apply to each object 4 | * @returns {Object|Array} Transformed value 5 | * @example cy.get('.todo').then(map('innerText')) 6 | */ 7 | export declare function map(fn: Function): (list: unknown) => any; 8 | //# sourceMappingURL=map.d.ts.map -------------------------------------------------------------------------------- /dist/really.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constructs a "should(callback)" function on the fly from a pipeline 3 | * of individual functions to be called 4 | * @example cy.get(...).should(really(...)) 5 | * @see https://github.com/bahmutov/cypress-should-really 6 | * @returns Function 7 | */ 8 | export declare function really(...functionsAndAssertion: unknown[]): (value: unknown) => any; 9 | //# sourceMappingURL=really.d.ts.map -------------------------------------------------------------------------------- /dist/invoke.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Invokes the given name (with optional arguments) on the given object. 3 | * @param {String} methodName 4 | * @param {...any} args 5 | * @returns Result of the method invocation 6 | * @example 7 | * cy.get('dates') 8 | * .then(map('innerText')) 9 | * .then(toDate) 10 | * .then(invoke('getTime')) 11 | */ 12 | export declare function invoke(methodName: string, ...args: unknown[]): (list: unknown | unknown[]) => any; 13 | //# sourceMappingURL=invoke.d.ts.map -------------------------------------------------------------------------------- /cypress/e2e/children/index.html: -------------------------------------------------------------------------------- 1 | 2 |

Entire parent element is overwritten

3 |
4 |
Parent...
5 |
6 | 17 | 18 | -------------------------------------------------------------------------------- /src/filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter the values by the given predicate function. 3 | * @param {Function} predicate 4 | */ 5 | export function filter(predicate: Function) { 6 | return function (list: unknown) { 7 | if (Cypress._.isArrayLike(list)) { 8 | const callbackFn = 9 | typeof predicate === 'function' 10 | ? (x: unknown) => predicate(x) 11 | : predicate 12 | return Cypress._.filter(list, callbackFn) 13 | } else { 14 | return predicate(list) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cypress/e2e/object-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { really, its } from '../../src' 4 | 5 | describe('Assertion helpers', () => { 6 | it('works with object props', () => { 7 | const p = { 8 | person: { 9 | name: 'Joe', 10 | age: 42, 11 | }, 12 | } 13 | setTimeout(() => { 14 | p.person.age = 90 15 | }, 1000) 16 | cy.wrap(p).should(really(its('person.age'), 'equal', 90)) 17 | // using custom ".map" child command 18 | // cy.wrap(p).map('person.age').should('eq', 90) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms an object or a list of objects using the supplied function or name of the property. 3 | * @param {Function} fn Function to apply to each object 4 | * @returns {Object|Array} Transformed value 5 | * @example cy.get('.todo').then(map('innerText')) 6 | */ 7 | export function map(fn: Function) { 8 | return function (list: unknown) { 9 | if (Cypress._.isArrayLike(list)) { 10 | const callbackFn = typeof fn === 'function' ? (x: unknown) => fn(x) : fn 11 | return Cypress._.map(list, callbackFn) 12 | } else { 13 | return fn(list) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cypress/e2e/json-attribute/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { really, invoke, its } from '../../..' 4 | 5 | // implementation similar to "Json data attribute" recipe 6 | // from https://glebbahmutov.com/cypress-examples 7 | 8 | it('gets the parsed data attribute value', () => { 9 | cy.visit('cypress/e2e/json-attribute/index.html') 10 | // grab the element's attribute "data-field" 11 | // convert it into a JSON object 12 | // and grab its "age" property -> should be equal 10 13 | cy.get('#person').should( 14 | really(invoke('attr', 'data-field'), JSON.parse, its('age'), 'equal', 10), 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-20.04 6 | steps: 7 | - name: Checkout 🛎 8 | uses: actions/checkout@v3 9 | 10 | - name: Run tests 🧪 11 | # https://github.com/cypress-io/github-action 12 | uses: cypress-io/github-action@v4 13 | with: 14 | build: npm run stop-only && npm run build 15 | 16 | - name: Semantic Release 🚀 17 | uses: cycjimmy/semantic-release-action@v3 18 | with: 19 | branch: main 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /dist/filter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.filter = void 0; 4 | /** 5 | * Filter the values by the given predicate function. 6 | * @param {Function} predicate 7 | */ 8 | function filter(predicate) { 9 | return function (list) { 10 | if (Cypress._.isArrayLike(list)) { 11 | const callbackFn = typeof predicate === 'function' 12 | ? (x) => predicate(x) 13 | : predicate; 14 | return Cypress._.filter(list, callbackFn); 15 | } 16 | else { 17 | return predicate(list); 18 | } 19 | }; 20 | } 21 | exports.filter = filter; 22 | -------------------------------------------------------------------------------- /.github/workflows/badges.yml: -------------------------------------------------------------------------------- 1 | name: badges 2 | on: 3 | schedule: 4 | # update badges every night 5 | # because we have a few badges that are linked 6 | # to the external repositories 7 | - cron: '0 3 * * *' 8 | 9 | jobs: 10 | badges: 11 | name: Badges 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Checkout 🛎 15 | uses: actions/checkout@v3 16 | 17 | - name: Update version badges 🏷 18 | run: npx -p dependency-version-badge update-badge cypress 19 | 20 | - name: Commit any changed files 💾 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Updated badges 24 | branch: main 25 | file_pattern: README.md 26 | -------------------------------------------------------------------------------- /dist/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,OACV,MAAM,aAG3B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,OACf,MAAM,aAG3B;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,aAAa,EAAE,GAAG,iBACV,GAAG,aAGlC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,OACvB,OAAO,KAAK,OAAO,SAGxC;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,QAE/B;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,EAAE,EAAE,QAAQ,OACV,OAAO,aAI5B;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,OAE/C;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,WAAW,EAAE,QAAQ,SACvB,OAAO,SAI9B;AAED,cAAc,QAAQ,CAAA;AACtB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA"} -------------------------------------------------------------------------------- /dist/map.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.map = void 0; 4 | /** 5 | * Transforms an object or a list of objects using the supplied function or name of the property. 6 | * @param {Function} fn Function to apply to each object 7 | * @returns {Object|Array} Transformed value 8 | * @example cy.get('.todo').then(map('innerText')) 9 | */ 10 | function map(fn) { 11 | return function (list) { 12 | if (Cypress._.isArrayLike(list)) { 13 | const callbackFn = typeof fn === 'function' ? (x) => fn(x) : fn; 14 | return Cypress._.map(list, callbackFn); 15 | } 16 | else { 17 | return fn(list); 18 | } 19 | }; 20 | } 21 | exports.map = map; 22 | -------------------------------------------------------------------------------- /cypress/e2e/parse-search-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { construct, invoke, pipe, really } from '../../src' 4 | 5 | // reusable function for converting search params to an object 6 | const searchToPlain = pipe( 7 | // string like "?foo=bar&baz=qux" 8 | construct(URLSearchParams), // URLSearchParams 9 | invoke('entries'), // Iterable<[string, string]> 10 | Array.from, // Array<[string, string]> 11 | Cypress._.fromPairs, // { [key: string]: string }) 12 | ) 13 | 14 | it( 15 | 'parses the URL search part', 16 | { baseUrl: 'https://example.cypress.io/' }, 17 | () => { 18 | // we assume the page does not redirect and does not lose the search part 19 | cy.visit('/commands/location.html?foo=bar&baz=qux') 20 | cy.location('search') 21 | .should('equal', '?foo=bar&baz=qux') 22 | .and( 23 | really(searchToPlain, 'deep.equal', { 24 | foo: 'bar', 25 | baz: 'qux', 26 | }), 27 | ) 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /cypress/e2e/tap-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { tap, really, its } from '../../src' 4 | 5 | describe('tap', () => { 6 | it('calls the given function', () => { 7 | const stub = cy.stub().returns(2) 8 | cy.wrap(1) 9 | .then(tap(stub)) 10 | .should('equal', 1) 11 | .then(() => { 12 | expect(stub).to.be.calledOnceWith(1) 13 | }) 14 | }) 15 | 16 | it('helps during debugging', () => { 17 | const stub = cy.stub(console, 'log') 18 | const o = { 19 | name: 'Joe', 20 | } 21 | cy.wrap(o) 22 | .should(really(its('name'), tap(console.log), 'equal', 'Mary')) 23 | .then(() => { 24 | expect(stub).to.be.calledWith('Joe') // was called multiple times 25 | expect(stub).to.be.calledWith('Mary') // was called just once before the test passes 26 | }) 27 | // change the name to Mary after some time 28 | setTimeout(() => { 29 | o.name = 'Mary' 30 | }, 1000) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/invoke.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Invokes the given name (with optional arguments) on the given object. 3 | * @param {String} methodName 4 | * @param {...any} args 5 | * @returns Result of the method invocation 6 | * @example 7 | * cy.get('dates') 8 | * .then(map('innerText')) 9 | * .then(toDate) 10 | * .then(invoke('getTime')) 11 | */ 12 | export function invoke(methodName: string, ...args: unknown[]) { 13 | return function (list: unknown | unknown[]) { 14 | if (arguments.length > 1) { 15 | // the user tried to pass extra arguments with the list/object 16 | // that is a mistake! 17 | throw new Error(`Call to "${methodName}" must have a single argument`) 18 | } 19 | 20 | // @ts-ignore 21 | if (typeof list[methodName] === 'function') { 22 | // @ts-ignore 23 | return list[methodName](...args) 24 | } 25 | 26 | if (Cypress._.isArrayLike(list)) { 27 | return Cypress._.invokeMap(list, methodName, ...args) 28 | } else { 29 | return Cypress._.invoke(list, methodName, ...args) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 bahmutov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cypress/e2e/table-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // @ts-ignore 4 | import { really, map, invoke, toDate } from '../..' 5 | chai.use(require('chai-sorted')) 6 | 7 | describe('Assertion helpers', () => { 8 | beforeEach(() => { 9 | cy.visit('/cypress/e2e/table') 10 | cy.get('#numrows').select('100') 11 | // check if the table fits into one page 12 | cy.get('.pagecontroller-num').should('have.length', 1) 13 | }) 14 | 15 | it('map elements', () => { 16 | // sort by clicking the header column 17 | cy.contains('.sorterHeader', 'Points').click() 18 | cy.get('tbody td + td + td + td + td').should( 19 | really(map('innerText'), map(parseFloat), 'be.ascending'), 20 | ) 21 | }) 22 | 23 | it('map by date', () => { 24 | cy.contains('.sorterHeader', 'Date').click() 25 | 26 | // just to see what the steps produce 27 | // cy.get('tbody td:nth-child(4)') 28 | // .then(map('innerText')) 29 | // .then(map(toDate)) 30 | // .then(invoke('getTime')) 31 | // .then(cy.log) 32 | 33 | cy.get('tbody td:nth-child(4)').should( 34 | really(map('innerText'), map(toDate), invoke('getTime'), 'be.ascending'), 35 | ) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-should-really", 3 | "version": "0.0.0-development", 4 | "description": "Functional helpers for constructing Cypress should callbacks", 5 | "main": "dist", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "src", 9 | "dist" 10 | ], 11 | "scripts": { 12 | "test": "cypress run", 13 | "semantic-release": "semantic-release", 14 | "stop-only": "stop-only --folder cypress/e2e", 15 | "build": "tsc -b", 16 | "watch": "tsc -w", 17 | "lint": "tsc --noEmit" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/bahmutov/cypress-should-really.git" 22 | }, 23 | "keywords": [ 24 | "cypress-plugin" 25 | ], 26 | "author": "Gleb Bahmutov ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/bahmutov/cypress-should-really/issues" 30 | }, 31 | "homepage": "https://github.com/bahmutov/cypress-should-really#readme", 32 | "devDependencies": { 33 | "chai-sorted": "0.2.0", 34 | "cypress": "^12.1.0", 35 | "prettier": "2.8.4", 36 | "semantic-release": "20.1.1", 37 | "stop-only": "3.3.1", 38 | "typescript": "^4.9.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /dist/invoke.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.invoke = void 0; 4 | /** 5 | * Invokes the given name (with optional arguments) on the given object. 6 | * @param {String} methodName 7 | * @param {...any} args 8 | * @returns Result of the method invocation 9 | * @example 10 | * cy.get('dates') 11 | * .then(map('innerText')) 12 | * .then(toDate) 13 | * .then(invoke('getTime')) 14 | */ 15 | function invoke(methodName, ...args) { 16 | return function (list) { 17 | if (arguments.length > 1) { 18 | // the user tried to pass extra arguments with the list/object 19 | // that is a mistake! 20 | throw new Error(`Call to "${methodName}" must have a single argument`); 21 | } 22 | // @ts-ignore 23 | if (typeof list[methodName] === 'function') { 24 | // @ts-ignore 25 | return list[methodName](...args); 26 | } 27 | if (Cypress._.isArrayLike(list)) { 28 | return Cypress._.invokeMap(list, methodName, ...args); 29 | } 30 | else { 31 | return Cypress._.invoke(list, methodName, ...args); 32 | } 33 | }; 34 | } 35 | exports.invoke = invoke; 36 | -------------------------------------------------------------------------------- /cypress/e2e/table/app.js: -------------------------------------------------------------------------------- 1 | // generate random but consistent users 2 | // https://github.com/Marak/faker.js#setting-a-randomness-seed 3 | const users = [] 4 | 5 | faker.seed(123) 6 | for (let i = 0; i < 23; i++) { 7 | users.push({ 8 | id: faker.random.number(1000), 9 | firstName: faker.name.firstName(), 10 | lastName: faker.name.lastName(), 11 | date: faker.date.past().toISOString().split('T')[0], 12 | points: faker.random.number(99), 13 | }) 14 | } 15 | // pad the numbers with leading zeros 16 | // because the sorting is alphanumeric only, does not work with numbers 17 | const rows = users 18 | .map((user) => { 19 | return ` 20 | 21 | ${String(user.id).padStart(4, '0')} 22 | ${user.firstName} 23 | ${user.lastName} 24 | ${user.date} 25 | ${String(user.points).padStart(2, '0')} 26 | 27 | ` 28 | }) 29 | .join('\n') 30 | 31 | $('.tablemanager tbody').html(rows) 32 | 33 | $('.tablemanager').tablemanager({ 34 | firstSort: [ 35 | [3, 0], 36 | [2, 0], 37 | [1, 'asc'], 38 | ], 39 | disable: [], 40 | appendFilterby: true, 41 | dateFormat: [[3, 'yyyy-mm-dd']], 42 | debug: false, 43 | vocabulary: { 44 | voc_filter_by: 'Filter By', 45 | voc_type_here_filter: 'Filter...', 46 | voc_show_rows: 'Rows Per Page', 47 | }, 48 | pagination: true, 49 | showrows: [5, 10, 20, 50, 100], 50 | disableFilterBy: [1], 51 | }) 52 | -------------------------------------------------------------------------------- /cypress/e2e/table/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto Condensed', Helvetica, sans-serif; 3 | background-color: #f7f7f7; 4 | } 5 | .container { 6 | margin: 150px auto; 7 | max-width: 960px; 8 | } 9 | a { 10 | text-decoration: none; 11 | } 12 | table { 13 | width: 100%; 14 | border-collapse: collapse; 15 | margin-top: 20px; 16 | margin-bottom: 20px; 17 | } 18 | table, 19 | th, 20 | td { 21 | border: 1px solid #bbb; 22 | text-align: left; 23 | } 24 | tr:nth-child(even) { 25 | background-color: #f2f2f2; 26 | } 27 | th { 28 | background-color: #ddd; 29 | } 30 | th, 31 | td { 32 | padding: 5px; 33 | } 34 | button { 35 | cursor: pointer; 36 | } 37 | /*Initial style sort*/ 38 | .tablemanager th.sorterHeader { 39 | cursor: pointer; 40 | } 41 | .tablemanager th.sorterHeader:after { 42 | content: ' \f0dc'; 43 | font-family: 'FontAwesome'; 44 | } 45 | /*Style sort desc*/ 46 | .tablemanager th.sortingDesc:after { 47 | content: ' \f0dd'; 48 | font-family: 'FontAwesome'; 49 | } 50 | /*Style sort asc*/ 51 | .tablemanager th.sortingAsc:after { 52 | content: ' \f0de'; 53 | font-family: 'FontAwesome'; 54 | } 55 | /*Style disabled*/ 56 | .tablemanager th.disableSort { 57 | } 58 | #for_numrows { 59 | padding: 10px; 60 | float: left; 61 | } 62 | #for_filter_by { 63 | padding: 10px; 64 | float: right; 65 | } 66 | #pagesControllers { 67 | display: block; 68 | text-align: center; 69 | } 70 | .currentPage { 71 | font-weight: bold; 72 | background-color: #fff; 73 | } 74 | -------------------------------------------------------------------------------- /src/really.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { pipe } from './pipe' 4 | 5 | /** 6 | * Constructs a "should(callback)" function on the fly from a pipeline 7 | * of individual functions to be called 8 | * @example cy.get(...).should(really(...)) 9 | * @see https://github.com/bahmutov/cypress-should-really 10 | * @returns Function 11 | */ 12 | export function really(...functionsAndAssertion: unknown[]) { 13 | if (!arguments.length) { 14 | throw new Error('really() needs arguments really badly') 15 | } 16 | 17 | const fns = Cypress._.takeWhile(arguments, (arg) => typeof arg === 'function') 18 | const chainerIndex = Cypress._.findIndex( 19 | arguments, 20 | (arg) => typeof arg === 'string', 21 | ) 22 | if (chainerIndex === -1) { 23 | throw new Error('sh: no chainer found') 24 | } 25 | const chainer = arguments[chainerIndex] 26 | const chainerArguments = Cypress._.slice(arguments, chainerIndex + 1) 27 | const chainers = chainer.split('.') 28 | const fn = pipe(...fns) 29 | 30 | return function (value: unknown) { 31 | // console.log('value', value) 32 | const transformed = fn(value) 33 | // console.log('transformed', transformed) 34 | return chainers.reduce((acc: any, chainer: string) => { 35 | const currentChainer = acc[chainer] 36 | if (typeof currentChainer === 'function') { 37 | return acc[chainer](...chainerArguments) 38 | } else { 39 | return acc[chainer] 40 | } 41 | }, expect(transformed).to) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cypress/e2e/filter-by-value/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // normally you would import from "cypress-should-really" 4 | import { really, map, its, filter, isEqual } from '../../..' 5 | 6 | // find the explanation in the video 7 | // "Filter Input Elements By Value Using cypress-should-really Plugin" 8 | // https://youtu.be/Gxoo6uZMo9I 9 | 10 | it( 11 | 'finds input elements with the current value "fox"', 12 | { viewportHeight: 200, viewportWidth: 200 }, 13 | () => { 14 | cy.visit('cypress/e2e/filter-by-value/index.html') 15 | // change one of the inputs by typing "fox" into it 16 | cy.get('#i2').type('fox') 17 | // NOTE: only the elements with the markup attribute "value" are returned 18 | cy.get('#inputs input[value=fox]').should('have.length', 1) 19 | 20 | // instead filter the elements by their current value 21 | // first approach: use separate commands 22 | // not ideal, since only the cy.filter is retried 23 | // which loses the Cypress retry-ability 24 | // https://on.cypress.io/retry-ability 25 | cy.get('#inputs input') 26 | .filter((k, el) => { 27 | return el.value === 'fox' 28 | }) 29 | // finds both input elements with the value "fox" 30 | .should('have.length', 2) 31 | 32 | // second approach: build up and use 33 | // a single custom "should(callback)" 34 | cy.get('#inputs input').should( 35 | // [jqueryElement] 36 | really( 37 | map(its('value')), // [string] 38 | filter(isEqual('fox')), 39 | 'have.length', 40 | 2, 41 | ), 42 | ) 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /dist/really.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /// 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.really = void 0; 5 | const pipe_1 = require("./pipe"); 6 | /** 7 | * Constructs a "should(callback)" function on the fly from a pipeline 8 | * of individual functions to be called 9 | * @example cy.get(...).should(really(...)) 10 | * @see https://github.com/bahmutov/cypress-should-really 11 | * @returns Function 12 | */ 13 | function really(...functionsAndAssertion) { 14 | if (!arguments.length) { 15 | throw new Error('really() needs arguments really badly'); 16 | } 17 | const fns = Cypress._.takeWhile(arguments, (arg) => typeof arg === 'function'); 18 | const chainerIndex = Cypress._.findIndex(arguments, (arg) => typeof arg === 'string'); 19 | if (chainerIndex === -1) { 20 | throw new Error('sh: no chainer found'); 21 | } 22 | const chainer = arguments[chainerIndex]; 23 | const chainerArguments = Cypress._.slice(arguments, chainerIndex + 1); 24 | const chainers = chainer.split('.'); 25 | const fn = (0, pipe_1.pipe)(...fns); 26 | return function (value) { 27 | // console.log('value', value) 28 | const transformed = fn(value); 29 | // console.log('transformed', transformed) 30 | return chainers.reduce((acc, chainer) => { 31 | const currentChainer = acc[chainer]; 32 | if (typeof currentChainer === 'function') { 33 | return acc[chainer](...chainerArguments); 34 | } 35 | else { 36 | return acc[chainer]; 37 | } 38 | }, expect(transformed).to); 39 | }; 40 | } 41 | exports.really = really; 42 | -------------------------------------------------------------------------------- /cypress/e2e/table/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | Table Manager Plugin Example 11 | 16 | 17 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |

Table Manager Plugin Example

28 |

29 | A simple yet powerful jQuery table management plugin that provides an 30 | easy way to sort/filter/paginate tabular data in an HTML table. 31 |

32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
IDFirst NameLast NameDatePoints
48 |
49 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /cypress/e2e/duplicates/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { 4 | really, 5 | invoke, 6 | map, 7 | tap, 8 | greaterThan, 9 | partial, 10 | pipe, 11 | flipTwoArguments, 12 | } from '../../..' 13 | const { countBy, pickBy } = Cypress._ 14 | 15 | describe( 16 | 'finding duplicates', 17 | { viewportHeight: 100, viewportWidth: 200 }, 18 | () => { 19 | it('checks if an object is empty', () => { 20 | const p = pipe(countBy, (counts) => pickBy(counts, (n) => n > 1)) 21 | const output = p(['a', 'b', 'c', 'a']) 22 | expect(output).to.be.deep.equal({ a: 2 }) 23 | }) 24 | 25 | it('by attribute (explicit)', () => { 26 | cy.visit('cypress/e2e/duplicates/index.html') 27 | 28 | cy.get('li').should( 29 | really( 30 | map(invoke('getAttribute', 'data-product-id')), 31 | countBy, 32 | (counts) => pickBy(counts, (n) => n > 1), 33 | // if you want to debug this pipeline of functions 34 | // use tap(console.log) function 35 | tap(console.log), 36 | 'be.empty', 37 | ), 38 | ) 39 | }) 40 | 41 | it('by attribute (greaterThan)', () => { 42 | cy.visit('cypress/e2e/duplicates/index.html') 43 | 44 | // using a few more shortcuts 45 | cy.get('li').should( 46 | really( 47 | map(invoke('getAttribute', 'data-product-id')), 48 | countBy, 49 | (counts) => pickBy(counts, greaterThan(1)), 50 | 'be.empty', 51 | ), 52 | ) 53 | }) 54 | 55 | it('by attribute (flip arguments and partial apply)', () => { 56 | cy.visit('cypress/e2e/duplicates/index.html') 57 | // modify the _.pickBy to take arguments in 58 | // the flipped order: (fn, array) 59 | // then we know the first argument - greaterThan(1) function 60 | // so we apply it right away. The returned function 61 | // is waiting for the object 62 | const pickLargerThanOne = partial( 63 | flipTwoArguments(pickBy), 64 | greaterThan(1), 65 | ) 66 | cy.get('li').should( 67 | really( 68 | map(invoke('getAttribute', 'data-product-id')), 69 | countBy, 70 | pickLargerThanOne, 71 | 'be.empty', 72 | ), 73 | ) 74 | }) 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /cypress/e2e/invoke-spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { invoke, map, its, pipe } from '../..' 4 | 5 | describe('invoke', () => { 6 | it('works on arrays', () => { 7 | const strings = ['a', 'bb', 'ccc'] 8 | const results = invoke('toUpperCase')(strings) 9 | expect(results).to.deep.equal(['A', 'BB', 'CCC']) 10 | }) 11 | 12 | it('works on jQuery', () => { 13 | const $ = Cypress.$(` 14 |
    15 |
  • a
  • 16 |
  • bb
  • 17 |
  • ccc
  • 18 |
19 | `) 20 | const li = invoke('find', 'li')($) 21 | const strings = map('innerText')(li) 22 | expect(strings).to.deep.equal(['a', 'bb', 'ccc']) 23 | 24 | // note that calling "text()" on jQuery element 25 | // returns a single string 26 | cy.wrap($) 27 | .then(invoke('find', 'li')) 28 | .then(invoke('text')) 29 | .should('deep.equal', 'abbccc') 30 | }) 31 | 32 | it('passes arguments', () => { 33 | const calc = { 34 | add(a, b) { 35 | return a + b 36 | }, 37 | } 38 | const sum = invoke('add', 1, 2)(calc) 39 | expect(sum).to.equal(3) 40 | }) 41 | 42 | it('throws an error if final call is not unary', () => { 43 | // command mistake - passing arguments to the final call 44 | // instead of preparing it with the name of the method 45 | expect(() => { 46 | invoke('something')({}, 1, 2) 47 | }).to.throw('Call to "something" must have a single argument') 48 | }) 49 | 50 | it('can be combined with map', () => { 51 | const $ = Cypress.$(` 52 |
    53 |
  • a
  • 54 |
  • bb
  • 55 |
  • ccc
  • 56 |
57 | `) 58 | const li = invoke('find', 'li')($) 59 | // when we map, we get the regular DOM elements 60 | const strings = map(its('innerText'))(li) 61 | const upper = map(invoke('toUpperCase'))(strings) 62 | expect(upper).to.deep.equal(['A', 'BB', 'CCC']) 63 | }) 64 | 65 | it('can be combined with pipe', () => { 66 | const $ = Cypress.$(` 67 |
    68 |
  • a
  • 69 |
  • bb
  • 70 |
  • ccc
  • 71 |
72 | `) 73 | 74 | const toStrings = pipe(invoke('find', 'li'), map('innerText')) 75 | const strings = toStrings($) 76 | expect(strings).to.deep.equal(['a', 'bb', 'ccc']) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /cypress/e2e/children/children-spec.js: -------------------------------------------------------------------------------- 1 | import { really, invoke } from '../../..' 2 | 3 | describe( 4 | 'count children elements', 5 | { viewportHeight: 300, viewportWidth: 300 }, 6 | () => { 7 | // fails because the entire parent element is replaced 8 | it.skip('finds the children elements even if the entire element is replaced', () => { 9 | cy.visit('cypress/e2e/children/index.html') 10 | cy.get('[data-cy=parent]') 11 | .should('be.visible') 12 | .contains('[data-cy=first]', 'First') 13 | .should('be.visible') 14 | }) 15 | 16 | it('retries until the parent element has two children', () => { 17 | cy.visit('cypress/e2e/children/index.html') 18 | // once we confirm the parent element has two children with data-cy attribute 19 | // we can continue 20 | cy.get('[data-cy=parent] [data-cy]').should('have.length', 2) 21 | cy.get('[data-cy=parent]') 22 | .contains('[data-cy=first]', 'First') 23 | .should('be.visible') 24 | }) 25 | 26 | it('finds the children after wait', () => { 27 | cy.visit('cypress/e2e/children/index.html') 28 | // let the page update itself including the parent element 29 | .wait(2000) 30 | cy.get('[data-cy=parent]') 31 | .invoke('find', '[data-cy]') 32 | .should('have.length', 2) 33 | }) 34 | 35 | it('really retries until the parent element has two children', () => { 36 | cy.visit('cypress/e2e/children/index.html') 37 | cy.get('[data-cy=parent]') 38 | .should(really(invoke('find', '[data-cy]'), 'have.length', 2)) 39 | .contains('[data-cy=first]', 'First') 40 | }) 41 | 42 | it('really finds a child element with text', () => { 43 | cy.visit('cypress/e2e/children/index.html') 44 | cy.get('[data-cy=parent]') 45 | // find the element with text "Second" 46 | // should exist 47 | .should(really(invoke('find', ':contains(Second)'), 'exist')) 48 | .contains('[data-cy=second]', 'Second') 49 | }) 50 | 51 | it('really finds text inside', () => { 52 | cy.visit('cypress/e2e/children/index.html') 53 | cy.get('[data-cy=parent]') 54 | // take the text inside the element 55 | // find the word "Second" 56 | // should be true 57 | .should(really(invoke('text'), invoke('includes', 'Second'), 'be.true')) 58 | .contains('[data-cy=second]', 'Second') 59 | }) 60 | }, 61 | ) 62 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Grabs a property or a nested path from the given object. 3 | * @param {String} path 4 | * @returns Value of the property 5 | * @example 6 | * cy.wrap({ foo: 'bar' }).then(its('foo')) 7 | */ 8 | export declare function its(path: string): (o: object) => unknown; 9 | /** 10 | * Curried > N function 11 | * @param {number} n 12 | * @returns Boolean 13 | * @example 14 | * expect(greaterThan(10)(5)).to.be.false 15 | */ 16 | export declare function greaterThan(n: number): (x: number) => boolean; 17 | /** 18 | * Curried deep comparison 19 | * @param {any} isEqual 20 | */ 21 | export declare function isEqual(expectedValue: any): (actualValue: any) => boolean; 22 | /** 23 | * Takes a function and returns a function that expects the first two 24 | * arguments in the reverse order. 25 | * @param {Function} fn Function to call 26 | * @returns Function 27 | * @example 28 | * flipTwoArguments(Cypress._.map)(x => x * 2, [1, 2, 3]) 29 | */ 30 | export declare function flipTwoArguments(fn: Function): (a: unknown, b: unknown) => any; 31 | /** 32 | * Converts the given string into a JavaScript Date object 33 | * @param {String} s dateString 34 | * @returns {Date} Date instance 35 | * @deprecated Use "constructor(Date)" instead 36 | */ 37 | export declare function toDate(s: string): Date; 38 | /** 39 | * Returns a function that waits for the argument, passes that argument 40 | * to the given callback, but returns the original value. Useful 41 | * for debugging data transformations. 42 | * @param {Function} fn 43 | * @example cyw.wrap(1).then(tap(console.log)).should('equal', 1) 44 | */ 45 | export declare function tap(fn: Function): (x: unknown) => unknown; 46 | /** 47 | * Returns a function with the first argument bound. 48 | * @param {Function} fn Function to partially apply 49 | * @param {any} a First argument to apply 50 | * @example 51 | * const add = (a, b) => a + b 52 | * const addOne = partial(add, 1) 53 | * addOne(2) // 3 54 | */ 55 | export declare function partial(fn: Function, a: unknown): any; 56 | /** 57 | * Given a constructor function, returns a function 58 | * that waits for a single argument before calling "new constructor(arg)" 59 | * @example constructor(Date) 60 | * @see https://glebbahmutov.com/blog/work-around-the-keyword-new-in-javascript/ 61 | */ 62 | export declare function construct(constructor: Function): (arg: unknown) => any; 63 | export * from './pipe'; 64 | export * from './really'; 65 | export * from './map'; 66 | export * from './filter'; 67 | export * from './invoke'; 68 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * Grabs a property or a nested path from the given object. 5 | * @param {String} path 6 | * @returns Value of the property 7 | * @example 8 | * cy.wrap({ foo: 'bar' }).then(its('foo')) 9 | */ 10 | export function its(path: string) { 11 | return function (o: object) { 12 | return Cypress._.property(path)(o) 13 | } 14 | } 15 | 16 | /** 17 | * Curried > N function 18 | * @param {number} n 19 | * @returns Boolean 20 | * @example 21 | * expect(greaterThan(10)(5)).to.be.false 22 | */ 23 | export function greaterThan(n: number) { 24 | return function (x: number) { 25 | return x > n 26 | } 27 | } 28 | 29 | /** 30 | * Curried deep comparison 31 | * @param {any} isEqual 32 | */ 33 | export function isEqual(expectedValue: any) { 34 | return function (actualValue: any) { 35 | return Cypress._.isEqual(actualValue, expectedValue) 36 | } 37 | } 38 | 39 | /** 40 | * Takes a function and returns a function that expects the first two 41 | * arguments in the reverse order. 42 | * @param {Function} fn Function to call 43 | * @returns Function 44 | * @example 45 | * flipTwoArguments(Cypress._.map)(x => x * 2, [1, 2, 3]) 46 | */ 47 | export function flipTwoArguments(fn: Function) { 48 | return function (a: unknown, b: unknown) { 49 | return fn(b, a) 50 | } 51 | } 52 | 53 | /** 54 | * Converts the given string into a JavaScript Date object 55 | * @param {String} s dateString 56 | * @returns {Date} Date instance 57 | * @deprecated Use "constructor(Date)" instead 58 | */ 59 | export function toDate(s: string) { 60 | return new Date(s) 61 | } 62 | 63 | /** 64 | * Returns a function that waits for the argument, passes that argument 65 | * to the given callback, but returns the original value. Useful 66 | * for debugging data transformations. 67 | * @param {Function} fn 68 | * @example cyw.wrap(1).then(tap(console.log)).should('equal', 1) 69 | */ 70 | export function tap(fn: Function) { 71 | return function (x: unknown) { 72 | fn(x) 73 | return x 74 | } 75 | } 76 | 77 | /** 78 | * Returns a function with the first argument bound. 79 | * @param {Function} fn Function to partially apply 80 | * @param {any} a First argument to apply 81 | * @example 82 | * const add = (a, b) => a + b 83 | * const addOne = partial(add, 1) 84 | * addOne(2) // 3 85 | */ 86 | export function partial(fn: Function, a: unknown) { 87 | return fn.bind(null, a) 88 | } 89 | 90 | /** 91 | * Given a constructor function, returns a function 92 | * that waits for a single argument before calling "new constructor(arg)" 93 | * @example constructor(Date) 94 | * @see https://glebbahmutov.com/blog/work-around-the-keyword-new-in-javascript/ 95 | */ 96 | export function construct(constructor: Function) { 97 | return function (arg: unknown) { 98 | // @ts-ignore 99 | return new constructor(arg) 100 | } 101 | } 102 | 103 | export * from './pipe' 104 | export * from './really' 105 | export * from './map' 106 | export * from './filter' 107 | export * from './invoke' 108 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /// 3 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 4 | if (k2 === undefined) k2 = k; 5 | var desc = Object.getOwnPropertyDescriptor(m, k); 6 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 7 | desc = { enumerable: true, get: function() { return m[k]; } }; 8 | } 9 | Object.defineProperty(o, k2, desc); 10 | }) : (function(o, m, k, k2) { 11 | if (k2 === undefined) k2 = k; 12 | o[k2] = m[k]; 13 | })); 14 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 15 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 16 | }; 17 | Object.defineProperty(exports, "__esModule", { value: true }); 18 | exports.construct = exports.partial = exports.tap = exports.toDate = exports.flipTwoArguments = exports.isEqual = exports.greaterThan = exports.its = void 0; 19 | /** 20 | * Grabs a property or a nested path from the given object. 21 | * @param {String} path 22 | * @returns Value of the property 23 | * @example 24 | * cy.wrap({ foo: 'bar' }).then(its('foo')) 25 | */ 26 | function its(path) { 27 | return function (o) { 28 | return Cypress._.property(path)(o); 29 | }; 30 | } 31 | exports.its = its; 32 | /** 33 | * Curried > N function 34 | * @param {number} n 35 | * @returns Boolean 36 | * @example 37 | * expect(greaterThan(10)(5)).to.be.false 38 | */ 39 | function greaterThan(n) { 40 | return function (x) { 41 | return x > n; 42 | }; 43 | } 44 | exports.greaterThan = greaterThan; 45 | /** 46 | * Curried deep comparison 47 | * @param {any} isEqual 48 | */ 49 | function isEqual(expectedValue) { 50 | return function (actualValue) { 51 | return Cypress._.isEqual(actualValue, expectedValue); 52 | }; 53 | } 54 | exports.isEqual = isEqual; 55 | /** 56 | * Takes a function and returns a function that expects the first two 57 | * arguments in the reverse order. 58 | * @param {Function} fn Function to call 59 | * @returns Function 60 | * @example 61 | * flipTwoArguments(Cypress._.map)(x => x * 2, [1, 2, 3]) 62 | */ 63 | function flipTwoArguments(fn) { 64 | return function (a, b) { 65 | return fn(b, a); 66 | }; 67 | } 68 | exports.flipTwoArguments = flipTwoArguments; 69 | /** 70 | * Converts the given string into a JavaScript Date object 71 | * @param {String} s dateString 72 | * @returns {Date} Date instance 73 | * @deprecated Use "constructor(Date)" instead 74 | */ 75 | function toDate(s) { 76 | return new Date(s); 77 | } 78 | exports.toDate = toDate; 79 | /** 80 | * Returns a function that waits for the argument, passes that argument 81 | * to the given callback, but returns the original value. Useful 82 | * for debugging data transformations. 83 | * @param {Function} fn 84 | * @example cyw.wrap(1).then(tap(console.log)).should('equal', 1) 85 | */ 86 | function tap(fn) { 87 | return function (x) { 88 | fn(x); 89 | return x; 90 | }; 91 | } 92 | exports.tap = tap; 93 | /** 94 | * Returns a function with the first argument bound. 95 | * @param {Function} fn Function to partially apply 96 | * @param {any} a First argument to apply 97 | * @example 98 | * const add = (a, b) => a + b 99 | * const addOne = partial(add, 1) 100 | * addOne(2) // 3 101 | */ 102 | function partial(fn, a) { 103 | return fn.bind(null, a); 104 | } 105 | exports.partial = partial; 106 | /** 107 | * Given a constructor function, returns a function 108 | * that waits for a single argument before calling "new constructor(arg)" 109 | * @example constructor(Date) 110 | * @see https://glebbahmutov.com/blog/work-around-the-keyword-new-in-javascript/ 111 | */ 112 | function construct(constructor) { 113 | return function (arg) { 114 | // @ts-ignore 115 | return new constructor(arg); 116 | }; 117 | } 118 | exports.construct = construct; 119 | __exportStar(require("./pipe"), exports); 120 | __exportStar(require("./really"), exports); 121 | __exportStar(require("./map"), exports); 122 | __exportStar(require("./filter"), exports); 123 | __exportStar(require("./invoke"), exports); 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-should-really [![ci](https://github.com/bahmutov/cypress-should-really/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/bahmutov/cypress-should-really/actions/workflows/ci.yml) ![cypress version](https://img.shields.io/badge/cypress-12.1.0-brightgreen) [![renovate-app badge][renovate-badge]][renovate-app] 2 | 3 | Read the blog posts [Functional Helpers For Cypress Tests](https://glebbahmutov.com/blog/fp-cy-helpers/) and [Check Items For Duplicates](https://glebbahmutov.com/blog/check-for-duplicates/). 🎓 Covered in my course [Cypress Plugins](https://cypress.tips/courses/cypress-plugins). 4 | 5 | ## Example 6 | 7 | Grab text from each list item, convert the strings into Dates, convert Dates into timestamps, then confirm they are sorted using [chai-sorted](https://www.chaijs.com/plugins/chai-sorted/) assertion. 8 | 9 | ```js 10 | import { innerText, toDate, invoke } from 'cypress-should-really' 11 | cy.get('.dates') 12 | .then(map('innerText')) 13 | .then(map(toDate)) 14 | .then(invoke('getDate')) 15 | .should('be.sorted') 16 | ``` 17 | 18 | The above separates each operation using [cy.then](https://on.cypress.io/then) commands, which are not retried. Luckily, we can easily combine the individual steps into a single data transformation function using the `pipe` command. 19 | 20 | ```js 21 | import { innerText, toDate, invoke, pipe } from 'cypress-should-really' 22 | const transform = pipe(map('innerText'), map(toDate), invoke('getDate')) 23 | cy.get('.dates').then(transform).should('be.sorted') 24 | ``` 25 | 26 | The above commands are still NOT retrying the first `cy.get` command, thus if the page changes, the assertion still fails since it never "sees" the changed elements. We need to remove the `.then(transform)` step and directly tie the `cy.get` command to the assertion. We can move the data transformation into the assertion callback that transforms the data AND runs the assertion using `really` function. 27 | 28 | ```js 29 | import { innerText, toDate, invoke, pipe, really } from 'cypress-should-really' 30 | const transform = pipe(map('innerText'), map(toDate), invoke('getDate')) 31 | cy.get('.dates').should(really(transform, 'be.sorted')) 32 | ``` 33 | 34 | Finally, we can skip using the `pipe` function, since it is built into the `really` automatically. All functions before the assertion are applied and then the assertion runs. 35 | 36 | ```js 37 | import { innerText, toDate, invoke, really } from 'cypress-should-really' 38 | cy.get('.dates').should( 39 | really(map('innerText'), map(toDate), invoke('getDate'), 'be.sorted'), 40 | ) 41 | ``` 42 | 43 | ## Installation 44 | 45 | ```text 46 | $ npm i -D cypress-should-really 47 | # or install using Yarn 48 | $ yarn add -D cypress-should-really 49 | ``` 50 | 51 | ## API 52 | 53 | - `map` 54 | - `invoke` 55 | - `constructor` 56 | - `toDate` (deprecated) use `constructor(Date)` instead 57 | - `its` 58 | - `greaterThan` 59 | - `flipTwoArguments` 60 | - `partial` 61 | - `pipe` 62 | - `tap` 63 | - `filter` 64 | - `isEqual` 65 | - `really` 66 | 67 | ## invoke 68 | 69 | `invoke(, ...arguments)` returns a function that waits for an object or an array, then calls the method and returns the results 70 | 71 | ```js 72 | const calc = { 73 | add(a, b) { 74 | return a + b 75 | }, 76 | } 77 | invoke('add', 1, 2)(calc) 78 | // 3 79 | ``` 80 | 81 | See [invoke-spec.js](./cypress/e2e/invoke-spec.js) 82 | 83 | ## constructor 84 | 85 | Takes a constructor function, returns a function that waits for a single argument and calls with the `new` keyword. 86 | 87 | ```js 88 | import {constructor} from 'cypress-should-really' 89 | // converts a string to Date object 90 | .then(constructor(Date)) 91 | ``` 92 | 93 | ## tap 94 | 95 | Passes the argument into the given function, but returns the original argument. Useful for debugging pipes of functions - insert it in every place of the pipeline to see the values. 96 | 97 | ```js 98 | const o = { 99 | name: 'Joe', 100 | } 101 | cy.wrap(o).should(really(its('name'), tap(console.log), 'equal', 'Mary')) 102 | // change the name to Mary after some time 103 | setTimeout(() => { 104 | o.name = 'Mary' 105 | }, 1000) 106 | ``` 107 | 108 | In the above example, the `console.log` the string "Joe" multiple times, before logging "Mary" once and passing the test. 109 | 110 | See [tap-spec.js](./cypress/e2e/tap-spec.js) 111 | 112 | ## Videos 113 | 114 | - [Filter Input Elements By Value Using cypress-should-really Plugin](https://youtu.be/Gxoo6uZMo9I) 115 | 116 | ## See also 117 | 118 | - [cypress-recurse](https://github.com/bahmutov/cypress-recurse) 119 | - [cypress-map](https://github.com/bahmutov/cypress-map) for Cypress v12+ 120 | 121 | ## Small print 122 | 123 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2021 124 | 125 | - [@bahmutov](https://twitter.com/bahmutov) 126 | - [glebbahmutov.com](https://glebbahmutov.com) 127 | - [blog](https://glebbahmutov.com/blog) 128 | - [videos](https://www.youtube.com/glebbahmutov) 129 | - [presentations](https://slides.com/bahmutov) 130 | - [cypress.tips](https://cypress.tips) 131 | 132 | License: MIT - do anything with the code, but don't blame me if it does not work. 133 | 134 | Support: if you find any problems with this module, email / tweet / 135 | [open issue](https://github.com/bahmutov/cypress-should-really/issues) on Github 136 | 137 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 138 | [renovate-app]: https://renovateapp.com/ 139 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "DOM", 17 | "ESNext" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 25 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 26 | // "noLib": true /* Disable including any library files, including the default lib.d.ts. */, 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 29 | 30 | /* Modules */ 31 | "module": "commonjs" /* Specify what module code is generated. */, 32 | // "rootDir": "./", /* Specify the root folder within your source files. */ 33 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "resolveJsonModule": true, /* Enable importing .json files. */ 42 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | "allowJs": false /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 48 | 49 | /* Emit */ 50 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 51 | "declarationMap": true /* Create sourcemaps for d.ts files. */, 52 | "emitDeclarationOnly": false /* Only output d.ts files and not JavaScript files. */, 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 55 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | // "noEmit": true, /* Disable emitting files from a compilation. */ 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | "strict": true /* Enable all strict type-checking options. */, 83 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 84 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | }, 106 | "include": ["./src/index.ts"] 107 | } 108 | -------------------------------------------------------------------------------- /cypress/e2e/table/tableManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | jQuery Plugin 4 | Name: tableManager 5 | Version: 1.1.0 6 | Author: Pietrantonio Alessandro (Stone) 7 | Author's website: http://www.stonewebdesign.it 8 | 9 | @-- What's new --@ 10 | 11 | 1) 12 | New classes to the table: tablePagination, tableFilterBy; 13 | 14 | 2) 15 | New class to the th element: disableFilterBy; 16 | 17 | 3) 18 | New option to disable filter on one or more column: disableFilterBy: [] 19 | 20 | @-- Usage --@ 21 | 22 | Important! This plugin NEED jQuery library to work. As a matter of fact it's a jQuery plugin. 23 | 24 | Minimum requirements: 25 | - jQuery library 26 | 27 | 1) 28 | To include jQuery library download it from jQuery site and put the .js file into your site folders, then put this line into your document: 29 | 30 | Alternatively you can include it without download it, just like this: 31 | 32 | 2) 33 | Include this plugin just like this: download it and put the .js file into your site folders, then write this line into your document: 34 | 35 | Important! REMEMBER: THIS LINE HAVE TO BE AFTER JQUERY INCLUDING LINE! 36 | 37 | 38 | If you want you can customize this plugin as you prefer and need just with the following options. 39 | Options: 40 | 41 | debug = (boolean) can be true or false or not set. It activates debug mode and show messages into browser console. 42 | 43 | firstSort = can be an array of integer, or just a single integer. The first parameter determines the number of column to start sorting, the second parameter determines the order (asc or 0 = ascending, desc or 1 = descending). Ex.: 44 | firstSort : [[1,'asc']] --> the table is sorted by first column (first parameter = 1), by ascending order (second parameter = 0). 45 | 46 | disable = this option is used to disable one or more columns and expect one parameter per column. The parameter can be an integer or, to disable last column, -1 or the word "last". Ex.: 47 | disable : [3] --> disable sorting on third column 48 | 49 | appendFilterby = (boolean) used to add a filter on top of the table. The filter will be composed by one select to select by which column to filter and an input text to filter typing. Ex.: 50 | appendFilterby : true 51 | 52 | dateFormat = used to indicate column and dateformat. It helps to sort by date which is formatted like dd-mm-yyyy or mmddyyyy. The first parameter is the column, the second parameter is date format. Ex.: 53 | dateFormat : [[3, 'dd-mm-yyyy']] --> the third column is date and it's formatted like dd-mm-yyyy 54 | 55 | pagination = true or false or not set. It permits to paginate table and append controllers under the table. Ex.: 56 | pagination : true --> enable pagination tool 57 | 58 | showrows = you can append to table a select by which you can select number of rows to show. It must be an array of numbers. Ex.: 59 | showrows : [10,100,1000] --> you can choose to show 10, 100, 1000 rows 60 | 61 | vocabulary = used to translate labels. The following are accepted labels: 62 | voc_filter_by 63 | voc_type_here_filter 64 | 65 | disableFilterBy = used to disable filter by specific columns. It must be an array of numbers. To disable filter by last column you can use "last". Ex.: 66 | disableFilterBy: [1, "last"] --> it disable filter by first and last column 67 | 68 | Classes: 69 | 70 | (th) disableSort = disable that specific column 71 | disableFilterBy = disable filter for that specific column 72 | 73 | (table) tablePagination = append pagination elements to table 74 | tableFilterBy = append filter tool to table 75 | 76 | Data- attributes: 77 | 78 | data-tablemanager = 79 | 'disable' --> disable that specific column 80 | 81 | '{"dateFormat":"dd-mm-yyyy"}' --> this secific column represent a date with the forma tdd-mm-yyyy (Important! To pass correctly this data attribute the attribute value has to be written between '' and attribute single elements like dateFormat or mm-dd-yyyy between "") 82 | 83 | 84 | Important! Do not edit this plugin if you're not sure you're doing it right. The Author is not responsible for any malfunctions caused by the end User. 85 | 86 | **/ 87 | 88 | ;(function ($) { 89 | /* Initialize function */ 90 | $.fn.tablemanager = function (options = null) { 91 | /** 92 | Get common variables, parts of tables and others utilities 93 | **/ 94 | var Table = $(this), 95 | Heads = $(this).find('thead th'), 96 | tbody = $(this).find('tbody'), 97 | rows = $(this).find('tbody tr'), 98 | rlen = rows.length, 99 | arr = [], 100 | cells, 101 | clen 102 | 103 | /** 104 | Options default values 105 | **/ 106 | var firstSort = [[0, 0]], 107 | dateColumn = [], 108 | dateFormat = [], 109 | disableFilterBy = [] 110 | 111 | /** 112 | Debug value true or false 113 | **/ 114 | var debug = false 115 | var debug = options !== null && options.debug == true ? true : false 116 | 117 | /** 118 | Set pagination true or false 119 | **/ 120 | var pagination = false 121 | pagination = options !== null && options.pagination == true ? true : false 122 | // default pagination variables 123 | var currentPage = 0 124 | var numPerPage = 125 | pagination !== true && showrows_option !== true ? rows.length : 5 126 | var numOfPages = 127 | options.numOfPages !== undefined && options.numOfPages > 0 128 | ? options.numOfPages 129 | : 5 130 | 131 | /** 132 | Set default show rows list or set if option is set 133 | **/ 134 | var showrows = [5, 10, 50] 135 | showrows = 136 | options !== null && 137 | options.showrows != '' && 138 | typeof options.showrows !== undefined && 139 | options.showrows !== undefined 140 | ? options.showrows 141 | : showrows 142 | 143 | /** 144 | Default labels translations 145 | **/ 146 | var voc_filter_by = 'Filter by', 147 | voc_type_here_filter = 'Type here to filter...', 148 | voc_show_rows = 'Show rows' 149 | 150 | /** 151 | Available options: 152 | **/ 153 | var availableOptions = new Array() 154 | availableOptions = [ 155 | 'debug', 156 | 'firstSort', 157 | 'disable', 158 | 'appendFilterby', 159 | 'dateFormat', 160 | 'pagination', 161 | 'showrows', 162 | 'vocabulary', 163 | 'disableFilterBy', 164 | 'numOfPages', 165 | ] 166 | 167 | // debug 168 | // make array form options object 169 | arrayOptions = $.map(options, function (value, index) { 170 | return [index] 171 | }) 172 | for (i = 0; i < arrayOptions.length; i++) { 173 | // check if options are in available options array 174 | if (availableOptions.indexOf(arrayOptions[i]) === -1) { 175 | if (debug) { 176 | cLog('Error! ' + arrayOptions[i] + ' is unavailable option.') 177 | } 178 | } 179 | } 180 | 181 | /** 182 | Get options if set 183 | **/ 184 | if (options !== null) { 185 | /** 186 | Check options vocabulary 187 | **/ 188 | if ( 189 | options.vocabulary != '' && 190 | typeof options.vocabulary !== undefined && 191 | options.vocabulary !== undefined 192 | ) { 193 | // Check every single label 194 | 195 | voc_filter_by = 196 | options.vocabulary.voc_filter_by != '' && 197 | options.vocabulary.voc_filter_by !== undefined 198 | ? options.vocabulary.voc_filter_by 199 | : voc_filter_by 200 | 201 | voc_type_here_filter = 202 | options.vocabulary.voc_type_here_filter != '' && 203 | options.vocabulary.voc_type_here_filter !== undefined 204 | ? options.vocabulary.voc_type_here_filter 205 | : voc_type_here_filter 206 | 207 | voc_show_rows = 208 | options.vocabulary.voc_show_rows != '' && 209 | options.vocabulary.voc_show_rows !== undefined 210 | ? options.vocabulary.voc_show_rows 211 | : voc_show_rows 212 | } 213 | 214 | /** 215 | Option disable 216 | **/ 217 | if ( 218 | options.disable != '' && 219 | typeof options.disable !== undefined && 220 | options.disable !== undefined 221 | ) { 222 | for (var i = 0; i < options.disable.length; i++) { 223 | // check if should be disabled last column 224 | col = 225 | options.disable[i] == -1 || options.disable[i] == 'last' 226 | ? Heads.length 227 | : options.disable[i] == 'first' 228 | ? 1 229 | : options.disable[i] 230 | Heads.eq(col - 1) 231 | .addClass('disableSort') 232 | .removeClass('sortingAsc') 233 | .removeClass('sortingDesc') 234 | 235 | // debug 236 | if (isNaN(col - 1)) { 237 | if (debug) { 238 | cLog('Error! Check your "disable" option.') 239 | } 240 | } 241 | } 242 | } 243 | 244 | /** 245 | Option select number of rows to show 246 | **/ 247 | var showrows_option = false 248 | if ( 249 | options.showrows != '' && 250 | typeof options.showrows !== undefined && 251 | options.showrows !== undefined 252 | ) { 253 | showrows_option = true 254 | 255 | // div num rows 256 | var numrowsDiv = 257 | '
' 260 | // append div to choose num rows to show 261 | Table.before(numrowsDiv) 262 | // get show rows options and append select to its div 263 | for (i = 0; i < showrows.length; i++) { 264 | $('select#numrows').append( 265 | $('