├── .eslintignore
├── .npmignore
├── .codeclimate.yml
├── .eslintrc.json
├── .travis.yml
├── .babelrc
├── modules
├── index.js
├── parser.js
├── methods
│ ├── inspectTags.js
│ ├── inspectSiblings.js
│ ├── index.js
│ ├── inspectSpecialAttributes.js
│ ├── validationHelpers.js
│ ├── inspectElementID.js
│ ├── inspectNthChild.js
│ └── inspectNthChild.test.js
├── exposeOnWindow.js
├── convertSelectorStateIntoCSSSelector.js
├── stackHierarchy.js
├── validateSelector.js
├── parser.test.js
├── configuration.js
├── convertSelectorStateIntoCSSSelector.test.js
├── stackHeirarchy.test.js
├── queryEngine.test.js
├── queryEngine.js
└── simmer.js
├── __tests__
├── server.js
├── integration
│ ├── utils.js
│ ├── fixture.js
│ └── index.test.js
└── e2e
│ ├── test.html
│ └── index.js
├── .gitignore
├── LICENSE
├── rollup.config.js
├── package.json
├── nightwatch.conf.js
├── README.md
└── dist
└── simmer.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/lib/*.js
2 | **/lib/**/*.js
3 | **/dist/*.js
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | modules/**/*.test.js
4 | __tests__
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | languages:
2 | JavaScript: true
3 | exclude_paths:
4 | - "node_modules"
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "plugins": [
4 | "jest"
5 | ],
6 | "env": {
7 | "jest/globals": true
8 | }
9 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | sudo: false
5 | install:
6 | - npm install
7 | script:
8 | - npm test
9 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "es2016"
5 | ],
6 | "plugins": [
7 | "transform-object-rest-spread"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/modules/index.js:
--------------------------------------------------------------------------------
1 | /* global window, document */
2 | import simmer from './simmer'
3 | import exposeOnWindow from './exposeOnWindow'
4 |
5 | exposeOnWindow(window, simmer(window))
6 |
--------------------------------------------------------------------------------
/modules/parser.js:
--------------------------------------------------------------------------------
1 | export default function Parser (parsingMethods) {
2 | const queue = parsingMethods.getMethods()
3 | return {
4 | finished () {
5 | return queue.length === 0
6 | },
7 | next (...args) {
8 | if (this.finished()) {
9 | return false
10 | }
11 | return queue.shift()(...args)
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/__tests__/server.js:
--------------------------------------------------------------------------------
1 | function makeServer(done = () => {}) {
2 | var express = require('express');
3 | var path = require('path');
4 | var app = express();
5 |
6 | app.get('/', function (req, res) {
7 | res.status(200).sendFile(`__tests__/e2e/test.html`, {root: path.resolve()});
8 | });
9 | app.get('/simmer.js', function (req, res) {
10 | res.status(200).sendFile(`dist/simmer.js`, {root: path.resolve()});
11 | });
12 | var server = app.listen(3993, function () {
13 | var port = server.address().port;
14 | done()
15 | });
16 | return server;
17 | }
18 | module.exports = makeServer;
19 |
--------------------------------------------------------------------------------
/modules/methods/inspectTags.js:
--------------------------------------------------------------------------------
1 | import { tagName } from './validationHelpers'
2 |
3 | /**
4 | /**
5 | * Inspect the elements' Tag names and add them to the calculates CSS selector
6 | * @param {array} hierarchy. The hierarchy of elements
7 | * @param {object} state. The current calculated CSS selector
8 | */
9 | export default function (hierarchy, state) {
10 | return hierarchy.reduce((selectorState, currentElem, index) => {
11 | ;[currentElem.el.nodeName].filter(tagName).forEach(tagName => {
12 | selectorState.stack[index].splice(0, 0, tagName)
13 | selectorState.specificity += 10
14 | })
15 | return selectorState
16 | }, state)
17 | }
18 |
--------------------------------------------------------------------------------
/modules/exposeOnWindow.js:
--------------------------------------------------------------------------------
1 | export default function (windowScope, simmerInstance) {
2 | // Save the previous value of the `simmer` variable.
3 | let conflictedSimmer = windowScope.Simmer
4 | windowScope.Simmer = simmerInstance
5 |
6 | /**
7 | * Revert the global window.simmer variable to it's original value and return this simmer object.
8 | * This allows users to include multiple versions of Simmer objects on a single page.
9 | * @example
10 |
11 | Simmer.noConflict();
12 |
13 | */
14 | simmerInstance.noConflict = function () {
15 | windowScope.Simmer = conflictedSimmer
16 | return simmerInstance
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Intellij ###
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode
3 |
4 | ## Directory-based project format
5 | .idea/
6 | # if you remove the above rule, at least ignore user-specific stuff:
7 | # .idea/workspace.xml
8 | # .idea/tasks.xml
9 | # and these sensitive or high-churn files:
10 | # .idea/dataSources.ids
11 | # .idea/dataSources.xml
12 | # .idea/sqlDataSources.xml
13 | # .idea/dynamic.xml
14 |
15 | ## File-based project format
16 | *.ipr
17 | *.iws
18 | *.iml
19 |
20 | ## Additional for IntelliJ
21 | out/
22 |
23 | # generated by mpeltonen/sbt-idea plugin
24 | .idea_modules/
25 |
26 |
27 | # files created by the build process and the "node install" command
28 | node_modules/*
29 | bower_components/*
30 | report/*
31 | reports/*
32 | selenium-debug.log
33 | screenshots/
34 | .DS_Store
35 | lib/
--------------------------------------------------------------------------------
/modules/convertSelectorStateIntoCSSSelector.js:
--------------------------------------------------------------------------------
1 | import takeRight from 'lodash.takeright'
2 | /**
3 | * Conver the Selector State into a merged CSS selector
4 | * @param state (object) The current selector state (has the stack and specificity sum)
5 | * @param depth (int) The number of levels to merge (1..state.stack.length)
6 | */
7 | export default function (state, depth = state.stack.length) {
8 | return (
9 | takeRight(
10 | state.stack.reduceRight((selectorSegments, elementState) => {
11 | if (elementState.length) {
12 | selectorSegments.push(elementState.join(''))
13 | } else if (selectorSegments.length) {
14 | selectorSegments.push('*')
15 | }
16 | return selectorSegments
17 | }, []),
18 | depth
19 | ).join(' > ') || '*'
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/modules/stackHierarchy.js:
--------------------------------------------------------------------------------
1 | function push (arr, val) {
2 | arr.push(val)
3 | return arr
4 | }
5 |
6 | function tail (arr) {
7 | return arr[arr.length - 1]
8 | }
9 | /**
10 | * Retireve the element's ancestors up to the configured level.
11 | * This is an internal function and is not to be used from the outside (nor can it, it is private)
12 | * @param element (Object) The elemen't whose ancestry we want to retrieve
13 | * @param depth (number) How deep to into the heirarchy to collect elements
14 | */
15 | export default function stackHierarchy (element, depth) {
16 | if (depth <= 0) {
17 | throw new Error(`Simmer: An invalid depth of ${depth} has been specified`)
18 | }
19 | return Array(depth - 1)
20 | .fill()
21 | .reduce(
22 | (acc, val) => (tail(acc).parent() ? push(acc, tail(acc).parent()) : acc),
23 | [element]
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/modules/methods/inspectSiblings.js:
--------------------------------------------------------------------------------
1 | import take from 'lodash.take'
2 | import { className as validateClassName } from './validationHelpers'
3 | /**
4 | /**
5 | * Inspect the element's siblings by CSS Class names and compare them to the analyzed element.
6 | * @param {array} hierarchy. The hierarchy of elements
7 | * @param {object} state. The current calculated CSS selector
8 | */
9 | export default function (hierarchy, state) {
10 | return hierarchy.reduce((selectorState, currentElem, index) => {
11 | const validClasses = take(currentElem.getClasses(), 10)
12 | .filter(validateClassName)
13 | .map(className => `.${className}`)
14 |
15 | if (validClasses.length) {
16 | // limit to 10 classes
17 | selectorState.stack[index].push(validClasses.join(''))
18 | selectorState.specificity += 10 * validClasses.length
19 | }
20 | return selectorState
21 | }, state)
22 | }
23 |
--------------------------------------------------------------------------------
/modules/methods/index.js:
--------------------------------------------------------------------------------
1 | import inspectElementID from './inspectElementID'
2 | import inspectTags from './inspectTags'
3 | import inspectSiblings from './inspectSiblings'
4 | import inspectNthChild from './inspectNthChild'
5 | import inspectSpecialAttributes from './inspectSpecialAttributes'
6 |
7 | /**
8 | * The ParsingMethods are the key methods for the parsing process. They provide the various ways by which we analyze an element.
9 | * This object is a wrapper for building the list of available parsing methods and managing the context in which they are run so
10 | * that they all have access to basic parsing helper methods
11 | * */
12 | const parsingMethods = {
13 | methods: [],
14 | getMethods: function () {
15 | return this.methods.slice(0)
16 | },
17 | addMethod: function (fn) {
18 | this.methods.push(fn)
19 | }
20 | }
21 |
22 | parsingMethods.addMethod(inspectElementID)
23 | parsingMethods.addMethod(inspectTags)
24 | parsingMethods.addMethod(inspectSpecialAttributes)
25 | parsingMethods.addMethod(inspectSiblings)
26 | parsingMethods.addMethod(inspectNthChild)
27 |
28 | export default parsingMethods
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Gidi Meir Morris AKA "CheKofif"
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | // rollup.config.js
2 | import resolve from 'rollup-plugin-node-resolve'
3 | import babel from 'rollup-plugin-babel'
4 | import closure from 'rollup-plugin-closure-compiler-js'
5 | import commonjs from 'rollup-plugin-commonjs'
6 | import fs from 'fs'
7 |
8 | const babelrc = JSON.parse(fs.readFileSync('./.babelrc', 'utf8'))
9 |
10 | const babelConfig = {
11 | babelrc: false,
12 | presets: [['es2015', { modules: false }]].concat(
13 | (babelrc.presets || []).filter(preset => preset !== 'es2015')
14 | ),
15 | plugins: ['external-helpers'].concat(babelrc.plugins || [])
16 | }
17 |
18 | export default {
19 | entry: 'modules/index.js',
20 | format: 'iife',
21 | plugins: [
22 | resolve({
23 | jsnext: true,
24 | main: true
25 | }),
26 | commonjs({
27 | // non-CommonJS modules will be ignored, but you can also
28 | // specifically include/exclude files
29 | include: 'node_modules/**',
30 |
31 | // if true then uses of `global` won't be dealt with by this plugin
32 | ignoreGlobal: false,
33 |
34 | // if false then skip sourceMap generation for CommonJS modules
35 | sourceMap: false
36 | }),
37 | babel(babelConfig),
38 | closure()
39 | ],
40 | dest: 'dist/simmer.js'
41 | }
42 |
--------------------------------------------------------------------------------
/modules/methods/inspectSpecialAttributes.js:
--------------------------------------------------------------------------------
1 | const handlers = {
2 | A: (state, elm) => {
3 | const attribute = elm.el.getAttribute('href')
4 | if (attribute) {
5 | state.stack[0].push(`A[href="${attribute}"]`)
6 | state.specificity += 10
7 | }
8 | return state
9 | },
10 | IMG: (state, elm) => {
11 | const attribute = elm.el.getAttribute('src')
12 | if (attribute) {
13 | state.stack[0].push(`IMG[src="${attribute}"]`)
14 | state.specificity += 10
15 | }
16 | return state
17 | }
18 | }
19 |
20 | /**
21 | * Inspect the elements' special attributes which are likely to be unique to the element
22 | * @param {array} hierarchy. The hierarchy of elements
23 | * @param {object} state. The current calculated CSS selector
24 | */
25 | export default function (hierarchy, state, validateSelector) {
26 | const elm = hierarchy[0]
27 | const tag = elm.el.nodeName
28 |
29 | if (handlers[tag]) {
30 | state = handlers[tag](state, elm)
31 | if (validateSelector(state)) {
32 | // the unique attribute worked!
33 | state.verified = true
34 | } else {
35 | // turns out our so called unique attribute isn't as unique as we thought,
36 | // we'll remove it to keep the selector's noise level down
37 | state.stack[0].pop()
38 | }
39 | }
40 | return state
41 | }
42 |
--------------------------------------------------------------------------------
/modules/methods/validationHelpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Validate the syntax of a tagName to make sure that it has a valid syntax for the query engine.
3 | * Many libraries use invalid property and tag names, such as Facebook that use FB: prefixed tags.
4 | * These make the query engines fail and must be filtered out.
5 | * @param {string} tagName. The element's tag name
6 | */
7 | export function tagName (tagName) {
8 | if (
9 | typeof tagName === 'string' &&
10 | tagName.match(/^[a-zA-Z0-9]+$/gi) !== null
11 | ) {
12 | return tagName
13 | }
14 | return false
15 | }
16 | /**
17 | * Validate the syntax of an attribute to make sure that it has a valid syntax for the query engine.
18 | * @param {string} attribute. The element's attribute's value
19 | */
20 | export function attr (attribute) {
21 | if (
22 | typeof attribute === 'string' &&
23 | attribute.match(/^[0-9a-zA-Z][a-zA-Z_\-:0-9.]*$/gi) !== null
24 | ) {
25 | return attribute
26 | }
27 | return false
28 | }
29 |
30 | /**
31 | * Validate the syntax of an attribute to make sure that it has a valid syntax for the query engine.
32 | * @param {string} attribute. The element's attribute's value
33 | */
34 | export function className (className) {
35 | if (
36 | typeof className === 'string' &&
37 | className.match(/^\.?[a-zA-Z_\-:0-9]*$/gi) !== null
38 | ) {
39 | return className
40 | }
41 | return false
42 | }
43 |
--------------------------------------------------------------------------------
/modules/validateSelector.js:
--------------------------------------------------------------------------------
1 | import convertSelectorStateIntoCSSSelector from './convertSelectorStateIntoCSSSelector'
2 | /**
3 | * Execute a query using the current selector state and see if it produces a unique result.
4 | * If the result is unique then the analysis is complete and we can finish the process.
5 | * @param {object} element. The element we are trying to build a selector for
6 | * @param {object} state. The current selector state (has the stack and specificity sum)
7 | */
8 | export default function (element, config, query, onError) {
9 | const { selectorMaxLength } = config
10 | return function (state) {
11 | let validated = false
12 | let selector
13 | let results
14 | for (let depth = 1; depth <= state.stack.length && !validated; depth += 1) {
15 | // use selector to query an element and see if it is a one-to-one selection
16 |
17 | selector = convertSelectorStateIntoCSSSelector(state, depth).trim()
18 |
19 | if (!(selector && selector.length)) {
20 | // too short
21 | return false
22 | }
23 | if (selectorMaxLength && selector.length > selectorMaxLength) {
24 | // the selector is too long
25 | return false
26 | }
27 | results = query(selector, onError)
28 | validated =
29 | results.length === 1 &&
30 | (element.el !== undefined
31 | ? results[0] === element.el
32 | : results[0] === element)
33 |
34 | // we have to mark how deep the valdiation passed at
35 | if (validated) {
36 | state.verificationDepth = depth
37 | }
38 | }
39 |
40 | return validated
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/modules/parser.test.js:
--------------------------------------------------------------------------------
1 | import Parser from './parser'
2 |
3 | describe('Parser', () => {
4 | describe('next', () => {
5 | test(`calls the next function in the queue with the supplied arguments`, function () {
6 | const returnValue = { some: 0 }
7 | const method = jest.fn(() => returnValue)
8 | const parser = new Parser({
9 | getMethods: () => [method]
10 | })
11 |
12 | expect(parser.next(1, 2, 3)).toBe(returnValue)
13 |
14 | expect(method.mock.calls[0]).toMatchObject([1, 2, 3])
15 | })
16 | test(`removes the called method`, function () {
17 | const first = jest.fn(val => val)
18 | const second = jest.fn(val => val)
19 | const parser = new Parser({
20 | getMethods: () => [first, second]
21 | })
22 |
23 | expect(parser.next(1, 2, 3, 4, 5, 6)).toBe(1)
24 |
25 | expect(parser.next(6, 5, 4, 3, 2, 1)).toBe(6)
26 |
27 | expect(second.mock.calls[0]).toMatchObject([6, 5, 4, 3, 2, 1])
28 | })
29 | test(`returns false if no more methods are left`, function () {
30 | const parser = new Parser({
31 | getMethods: () => [val => val]
32 | })
33 |
34 | parser.next(1, 2, 3, 4, 5, 6)
35 |
36 | expect(parser.next(6, 5, 4, 3, 2, 1)).toBe(false)
37 | })
38 | })
39 | describe('finish', () => {
40 | test(`returns true if no methods are left in the queue`, function () {
41 | const parser = new Parser({
42 | getMethods: () => []
43 | })
44 |
45 | expect(parser.finished()).toBe(true)
46 | })
47 | test(`returns false if there are methods left in the queue`, function () {
48 | const parser = new Parser({
49 | getMethods: () => [() => 123]
50 | })
51 |
52 | expect(parser.finished()).toBe(false)
53 | })
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/modules/configuration.js:
--------------------------------------------------------------------------------
1 | // Configuration
2 | export const DEFAULT_CONFIGURATION = {
3 | // A function for calling an external query engine for testing CSS selectors such as jQuery or Sizzle
4 | // (If you have jQuery or Sizzle on the page, you have no need to supply such a function as Simmer will detect
5 | // these and use them if they are available. this will not work if you have these libraries in noConflict mode.
6 | queryEngine: null,
7 | // A minimum specificty level. Once the parser reaches this level it starts verifying the selector after every method is called
8 | // This can cut down our execution time by avoiding needless parsing but can also hurt execution times by performing many
9 | // verifications. This number will have to be tweeked here and there as we use the component...
10 | specificityThreshold: 100,
11 | // How deep into the DOM hierarchy should Simmer go in order to reach a unique selector.
12 | // This is a delicate game because the higher the number the more likely you are to reach a unique selector,
13 | // but it also means a longer and more breakable one. Assuming you want to store this selector to use later,
14 | // making it longer also means it is more likely to change and loose it's validity.
15 | depth: 3,
16 | // Handling errors in the Simmer analysis process.
17 | // true / false / callback
18 | // false: errors are ignored by Simmer
19 | // true: errors rethrown by the process
20 | // a function callback will be called with two parameters: the exception and the element being analyzed
21 | errorHandling: false,
22 | // A maximum length for the CSS selector can be specified - if no specific selector can be found which is shorter than this length
23 | // then it is treated as if no selector could be found
24 | selectorMaxLength: 512
25 | }
26 |
27 | export function configure (config = {}) {
28 | return {
29 | ...DEFAULT_CONFIGURATION,
30 | ...config
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/modules/methods/inspectElementID.js:
--------------------------------------------------------------------------------
1 | import { isUniqueElementID } from '../queryEngine'
2 | import { attr } from './validationHelpers'
3 | /**
4 | * Inspect the elements' IDs and add them to the CSS Selector
5 | * @param {array} hierarchy. The hierarchy of elements
6 | * @param {object} state. The current selector state (has the stack and specificity sum)
7 | */
8 | export default function (hierarchy, state, validateSelector, config, query) {
9 | return hierarchy.reduce((selectorState, currentElem, index) => {
10 | if (!selectorState.verified) {
11 | const [validatedState] = [currentElem.el.getAttribute('id')]
12 | .filter(id => attr(id))
13 | // make sure the ID is unique
14 | .filter(id => isUniqueElementID(query, id))
15 | .map(validId => {
16 | selectorState.stack[index].push(`[id='${validId}']`)
17 | selectorState.specificity += 100
18 |
19 | if (selectorState.specificity >= config.specificityThreshold) {
20 | // we have reached the minimum specificity, lets try verifying now, as this will save us having to add more IDs to the selector
21 | if (validateSelector(selectorState)) {
22 | // The ID worked like a charm - mark this state as verified and move on!
23 | selectorState.verified = true
24 | }
25 | }
26 |
27 | if (!selectorState.verified && index === 0) {
28 | // if the index is 0 then this is the ID of the actual element! Which means we have found our selector!
29 | // The ID wasn't enough, this means the page, this should never happen as we tested for the ID's uniquness, but just incase
30 | // we will pop it from the stack as it only adds noise
31 | selectorState.stack[index].pop()
32 | selectorState.specificity -= 100
33 | }
34 | return selectorState
35 | })
36 | return validatedState || selectorState
37 | }
38 | return selectorState
39 | }, state)
40 | }
41 |
--------------------------------------------------------------------------------
/modules/convertSelectorStateIntoCSSSelector.test.js:
--------------------------------------------------------------------------------
1 | import convertSelectorStateIntoCSSSelector from './convertSelectorStateIntoCSSSelector'
2 |
3 | describe.only('convertSelectorStateIntoCSSSelector', () => {
4 | test(`converts an empty stack into a "select all" selector`, function () {
5 | expect(
6 | convertSelectorStateIntoCSSSelector({
7 | stack: [[]]
8 | })
9 | ).toBe('*')
10 | })
11 |
12 | test(`converts an one level stack with one selector into that selector`, function () {
13 | expect(
14 | convertSelectorStateIntoCSSSelector({
15 | stack: [['DIV']]
16 | })
17 | ).toBe('DIV')
18 | })
19 |
20 | test(`converts an one level stack with multiple selectors into a merged selector`, function () {
21 | expect(
22 | convertSelectorStateIntoCSSSelector({
23 | stack: [['DIV', '.className']]
24 | })
25 | ).toBe('DIV.className')
26 | })
27 |
28 | test(`converts an multi level stack with multiple selectors into a merged selector`, function () {
29 | expect(
30 | convertSelectorStateIntoCSSSelector({
31 | stack: [['span'], ['DIV', '#someId'], ['DIV', '.className']]
32 | })
33 | ).toBe('DIV.className > DIV#someId > span')
34 | })
35 |
36 | test(`converts an empty selector in a multi level stack into a "select all" selector`, function () {
37 | expect(
38 | convertSelectorStateIntoCSSSelector({
39 | stack: [['span'], [], ['DIV', '.className']]
40 | })
41 | ).toBe('DIV.className > * > span')
42 | })
43 |
44 | test(`omits "select all" selectors from the parent levels`, function () {
45 | expect(
46 | convertSelectorStateIntoCSSSelector({
47 | stack: [['span'], [], ['DIV', '.className'], [], []]
48 | })
49 | ).toBe('DIV.className > * > span')
50 | })
51 |
52 | test(`takes a depth level which will omit any selectors beyond that depth`, function () {
53 | expect(
54 | convertSelectorStateIntoCSSSelector(
55 | {
56 | stack: [
57 | ['span'],
58 | [],
59 | ['DIV', '.className'],
60 | [],
61 | ['DIV', '#someId'],
62 | ['ARTICLE'],
63 | []
64 | ]
65 | },
66 | 3
67 | )
68 | ).toBe('DIV.className > * > span')
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/__tests__/integration/utils.js:
--------------------------------------------------------------------------------
1 | // code utils
2 | export const either = (value, left, right) =>
3 | value ? right(value) : left(value)
4 | export const identity = i => i
5 | export const compose = (f, ...fns) => {
6 | if (fns.length === 1) {
7 | const g = fns.pop()
8 | return (...args) => f(g(...args))
9 | }
10 | return (...args) => f(compose(...fns)(...args))
11 | }
12 | export const rcompose = (...fns) => {
13 | return compose(...fns.reverse())
14 | }
15 | export const mapfn = fn => arr => arr.map(fn)
16 | export const reducefn = (fn, init) => arr => arr.reduce(fn, init)
17 |
18 | // Dom utils
19 | // use query in current scope
20 | export const queryEngine = document => ({
21 | query: (...args) => {
22 | if (!document) {
23 | throw Error(
24 | 'No global scoped document is available - somethign is wrong with the test runner'
25 | )
26 | }
27 | let elm = document.querySelectorAll(...args)
28 | if (elm && elm.length) {
29 | if (elm.length === 1) {
30 | return elm[0]
31 | }
32 | throw new Error(
33 | `Invalid number of elements returned for query: ${elm.join()}`
34 | )
35 | }
36 | return elm
37 | },
38 | default: true
39 | })
40 |
41 | export const setQueryFunction = (queryFn, name) => {
42 | queryEngine.default = false
43 | queryEngine.query = queryFn
44 | describe(`Simmer tests along side a ${name} selector engine`)
45 | }
46 |
47 | export const NoResult = { el: undefined, SimmerEl: false }
48 |
49 | export const compareParentElementAndSimmer = (windowScope, elementSelector) => {
50 | const comp = compareElementsAndSimmer(windowScope, elementSelector)
51 | if (comp !== NoResult && comp.element) {
52 | comp.el = comp.el.parentNode
53 | }
54 | return comp
55 | }
56 |
57 | export const compareElementsAndSimmer = (windowScope, elementSelector) => {
58 | var element, selector
59 | if (typeof elementSelector === 'string') {
60 | element = windowScope.document.querySelectorAll(elementSelector)[0]
61 | } else {
62 | element = elementSelector
63 | }
64 |
65 | if (!element) {
66 | return NoResult
67 | }
68 |
69 | selector = windowScope.Simmer(element)
70 | if (selector === false) {
71 | return false
72 | }
73 |
74 | return {
75 | el: element,
76 | SimmerEl: queryEngine(windowScope.document).query(selector),
77 | selector: selector
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/modules/methods/inspectNthChild.js:
--------------------------------------------------------------------------------
1 | import difference from 'lodash.difference'
2 | import flatmap from 'lodash.flatmap'
3 | /**
4 | * Inspect the element's siblings by index (nth-child) name and compare them to the analyzed element.
5 | * The sibling comparison is done from level 0 (the analyzed element) upwards in the hierarchy, In an effort to avoid unneeded parsing.
6 | * @param {array} hierarchy. The hierarchy of elements
7 | * @param {object} state. The current calculated CSS selector
8 | */
9 |
10 | const not = i => !i
11 | const getNodeNames = siblings => siblings.map(sibling => sibling.el.nodeName)
12 | /***
13 | * The main method splits up the hierarchy and isolates each element, while this helper method is designed
14 | * to then take each level in the hierarchy and fid into it's surounding siblings.
15 | * We've separated these to make the code more managable.
16 | * @param siblingElement
17 | * @param siblings
18 | * @returns {boolean}
19 | */
20 | export function analyzeElementSiblings (element, siblings) {
21 | return (
22 | difference(
23 | element.getClasses(),
24 | flatmap(siblings, sibling => sibling.getClasses())
25 | ).length > 0 || not(getNodeNames(siblings).includes(element.el.nodeName))
26 | )
27 | }
28 |
29 | export default function (hierarchy, state, validateSelector) {
30 | return hierarchy.reduce((selectorState, currentElem, index) => {
31 | if (!selectorState.verified) {
32 | // get siblings BEFORE out element
33 | const prevSiblings = currentElem.prevAll()
34 | const nextSiblings = currentElem.nextAll()
35 |
36 | // get element's index by the number of elements before it
37 | // note that the nth-child selector uses a 1 based index, not 0
38 | const indexOfElement = prevSiblings.length + 1
39 |
40 | // If the element has no siblings!
41 | // we have no need for the nth-child selector
42 | if (
43 | (prevSiblings.length || nextSiblings.length) &&
44 | !analyzeElementSiblings(currentElem, [...prevSiblings, ...nextSiblings])
45 | ) {
46 | // if we don't have a unique tag or a unique class, then we need a nth-child to help us
47 | // differenciate our element from the rest of the pack
48 | selectorState.stack[index].push(`:nth-child(${indexOfElement})`)
49 |
50 | // Verify the selector as we don't want to go on and parse the parent's siblings
51 | // if we don't have to!
52 | selectorState.verified = validateSelector(selectorState)
53 | }
54 | }
55 | return selectorState
56 | }, state)
57 | }
58 |
--------------------------------------------------------------------------------
/modules/stackHeirarchy.test.js:
--------------------------------------------------------------------------------
1 | import { wrap } from './queryEngine'
2 | import stackHierarchy from './stackHierarchy'
3 | const { JSDOM } = require('jsdom')
4 |
5 | const createElementHeirarchy = (dom = '', selector) =>
6 | new JSDOM(`
${dom}`).window.document.querySelector(selector)
7 |
8 | describe('stackHierarchy', () => {
9 | test(`takes a wrapped element and a depth and returns the element heirarchy up tp that depth`, function () {
10 | const child = createElementHeirarchy(
11 | `
12 |
20 | `,
21 | `#leaf`
22 | )
23 | const stackedHeirarchy = stackHierarchy(wrap(child), 3)
24 |
25 | expect(stackedHeirarchy[0].el).toBe(child)
26 | expect(stackedHeirarchy[1].el).toBe(child.parentNode)
27 | expect(stackedHeirarchy[2].el).toBe(child.parentNode.parentNode)
28 | expect(stackedHeirarchy.length).toBe(3)
29 | })
30 |
31 | test(`throws an error if the depth is invalid`, function () {
32 | const child = createElementHeirarchy(
33 | `
34 |
42 | `,
43 | `#leaf`
44 | )
45 |
46 | expect(() => stackHierarchy(wrap(child), 0)).toThrowError(
47 | /An invalid depth of/
48 | )
49 |
50 | expect(() => stackHierarchy(wrap(child), -5)).toThrowError(
51 | /An invalid depth of/
52 | )
53 | })
54 |
55 | test(`returns a partial heirarchy if the depth is higher than what is available on the DOM`, function () {
56 | const child = createElementHeirarchy(
57 | `
58 |
62 | `,
63 | `#leaf`
64 | )
65 |
66 | const stackedHeirarchy = stackHierarchy(wrap(child), 10)
67 |
68 | expect(stackedHeirarchy[0].el).toBe(child)
69 | expect(stackedHeirarchy[1].el).toBe(child.parentNode)
70 | // Body
71 | expect(stackedHeirarchy[2].el).toBe(child.parentNode.parentNode)
72 | /// HTML
73 | expect(stackedHeirarchy[3].el).toBe(child.parentNode.parentNode.parentNode)
74 | // Document
75 | expect(stackedHeirarchy[4].el).toBe(
76 | child.parentNode.parentNode.parentNode.parentNode
77 | )
78 |
79 | expect(stackedHeirarchy.length).toBe(5)
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/modules/queryEngine.test.js:
--------------------------------------------------------------------------------
1 | import initQueryEngine, { isUniqueElementID } from './queryEngine'
2 | const { JSDOM } = require('jsdom')
3 | const createElement = (dom = '') =>
4 | new JSDOM(``).window.document.querySelector('div')
5 | /// HTMLElement
6 | describe('QueryEngine', () => {
7 | describe('attachQueryEngine', () => {
8 | test(`defaults to using the document.querySelectorAll as queryEngine`, function () {
9 | const returnValue = [{ tagName: 'div' }, { tagName: 'div' }]
10 | const querySelectorAll = jest.fn(() => returnValue)
11 |
12 | const windowScope = {
13 | document: {
14 | querySelectorAll
15 | }
16 | }
17 |
18 | const $ = initQueryEngine(windowScope)
19 |
20 | expect($('div')).toBe(returnValue)
21 |
22 | expect(querySelectorAll.mock.calls[0][0]).toBe('div')
23 | })
24 |
25 | test(`takes a query engine and uses it to query`, function () {
26 | const returnValue = [{ tagName: 'div' }, { tagName: 'div' }]
27 | const customQueryEngine = jest.fn(() => returnValue)
28 | const querySelectorAll = jest.fn(() => [])
29 |
30 | const windowScope = {
31 | document: {
32 | querySelectorAll
33 | }
34 | }
35 |
36 | const $ = initQueryEngine(windowScope, customQueryEngine)
37 |
38 | expect($('div')).toBe(returnValue)
39 |
40 | expect(querySelectorAll.mock.calls.length).toBe(0)
41 | expect(customQueryEngine.mock.calls[0][0]).toBe('div')
42 | })
43 | })
44 |
45 | describe('isUniqueElementID', () => {
46 | test(`takes a query engine and an id and returns true if the query engine has only one result for that id`, function () {
47 | const query = jest.fn(() => [createElement()])
48 |
49 | expect(isUniqueElementID(query, 'uniqueId')).toBe(true)
50 |
51 | expect(query.mock.calls[0][0]).toBe(`[id="uniqueId"]`)
52 | })
53 |
54 | test(`takes a query engine and an id and returns false if the query engine has no result for that id`, function () {
55 | const query = jest.fn(() => [])
56 |
57 | expect(isUniqueElementID(query, 'uniqueId')).toBe(false)
58 |
59 | expect(query.mock.calls[0][0]).toBe(`[id="uniqueId"]`)
60 | })
61 |
62 | test(`takes a query engine and an id and returns false if the query engine returns multiple result for that id`, function () {
63 | const query = jest.fn(() => [createElement(), createElement()])
64 |
65 | expect(isUniqueElementID(query, 'uniqueId')).toBe(false)
66 |
67 | expect(query.mock.calls[0][0]).toBe(`[id="uniqueId"]`)
68 | })
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/modules/queryEngine.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Verify a specific ID's uniqueness one the page
3 | * @param {object} element. The element we are trying to build a selector for
4 | * @param {object} state. The current selector state (has the stack and specificity sum)
5 | */
6 | export function isUniqueElementID (query, elementID) {
7 | // use selector to query an element and see if it is a one-to-one selection
8 | var results = query(`[id="${elementID}"]`) || []
9 | return results.length === 1
10 | }
11 |
12 | function traverseAttribute (el, dir) {
13 | const matched = []
14 | let cur = el[dir]
15 |
16 | while (cur && cur.nodeType !== 9) {
17 | if (cur.nodeType === 1) {
18 | matched.push(wrap(cur))
19 | }
20 | cur = cur[dir]
21 | }
22 | return matched
23 | }
24 |
25 | export function wrap (el) {
26 | /// When the DOM wrapper return the selected element it wrapps
27 | /// it with helper methods which aid in analyzing the result
28 | return {
29 | el,
30 |
31 | getClass: function () {
32 | return this.el.getAttribute('class') || ''
33 | },
34 |
35 | getClasses: function () {
36 | return this.getClass()
37 | .split(' ')
38 | .map(className => className.replace(/^\s\s*/, '').replace(/\s\s*$/, ''))
39 | .filter(className => className.length > 0)
40 | },
41 |
42 | prevAll: function () {
43 | return traverseAttribute(this.el, 'previousSibling')
44 | },
45 |
46 | nextAll: function () {
47 | return traverseAttribute(this.el, 'nextSibling')
48 | },
49 |
50 | parent: function () {
51 | return this.el.parentNode && this.el.parentNode.nodeType !== 11
52 | ? wrap(this.el.parentNode)
53 | : null
54 | }
55 | }
56 | }
57 |
58 | const INVALID_DOCUMENT = {
59 | querySelectorAll () {
60 | throw new Error(
61 | 'An invalid context has been provided to Simmer, it doesnt know how to query it'
62 | )
63 | }
64 | }
65 |
66 | const documentQuerySelector = scope => {
67 | const document = typeof scope.querySelectorAll === 'function'
68 | ? scope
69 | : scope.document ? scope.document : INVALID_DOCUMENT
70 | return (selector, onError) => {
71 | try {
72 | return document.querySelectorAll(selector)
73 | } catch (ex) {
74 | // handle error
75 | onError(ex)
76 | }
77 | }
78 | }
79 |
80 | /**
81 | * A DOM manipulation object. Provides both CSS selector querying for verifications and a wrapper for the elements them selves to provide
82 | * key behavioural methods, such as sibling querying etc.
83 | * @param {object} elementOrSelector. An element we wish to wrapper or a CSS query string
84 | */
85 | export default function (scope, configuredQueryEngine) {
86 | const queryEngine = typeof configuredQueryEngine === 'function'
87 | ? configuredQueryEngine
88 | : documentQuerySelector(scope)
89 |
90 | // If no selector we return an empty array - no CSS selector will ever be generated in this situation!
91 | return (selector, onError) =>
92 | typeof selector !== 'string' ? [] : queryEngine(selector, onError, scope)
93 | }
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simmerjs",
3 | "description": "A pure Javascript reverse CSS selector engine which calculates a DOM element's unique CSS selector on the current page.",
4 | "version": "0.5.6",
5 | "author": "Gidi Meir Morris",
6 | "main": "lib/simmer.js",
7 | "jsnext:main": "modules/simmer.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/gmmorris/simmerjs.git"
11 | },
12 | "licenses": [
13 | {
14 | "type": "MIT",
15 | "url": "https://github.com/gmmorris/simmerjs/blob/master/LICENSE"
16 | }
17 | ],
18 | "dependencies": {
19 | "lodash.difference": "^4.5.0",
20 | "lodash.flatmap": "^4.5.0",
21 | "lodash.isfunction": "^3.0.8",
22 | "lodash.take": "^4.1.1",
23 | "lodash.takeright": "^4.1.1"
24 | },
25 | "scripts": {
26 | "test": "npm run testUnit && npm run testIntegration",
27 | "testw": "jest .test.js --watch",
28 | "testUnit": "jest ./modules",
29 | "testIntegration": "jest ./**/integration/*.test.js",
30 | "testE2E": "npm run build && nightwatch --config nightwatch.conf.js",
31 | "build": "npm run buildBrowser && npm run buildModule",
32 | "buildBrowser": "rollup -c",
33 | "buildModule": "rm -rf ./lib && babel modules -d lib --ignore .test.js",
34 | "lint": "eslint ./modules/**/*.js",
35 | "format": "prettier-eslint --write \"modules/**/*.js\"",
36 | "formatTests": "prettier-eslint --write \"__tests__/integration/*.js\"",
37 | "precommit": "lint-staged",
38 | "prepublish": "npm run buildModule"
39 | },
40 | "lint-staged": {
41 | "*.js": [
42 | "npm run format",
43 | "npm run lint",
44 | "git add"
45 | ]
46 | },
47 | "devDependencies": {
48 | "babel-cli": "^6.24.1",
49 | "babel-jest": "^20.0.3",
50 | "babel-plugin-external-helpers": "^6.22.0",
51 | "babel-plugin-transform-object-rest-spread": "^6.23.0",
52 | "babel-preset-env": "^1.5.1",
53 | "babel-preset-es2015": "^6.24.1",
54 | "babel-preset-es2016": "^6.24.1",
55 | "env2": "^2.2.0",
56 | "eslint": "^4.0.0",
57 | "eslint-config-standard": "^10.2.1",
58 | "eslint-plugin-import": "^2.3.0",
59 | "eslint-plugin-jest": "^20.0.3",
60 | "eslint-plugin-node": "^5.0.0",
61 | "eslint-plugin-promise": "^3.5.0",
62 | "eslint-plugin-standard": "^3.0.1",
63 | "express": "^4.15.3",
64 | "husky": "^0.13.4",
65 | "jest": "^20.0.4",
66 | "jest-codemods": "^0.10.1",
67 | "jsdom": "^11.0.0",
68 | "lint-staged": "^4.0.0",
69 | "nightwatch": "^0.9.15",
70 | "path": "^0.12.7",
71 | "prettier": "^1.4.4",
72 | "prettier-eslint-cli": "^4.1.1",
73 | "regenerator-runtime": "^0.10.5",
74 | "rollup": "^0.42.0",
75 | "rollup-plugin-babel": "^2.7.1",
76 | "rollup-plugin-closure-compiler-js": "^1.0.4",
77 | "rollup-plugin-commonjs": "^8.0.2",
78 | "rollup-plugin-node-resolve": "^3.0.0",
79 | "selenium-download": "^2.0.10",
80 | "sinon": "^2.3.2",
81 | "standard": "^10.0.2"
82 | },
83 | "keywords": [
84 | "Simmer",
85 | "css",
86 | "css selector",
87 | "sizzle"
88 | ]
89 | }
90 |
--------------------------------------------------------------------------------
/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | const SCREENSHOT_PATH = "./screenshots/";
2 | const BINPATH = './node_modules/nightwatch/bin/';
3 |
4 | // we use a nightwatch.conf.js file so we can include comments and helper functions
5 | module.exports = {
6 | "src_folders": [
7 | "__tests__/e2e"// Where you are storing your Nightwatch e2e tests
8 | ],
9 | "output_folder": "./reports", // reports (test outcome) output by nightwatch
10 | "selenium": { // downloaded by selenium-download module (see readme)
11 | "start_process": true, // tells nightwatch to start/stop the selenium process
12 | "server_path": "./node_modules/nightwatch/bin/selenium.jar",
13 | "host": "127.0.0.1",
14 | "port": 4444, // standard selenium port
15 | "cli_args": { // chromedriver is downloaded by selenium-download (see readme)
16 | "webdriver.chrome.driver" : "./node_modules/nightwatch/bin/chromedriver"
17 | }
18 | },
19 | "test_settings": {
20 | "default": {
21 | "screenshots": {
22 | "enabled": true, // if you want to keep screenshots
23 | "path": './screenshots' // save screenshots here
24 | },
25 | "globals": {
26 | "waitForConditionTimeout": 5000 // sometimes internet is slow so wait.
27 | },
28 | "desiredCapabilities": { // use Chrome as the default browser for tests
29 | "browserName": "chrome"
30 | }
31 | },
32 | "chrome": {
33 | "desiredCapabilities": {
34 | "browserName": "chrome",
35 | "javascriptEnabled": true // turn off to test progressive enhancement
36 | }
37 | }
38 | }
39 | }
40 | /**
41 | * selenium-download does exactly what it's name suggests;
42 | * downloads (or updates) the version of Selenium (& chromedriver)
43 | * on your localhost where it will be used by Nightwatch.
44 | /the following code checks for the existence of `selenium.jar` before trying to run our tests.
45 | */
46 |
47 | require('fs').stat(BINPATH + 'selenium.jar', function (err, stat) { // got it?
48 | if (err || !stat || stat.size < 1) {
49 | require('selenium-download').ensure(BINPATH, function(error) {
50 | if (error) throw new Error(error); // no point continuing so exit!
51 | console.log('✔ Selenium & Chromedriver downloaded to:', BINPATH);
52 | });
53 | }
54 | });
55 |
56 | function padLeft (count) { // theregister.co.uk/2016/03/23/npm_left_pad_chaos/
57 | return count < 10 ? '0' + count : count.toString();
58 | }
59 |
60 | var FILECOUNT = 0; // "global" screenshot file count
61 | /**
62 | * The default is to save screenshots to the root of your project even though
63 | * there is a screenshots path in the config object above! ... so we need a
64 | * function that returns the correct path for storing our screenshots.
65 | * While we're at it, we are adding some meta-data to the filename, specifically
66 | * the Platform/Browser where the test was run and the test (file) name.
67 | */
68 | function imgpath (browser) {
69 | var a = browser.options.desiredCapabilities;
70 | var meta = [a.platform];
71 | meta.push(a.browserName ? a.browserName : 'any');
72 | meta.push(a.version ? a.version : 'any');
73 | meta.push(a.name); // this is the test filename so always exists.
74 | var metadata = meta.join('~').toLowerCase().replace(/ /g, '');
75 | return SCREENSHOT_PATH + metadata + '_' + padLeft(FILECOUNT++) + '_';
76 | }
77 |
78 | module.exports.imgpath = imgpath;
79 | module.exports.SCREENSHOT_PATH = SCREENSHOT_PATH;
--------------------------------------------------------------------------------
/modules/methods/inspectNthChild.test.js:
--------------------------------------------------------------------------------
1 | import { analyzeElementSiblings } from './inspectNthChild'
2 |
3 | describe('analyzeElementSiblings', () => {
4 | test(`takes an only child with no classes and identifies it as having a unique tag / class`, function () {
5 | expect(
6 | analyzeElementSiblings(
7 | { el: { nodeName: 'DIV' }, getClasses: () => [] },
8 | []
9 | )
10 | ).toBe(true)
11 | })
12 |
13 | test(`takes an only child with classes and identifies it as having a unique tag / class`, function () {
14 | expect(
15 | analyzeElementSiblings(
16 | { el: { nodeName: 'DIV' }, getClasses: () => ['someClass'] },
17 | []
18 | )
19 | ).toBe(true)
20 | })
21 |
22 | test(`takes an child with a sibling and identifies it as having a unique tag`, function () {
23 | expect(
24 | analyzeElementSiblings(
25 | { el: { nodeName: 'DIV' }, getClasses: () => ['someClass'] },
26 | [{ el: { nodeName: 'P' }, getClasses: () => [] }]
27 | )
28 | ).toBe(true)
29 | })
30 |
31 | test(`takes an child with a sibling and identifies it as having a unique class`, function () {
32 | expect(
33 | analyzeElementSiblings(
34 | { el: { nodeName: 'DIV' }, getClasses: () => ['someClass'] },
35 | [{ el: { nodeName: 'DIV' }, getClasses: () => [] }]
36 | )
37 | ).toBe(true)
38 | })
39 |
40 | test(`takes an child with a sibling and identifies it as having a unique class among matching classes`, function () {
41 | expect(
42 | analyzeElementSiblings(
43 | {
44 | el: { nodeName: 'DIV' },
45 | getClasses: () => ['someAClass', 'someUniqueClass', 'someBClass']
46 | },
47 | [
48 | {
49 | el: { nodeName: 'DIV' },
50 | getClasses: () => ['someAClass', 'someBClass']
51 | }
52 | ]
53 | )
54 | ).toBe(true)
55 | })
56 |
57 | test(`takes an child with multiple sibling and identifies it as having a unique class among matching classes`, function () {
58 | expect(
59 | analyzeElementSiblings(
60 | {
61 | el: { nodeName: 'DIV' },
62 | getClasses: () => ['someAClass', 'someUniqueClass', 'someBClass']
63 | },
64 | [
65 | {
66 | el: { nodeName: 'DIV' },
67 | getClasses: () => ['someAClass', 'someBClass']
68 | },
69 | { el: { nodeName: 'DIV' }, getClasses: () => ['someBClass'] },
70 | { el: { nodeName: 'DIV' }, getClasses: () => ['someBClass'] },
71 | { el: { nodeName: 'DIV' }, getClasses: () => ['someDClass'] }
72 | ]
73 | )
74 | ).toBe(true)
75 | })
76 |
77 | test(`takes an child with multiple sibling and identifies it as having no unique class`, function () {
78 | expect(
79 | analyzeElementSiblings(
80 | {
81 | el: { nodeName: 'DIV' },
82 | getClasses: () => ['someAClass', 'someBClass']
83 | },
84 | [
85 | {
86 | el: { nodeName: 'DIV' },
87 | getClasses: () => ['someAClass', 'someBClass']
88 | },
89 | { el: { nodeName: 'DIV' }, getClasses: () => ['someBClass'] },
90 | { el: { nodeName: 'DIV' }, getClasses: () => ['someBClass'] },
91 | { el: { nodeName: 'DIV' }, getClasses: () => ['someDClass'] }
92 | ]
93 | )
94 | ).toBe(false)
95 | })
96 |
97 | test(`takes an child with multiple sibling and identifies it as having no unique tag`, function () {
98 | expect(
99 | analyzeElementSiblings(
100 | { el: { nodeName: 'DIV' }, getClasses: () => [] },
101 | [
102 | { el: { nodeName: 'DIV' }, getClasses: () => [] },
103 | { el: { nodeName: 'DIV' }, getClasses: () => [] }
104 | ]
105 | )
106 | ).toBe(false)
107 | })
108 | })
109 |
--------------------------------------------------------------------------------
/__tests__/integration/fixture.js:
--------------------------------------------------------------------------------
1 | export default `
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

18 |
19 |
20 | | First |
21 | Second |
22 | Third |
23 |
24 |
25 | | 1 |
28 | 2 |
29 | Cell to target! |
30 |
31 |
32 | | A |
33 | B |
34 | C |
35 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
51 |
56 |
61 |
62 |
77 |
78 | `
79 |
--------------------------------------------------------------------------------
/__tests__/e2e/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | hello
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |

23 |
24 |
25 | | First |
26 | Second |
27 | Third |
28 |
29 |
30 | | 1 |
33 | 2 |
34 | Cell to target! |
35 |
36 |
37 | | A |
38 | B |
39 | C |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | a
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | a
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | a
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
85 |
90 |
95 |
96 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/modules/simmer.js:
--------------------------------------------------------------------------------
1 | import initQueryEngine, { wrap } from './queryEngine'
2 | import parsingMethods from './methods'
3 | import validateSelector from './validateSelector'
4 | import convertSelectorStateIntoCSSSelector from './convertSelectorStateIntoCSSSelector'
5 | import Parser from './parser'
6 | import stackHierarchy from './stackHierarchy'
7 | import { configure } from './configuration'
8 |
9 | export default function createSimmer (
10 | windowScope = window,
11 | customConfig = {},
12 | customQuery = false
13 | ) {
14 | const config = configure(customConfig)
15 | const query = customQuery || initQueryEngine(windowScope, config.queryEngine)
16 | /**
17 | * Handle errors in accordance with what is specified in the configuration
18 | * @param {object/string} ex. The exception object or message
19 | * @param {object} element. The element Simmer was asked to process
20 | */
21 | function onError (ex, element) {
22 | // handle error
23 | if (config.errorHandling === true) {
24 | throw ex
25 | }
26 | if (typeof config.errorHandling === 'function') {
27 | config.errorHandling(ex, element)
28 | }
29 | }
30 |
31 | // Initialize the Simmer object and set it over the reference on the window
32 | /**
33 | * The main Simmer action - parses an element on the page to produce a CSS selector for it.
34 | * This function will be returned into the global Simmer object.
35 | * @param {object} element. A DOM element you wish to create a selector for.
36 | * @example
37 |
38 | var cssSelectorForDonJulio = Simmer(document.getElementByID('DonJulio'));
39 |
40 | */
41 | const simmer = function (element) {
42 | if (!element) {
43 | // handle error
44 | onError.call(
45 | simmer,
46 | new Error('Simmer: No element was specified for parsing.'),
47 | element
48 | )
49 | return false
50 | }
51 |
52 | // The parser cycles through a set of parsing methods specified in an order optimal
53 | // for creating as specific as possible a selector
54 | const parser = new Parser(parsingMethods)
55 |
56 | // get the element's ancestors
57 | const hierarchy = stackHierarchy(wrap(element), config.depth)
58 |
59 | // initialize the state of the selector
60 | let selectorState = {
61 | // the stack is used to build a layer of selectors, each layer coresponding to a specific element in the heirarchy
62 | // for each level we create a private stack of properties, so that we can then merge them
63 | // comfortably and allow all methods to see the level at which existing properties have been set
64 | stack: Array(hierarchy.length).fill().map(() => []),
65 | // follow the current specificity level of the selector - the higher the better
66 | specificity: 0
67 | }
68 |
69 | const validator = validateSelector(element, config, query, onError)
70 |
71 | // cycle through the available parsing methods and while we still have yet to find the requested element's one-to-one selector
72 | // we keep calling the methods until we are either satisfied or run out of methods
73 | while (!parser.finished() && !selectorState.verified) {
74 | try {
75 | selectorState = parser.next(
76 | hierarchy,
77 | selectorState,
78 | validator,
79 | config,
80 | query
81 | )
82 |
83 | // if we have reached a satisfactory level of specificity, try the selector, perhaps we have found our selector?
84 | if (
85 | selectorState.specificity >= config.specificityThreshold &&
86 | !selectorState.verified
87 | ) {
88 | selectorState.verified = validator(selectorState)
89 | }
90 | } catch (ex) {
91 | // handle error
92 | onError.call(simmer, ex, element)
93 | }
94 | }
95 |
96 | // if we were not able to produce a one-to-one selector, return false
97 | if (
98 | selectorState.verified === undefined ||
99 | selectorState.specificity < config.specificityThreshold
100 | ) {
101 | // if it is undefined then verfication has never been run!
102 | // try and verify, and if verification fails - return false
103 | // if it is false and the specificity is too low to actually try and find the element in the first place, then we may simply have not run
104 | // an up to date verification - try again
105 | selectorState.verified = validator(selectorState)
106 | }
107 |
108 | if (!selectorState.verified) {
109 | return false
110 | }
111 |
112 | if (selectorState.verificationDepth) {
113 | return convertSelectorStateIntoCSSSelector(
114 | selectorState,
115 | selectorState.verificationDepth
116 | )
117 | }
118 | return convertSelectorStateIntoCSSSelector(selectorState)
119 | }
120 |
121 | /**
122 | * Get/Set the configuration for the Simmer object
123 | * @param config (Object) A configuration object with any of the properties tweeked (none/depth/minimumSpecificity)
124 | * @example
125 |
126 | configuration({
127 | depth: 3
128 | });
129 |
130 | */
131 | simmer.configure = function (configValues = {}, scope = windowScope) {
132 | const newConfig = configure({
133 | ...config,
134 | ...configValues
135 | })
136 | return createSimmer(
137 | scope,
138 | newConfig,
139 | initQueryEngine(scope, newConfig.queryEngine)
140 | )
141 | }
142 |
143 | return simmer
144 | }
145 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Simmer JS [](https://travis-ci.org/gmmorris/simmerjs) [](https://codeclimate.com/github/gmmorris/simmerjs)
2 | =========
3 |
4 | ## Docs
5 |
6 | A pure Javascript reverse CSS selector engine which calculates a DOM element's unique CSS selector on the current page.
7 |
8 | ## Installation
9 | Simmer is now meant to be consumed as a module via npm and is no longer meant as a drop in script into the browser.
10 | That said, we still build a version meant to be dropped into the browser which resides on *window.Simmer* as before.
11 |
12 | We highly recommend you consume Simmer in the following manner, but in the interest of supporting exosting users we've retained the previous API.
13 |
14 | ```bash
15 | npm i --save simmerjs
16 | ```
17 |
18 | And then in your code:
19 | ```js
20 | import Simmer from 'simmerjs'
21 |
22 | const simmer = new Simmer()
23 |
24 | const el = document.getElementById('#SomeElement')
25 |
26 | expect(
27 | simmer(el)
28 | ).to.equal(
29 | '#SomeElement'
30 | )
31 |
32 | ```
33 |
34 | When using the distribution located in the repo under **dist/simmer.js** a global *Simmer* function will be exposed on the **window**.
35 | This is **not** the constructor, but rather a default instance which has exposed itself on the window wit ha default configuration.
36 | This is not an idea API and is meant to maintain the original API dating back to 2011 when this library was originally written.
37 |
38 | ### Basic usage
39 |
40 | To use Simmer to analyze an element and produce a unique CSS selector for it, all you have to do is instansiate a simmer query engine and pass it an element on the page.
41 | Usually you'd create a single instance which you would then use multiple times.
42 |
43 |
44 | ```html
45 |
46 |
47 |
48 |
49 |
50 | ```
51 |
52 | ```js
53 | import Simmer from 'simmerjs'
54 |
55 | const simmer = new Simmer(window, { /* some custom configuration */ })
56 | var myElement = document.getElementById("#myUniqueElement");
57 |
58 | console.log(simmer(myElement)); // prints "[id='myUniqueElement']"
59 | ```
60 | ## API
61 |
62 | ### Simmer
63 | ```js
64 | Simmer([Scope], [Options], [query])
65 | ```
66 |
67 | #### Scope
68 | When you create an instance of Simmer its first argument is the context in which Simmer should query for elements.
69 | Generally speaking this would be the **window**, which is the default value, but it would be overriden in a situation where you might be using Simmer against a Virtual Dom implementation.
70 | If you _are_ using a Virtual DOM, you should provide the Window object of the Virtual DOM
71 |
72 | #### Options
73 | The second argument is an Options object allowing you to override the default configuration for Simmer's behaviour.
74 |
75 | The options are:
76 |
77 | | Option | Default |
78 | | ------ | ------- |
79 | | **specificityThreshold** - A minimum specificty level. Once the parser reaches this level it starts verifying the selector after every method is called. This can cut down our execution time by avoiding needless parsing but can also hurt execution times by performing many verifications. Specificity is calculated based on the W3C spec: http://www.w3.org/TR/css3-selectors/#specificity | 100 |
80 | | **depth** - How deep into the DOM hierarchy should Simmer go in order to reach a unique selector. This is a delicate game because the higher the number the more likely you are to reach a unique selector, but it also means a longer and more breakable one. Assuming you want to store this selector to use later, making it longer also means it is more likely to change and loose it's validity. | 3 | |
81 | | **errorHandling** - How to handle errors which occur during the analysis
Valid Options
- _false_: errors are ignored by Simmer
- _true_: errors rethrown and expected to be caught by the user
- _a function callback will be called with two parameters: the exception and the element being analyzed
| false |
82 | | **selectorMaxLength** - A maximum length for the CSS selector can be specified - if no specific selector can be found which is shorter than this length then it is treated as if no selector could be found. | 520 |
83 |
84 | #### query
85 | The third argument is a query engine you wish Simmer to use when evaluating generated selectors.
86 | By default Simmer uses the **window.document.querySelectorAll** function and if you provide a window to the scope, Simmer will assume that you want it to use the **document.querySelectorAll** on that **window**.
87 | But if you wish Simmer to use another custom function, such as your own tweaked version of jQuery, you can do so by passing the third argument to the Simmer constructor.
88 |
89 | The signature the query function should provide is this:
90 |
91 | ##### query(selector: String, onError: Error => {}) : Array(DomElements)
92 | What this means is that the function you provide should expect to receive a string CSS selector and a function.
93 | It should them query for elements matching the selector and return an array of the results (even if there is only one result, it should be returned in an Array.).
94 |
95 | The second argument is a function which should be called if any error is encountered when querying for the elements.
96 | If an error occurs or a problem is encountered, instead of throwing, you should call the function and pass the error object to it. Simmer will then handle the error as per its configuration.
97 |
98 | ### Reconfiguring
99 | If you have an existing instance of Simmer, you can use its **configure** method to instanciate a new Simmer which has the same scope and configuration as the existing one, with any new configuration you wish to apply.
100 |
101 | So, for example, in the browser, you can replace the global Simmer wit ha newly configured version:
102 | ```js
103 |
104 | window.Simmer = window.Simmer.configure({
105 | depth: 10
106 | })
107 | ```
108 |
109 | And in Node:
110 | ```js
111 | import Simmer from 'simmerjs'
112 |
113 | const virtualWindow = new JSDom()
114 |
115 | const simmer = new Simmer(virtualWindow)
116 |
117 | const reconfiguredSimmer = simmer.configure({ /* some custom configuration */ })
118 |
119 | ```
120 |
121 | ### Conflict
122 | When the Simmer browser dist located at **dist/simmer.js** is injected in to the browser it adds the noConflict function on itself.
123 | This is not relevant in a Node environment and isn't available there.
124 |
125 | Just in case you also had the brilliant idea of using a variable called "Simmer", or you wish to move it off of the global object then you can use the noConflict method to receive a reference to the object and remove it from the window.
126 | Calling it will also revert the original value of window.Simmer which was there before loading the Simmer.js script (if there was one)
127 |
128 | ```js
129 | var mySimmer = Simmer.noConflict();
130 | mySimmer(myElm);
131 | ```
132 |
--------------------------------------------------------------------------------
/__tests__/integration/index.test.js:
--------------------------------------------------------------------------------
1 | import fixture from './fixture'
2 | import {
3 | queryEngine,
4 | NoResult,
5 | compareParentElementAndSimmer,
6 | compareElementsAndSimmer
7 | } from './utils'
8 |
9 | import createSimmer from '../../modules/simmer'
10 | import exposeOnWindow from '../../modules/exposeOnWindow'
11 |
12 | const { JSDOM } = require('jsdom')
13 |
14 | const installSimmerOnWindow = windowScope => {
15 | exposeOnWindow(
16 | windowScope,
17 | createSimmer(windowScope, { errorHandling: e => console.log(e) })
18 | )
19 | return windowScope
20 | }
21 |
22 | const createWindow = (dom = '') =>
23 | installSimmerOnWindow(new JSDOM(`${dom}`).window)
24 |
25 | test(`can analyze an element with an ID`, function () {
26 | const windowScope = createWindow(fixture)
27 | var elements = compareElementsAndSimmer(windowScope, '#BodyDiv')
28 | expect(elements).not.toBe(undefined)
29 | expect(elements.SimmerEl).not.toBe(undefined)
30 | expect(elements.el).not.toBe(undefined)
31 | expect(elements.el).toBe(elements.SimmerEl)
32 | })
33 |
34 | test(`can analyze an element with classes only (No ID)`, function () {
35 | const windowScope = createWindow(fixture)
36 | var elements = compareElementsAndSimmer(windowScope, '.header')
37 | expect(elements).not.toBe(undefined)
38 | expect(elements.SimmerEl).not.toBe(undefined)
39 | expect(elements.el).not.toBe(undefined)
40 | expect(elements.el).toBe(elements.SimmerEl)
41 | })
42 |
43 | test(`can analyze an element which is a child of an element with an ID`, function () {
44 | const windowScope = createWindow(fixture)
45 | var elements = compareElementsAndSimmer(windowScope, '#NavBar ul')
46 | expect(elements).not.toBe(undefined)
47 | expect(elements.SimmerEl).not.toBe(undefined)
48 | expect(elements.el).not.toBe(undefined)
49 | expect(elements.el).toBe(elements.SimmerEl)
50 | })
51 |
52 | test(`can analyze anelement which has a class only (no IDs on it or its direct parent)`, function () {
53 | const windowScope = createWindow(fixture)
54 | var elements = compareElementsAndSimmer(windowScope, '.Active')
55 | expect(elements).not.toBe(undefined)
56 | expect(elements.el).toBe(elements.SimmerEl)
57 | expect(elements.SimmerEl).not.toBe(undefined)
58 | expect(elements.el).not.toBe(undefined)
59 | })
60 |
61 | test(`can analyze an element with an invalid ID (isn't unique)`, function () {
62 | const windowScope = createWindow(fixture)
63 | var elements = compareElementsAndSimmer(
64 | windowScope,
65 | '#BodyDiv div:nth-child(3)'
66 | )
67 | expect(elements.el).toBe(elements.SimmerEl)
68 | expect(elements.SimmerEl).not.toBe(undefined)
69 | expect(elements.el).not.toBe(undefined)
70 | expect(elements).not.toBe(undefined)
71 | })
72 |
73 | test(`can analyze an element with an invalid ID (isn't unique) at same level as other element with the same ID`, function () {
74 | const windowScope = createWindow(fixture)
75 | var elements = compareElementsAndSimmer(
76 | windowScope,
77 | '#BodyDiv div:nth-child(1)'
78 | )
79 | expect(elements).not.toBe(undefined)
80 | expect(elements.el).toBe(elements.SimmerEl)
81 | expect(elements.SimmerEl).not.toBe(undefined)
82 | expect(elements.el).not.toBe(undefined)
83 | })
84 |
85 | test(`can analyze an element with identical siblings (neither unique ID nor class)`, function () {
86 | const windowScope = createWindow(fixture)
87 | var elements = compareElementsAndSimmer(windowScope, '.Edge:nth-child(5)')
88 | expect(elements).not.toBe(undefined)
89 | expect(elements.SimmerEl).not.toBe(undefined)
90 | expect(elements.el).not.toBe(undefined)
91 | expect(elements.el).toBe(elements.SimmerEl)
92 | })
93 |
94 | test(`can analyze an element with a parent which has identical siblings`, function () {
95 | const windowScope = createWindow(fixture)
96 | var elements = compareParentElementAndSimmer(windowScope, '#helper') // parent of #helper!
97 | expect(elements).not.toBe(undefined)
98 | expect(elements.SimmerEl).not.toBe(undefined)
99 | expect(elements.el).not.toBe(undefined)
100 | expect(elements.el).toBe(elements.SimmerEl)
101 | })
102 |
103 | test(`can analyze an image who'se paren't has an invalid ID`, function () {
104 | const windowScope = createWindow(fixture)
105 | var elements = compareElementsAndSimmer(
106 | windowScope,
107 | '#BodyDiv div:nth-child(1) img'
108 | )
109 | expect(elements).not.toBe(undefined)
110 | expect(elements.SimmerEl).not.toBe(undefined)
111 | expect(elements.el).not.toBe(undefined)
112 | expect(elements.el).toBe(elements.SimmerEl)
113 | })
114 |
115 | test(`can analyze an image with a non unique src attribute`, function () {
116 | const windowScope = createWindow(fixture)
117 | var elements = compareElementsAndSimmer(
118 | windowScope,
119 | '#BodyDiv div:nth-child(2) img'
120 | )
121 | expect(elements).not.toBe(undefined)
122 | expect(elements.SimmerEl).not.toBe(undefined)
123 | expect(elements.el).not.toBe(undefined)
124 | expect(elements.el).toBe(elements.SimmerEl)
125 | })
126 |
127 | test(`can analyze an image with a unique src attribute`, function () {
128 | const windowScope = createWindow(fixture)
129 | var elements = compareElementsAndSimmer(
130 | windowScope,
131 | 'img[src="./image/Pixel.png"]'
132 | )
133 | expect(elements).not.toBe(undefined)
134 | expect(elements.SimmerEl).not.toBe(undefined)
135 | expect(elements.el).not.toBe(undefined)
136 | expect(elements.el).toBe(elements.SimmerEl)
137 | })
138 |
139 | test(`can analyze a child of a hierarchy with identical siblings and no unique IDs`, function () {
140 | const windowScope = createWindow(fixture)
141 | var elements = compareElementsAndSimmer(
142 | windowScope,
143 | 'tr:nth-child(2) td:nth-child(3)'
144 | )
145 | expect(elements).not.toBe(undefined)
146 | expect(elements.SimmerEl).not.toBe(undefined)
147 | expect(elements.el).not.toBe(undefined)
148 | expect(elements.el).toBe(elements.SimmerEl)
149 | })
150 |
151 | test(`can analyze an anchor (link) with a unique href attribute`, function () {
152 | const windowScope = createWindow(fixture)
153 | var elements = compareElementsAndSimmer(
154 | windowScope,
155 | 'table tr th:nth-child(2) a'
156 | )
157 | expect(elements).not.toBe(undefined)
158 | expect(elements.SimmerEl).not.toBe(undefined)
159 | expect(elements.el).not.toBe(undefined)
160 | expect(elements.el).toBe(elements.SimmerEl)
161 | })
162 |
163 | test(`can analyze a link with a non unique href attribute and a heirarchy which has no unique IDs`, function () {
164 | const windowScope = createWindow(fixture)
165 | var elements = compareElementsAndSimmer(
166 | windowScope,
167 | 'table tr th:nth-child(3) a'
168 | )
169 | expect(elements).not.toBe(undefined)
170 | expect(elements.SimmerEl).not.toBe(undefined)
171 | expect(elements.el).not.toBe(undefined)
172 | expect(elements.el).toBe(elements.SimmerEl)
173 | })
174 |
175 | test(`can analyze an element with identical siblings, no IDs and sibling with the same class`, function () {
176 | const windowScope = createWindow(fixture)
177 | var elements = compareElementsAndSimmer(windowScope, 'td span:nth-child(3)')
178 | expect(elements).not.toBe(undefined)
179 | expect(elements.SimmerEl).not.toBe(undefined)
180 | expect(elements.el).not.toBe(undefined)
181 | expect(elements.el).toBe(elements.SimmerEl)
182 | })
183 |
184 | test(`can analyze an element with identical siblings, no ID and same class as sibling but different class order`, function () {
185 | const windowScope = createWindow(fixture)
186 | var elements = compareElementsAndSimmer(windowScope, 'td span:nth-child(4)')
187 | expect(elements).not.toBe(undefined)
188 | expect(elements.SimmerEl).not.toBe(undefined)
189 | expect(elements.el).not.toBe(undefined)
190 | expect(elements.el).toBe(elements.SimmerEl)
191 | })
192 |
193 | test(`can analyze an element with identical siblings, no ID and different classes completely`, function () {
194 | const windowScope = createWindow(fixture)
195 | var elements = compareElementsAndSimmer(windowScope, 'td span:nth-child(2)')
196 | expect(elements).not.toBe(undefined)
197 | expect(elements.SimmerEl).not.toBe(undefined)
198 | expect(elements.el).not.toBe(undefined)
199 | expect(elements.el).toBe(elements.SimmerEl)
200 | })
201 |
202 | test(`cannot parse an element with an identical hierarchy whithin the Simmer's default configured depth`, function () {
203 | const windowScope = createWindow(fixture)
204 | var placeHolder = queryEngine(windowScope.document).query('#placeholderId')
205 | placeHolder.removeAttribute('id')
206 | const elements = compareElementsAndSimmer(windowScope, placeHolder[0])
207 | expect(elements).not.toBe(undefined)
208 | expect(elements).toEqual(NoResult)
209 | })
210 |
211 | test(`can analyze an element with a parent which has an invalid Tag name`, function () {
212 | const windowScope = createWindow(fixture)
213 | var elements = compareElementsAndSimmer(
214 | windowScope,
215 | '.invalid_child_tag span'
216 | )
217 | expect(elements).not.toBe(undefined)
218 | expect(elements.SimmerEl).not.toBe(undefined)
219 | expect(elements.el).not.toBe(undefined)
220 | expect(elements.el).toBe(elements.SimmerEl)
221 | })
222 |
223 | test(`can analyze an element based only on child elements when specific enough at level 1`, function () {
224 | const windowScope = createWindow(fixture)
225 | var elements = compareElementsAndSimmer(windowScope, '.uniqueClassName')
226 | expect(elements).not.toBe(undefined)
227 | expect(elements.SimmerEl).not.toBe(undefined)
228 | expect(elements.el).not.toBe(undefined)
229 | expect(elements.el).toBe(elements.SimmerEl)
230 | // make sure the selector is one level deep
231 | expect(elements.selector.split('>').length).toBe(1)
232 | })
233 |
234 | test(`can analyze an element based only on child elements when specific enough at level 2`, function () {
235 | const windowScope = createWindow(fixture)
236 | var elements = compareElementsAndSimmer(
237 | windowScope,
238 | '.secondLevelUniqueClassName div'
239 | )
240 | expect(elements).not.toBe(undefined)
241 | expect(elements.SimmerEl).not.toBe(undefined)
242 | expect(elements.el).not.toBe(undefined)
243 | expect(elements.el).toBe(elements.SimmerEl)
244 | // make sure the selector is one level deep
245 | expect(elements.selector.split('>').length).toBe(2)
246 | })
247 |
248 | test(`can analyze an element with a valid ID that ends with numbers in it's ID`, function () {
249 | const windowScope = createWindow(fixture)
250 | var elements = compareElementsAndSimmer(windowScope, '#a111')
251 | expect(elements).not.toBe(undefined)
252 | expect(elements.SimmerEl).not.toBe(undefined)
253 | expect(elements.el).not.toBe(undefined)
254 | expect(elements.el).toBe(elements.SimmerEl)
255 | // make sure the selector is one level deep
256 | expect(elements.selector.match('a111')).toBeTruthy()
257 | })
258 |
259 | test(`can analyze an element with a valid ID that has numbers in the middle of it's ID`, function () {
260 | const windowScope = createWindow(fixture)
261 | var elements = compareElementsAndSimmer(windowScope, '#a111a')
262 | expect(elements).not.toBe(undefined)
263 | expect(elements.SimmerEl).not.toBe(undefined)
264 | expect(elements.el).not.toBe(undefined)
265 | expect(elements.el).toBe(elements.SimmerEl)
266 | // make sure the selector is one level deep
267 | expect(elements.selector.match('a111a')).toBeTruthy()
268 | })
269 |
270 | test(`can analyze an element with an invalid ID (not unique)`, function () {
271 | const windowScope = createWindow(fixture)
272 | var elements = compareElementsAndSimmer(windowScope, '.invalidIDElement')
273 | expect(elements).not.toBe(undefined)
274 | expect(elements.SimmerEl).not.toBe(undefined)
275 | expect(elements.el).not.toBe(undefined)
276 | expect(elements.el).toBe(elements.SimmerEl)
277 | // make sure the selector is one level deep
278 | expect(elements.selector.match(':asd')).toBe(null)
279 | })
280 |
281 | test(`can't analyze an element which is longer than the selectorMaxLength chars`, function () {
282 | const windowScope = createWindow(fixture)
283 | const placeHolder = queryEngine(windowScope.document).query(
284 | '#aZROCRDPX41Qkden3aiC3o9Tkl0xqENUjIgNSWbe6pSddw86ogN018T9lD67zAF1YHaLkRngy8YVq88IBfqdvtO9aXZZbD1NsSBiUo6txcv22ufrkRs9AZKkxIkTF1gNAZ3Oh4M6TcYWRARVJqOZwo3dQufTDm904ep3yHZ5vdHqIyFqTFdZYPWYumx5gJBmWn7GbZQ3O3HodzmHYIHhCYg4dCDfSN8iCHzezerdHbzWUKR7pzMDOzvq017a63LSqYkSJ0gWxrgJFj45HR25eJj5szEFmuQlCfkbWpCwYopeNhy1toC9PvSfVCnHpI7EXeqVcspP0aQISflgD0pBMgg2ieITRa5gXRnKoDdem1yXvHjcDBXJFoUy63zDwg6tTtRR6rijcvoxNzGjWCgQhdqzlv6CW2CVgK2aa0VSX9RMSUTSKXmru7mvZUXJxv7RO7n1Zw9meFygwHwgNrZgeRWVYhsXBtEG8Bak7sPQ7x37QXgIgbJRcbhqMK2F5baa'
285 | )
286 | const elements = compareElementsAndSimmer(windowScope, placeHolder[0])
287 | expect(elements).not.toBe(undefined)
288 | expect(elements).toEqual(NoResult)
289 | })
290 |
--------------------------------------------------------------------------------
/__tests__/e2e/index.js:
--------------------------------------------------------------------------------
1 | // var config = require('../../nightwatch.conf.js')
2 |
3 | const compareElementsAndSimmer = (
4 | browser,
5 | selector,
6 | onFinish = () => browser.end()
7 | ) => {
8 | browser.execute(
9 | function (selector) {
10 | var el = document.querySelector(selector)
11 | var simmerSelector = window.Simmer(el)
12 | var simmerEl = document.querySelector(simmerSelector)
13 |
14 | return {
15 | simmerSelector,
16 | didTheyMatch: el === simmerEl
17 | }
18 | },
19 | [selector],
20 | function (result) {
21 | browser.assert.equal(result.value.didTheyMatch, true)
22 | onFinish(result.value.simmerSelector)
23 | }
24 | )
25 | }
26 |
27 | const assertUnqueriableBySimmer = (
28 | browser,
29 | selector,
30 | onFinish = () => browser.end()
31 | ) => {
32 | browser.execute(
33 | function (selector) {
34 | var el = document.querySelector(selector)
35 | var simmerSelector = window.Simmer(el)
36 |
37 | return {
38 | simmerSelector,
39 | wasUnselectable: simmerSelector === false
40 | }
41 | },
42 | [selector],
43 | function (result) {
44 | browser.assert.equal(result.value.wasUnselectable, true)
45 | onFinish(result.value.simmerSelector)
46 | }
47 | )
48 | }
49 |
50 | const compareElementsAndSimmerWithReconfiguration = (
51 | browser,
52 | id,
53 | configuration,
54 | onFinish = () => browser.end()
55 | ) => {
56 | browser.execute(
57 | function (id, configuration) {
58 | var el = document.getElementById(id)
59 | el.removeAttribute('id')
60 |
61 | var simmerSelector = window.Simmer(el)
62 |
63 | var reconfiguredSimmer = window.Simmer.configure(configuration)
64 |
65 | var reconfiguredSimmerSelector = reconfiguredSimmer(el)
66 | var simmerEl = document.querySelector(reconfiguredSimmerSelector)
67 |
68 | return {
69 | configuration,
70 | reconfiguredSimmerSelector,
71 | didItFailAtDefault: simmerSelector === false,
72 | didTheyMatch: el === simmerEl
73 | }
74 | },
75 | [id, configuration],
76 | function (result) {
77 | browser.assert.equal(result.value.didItFailAtDefault, true)
78 | browser.assert.equal(result.value.didTheyMatch, true)
79 | onFinish(result.value.reconfiguredSimmerSelector)
80 | }
81 | )
82 | }
83 |
84 | let server
85 | module.exports = {
86 | before: function (browser, done) {
87 | server = require('../server')(done) // done is a callback that executes when the server is started
88 | },
89 |
90 | after: function () {
91 | server.close()
92 | },
93 |
94 | 'can analyze an element with an ID': function (browser) {
95 | browser
96 | .url('localhost:3993') // visit the local url
97 | .waitForElementVisible('body') // wait for the body to be rendered
98 |
99 | compareElementsAndSimmer(browser, '#BodyDiv')
100 | },
101 |
102 | 'can analyze an element with classes only (No ID)': function (browser) {
103 | browser
104 | .url('localhost:3993') // visit the local url
105 | .waitForElementVisible('body') // wait for the body to be rendered
106 |
107 | compareElementsAndSimmer(browser, '.header')
108 | },
109 |
110 | 'can analyze an element which is a child of an element with an ID': function (
111 | browser
112 | ) {
113 | browser
114 | .url('localhost:3993') // visit the local url
115 | .waitForElementVisible('body') // wait for the body to be rendered
116 |
117 | compareElementsAndSimmer(browser, '#NavBar ul')
118 | },
119 |
120 | 'can analyze anelement which has a class only (no IDs on it or its direct parent)': function (
121 | browser
122 | ) {
123 | browser
124 | .url('localhost:3993') // visit the local url
125 | .waitForElementVisible('body') // wait for the body to be rendered
126 |
127 | compareElementsAndSimmer(browser, '.Active')
128 | },
129 |
130 | 'can analyze an element with an invalid ID (isnt unique)': function (browser) {
131 | browser
132 | .url('localhost:3993') // visit the local url
133 | .waitForElementVisible('body') // wait for the body to be rendered
134 |
135 | compareElementsAndSimmer(browser, '#BodyDiv div:nth-child(3)')
136 | },
137 |
138 | 'can analyze an element with an invalid ID (isnt unique) at same level as other element with the same ID': function (
139 | browser
140 | ) {
141 | browser
142 | .url('localhost:3993') // visit the local url
143 | .waitForElementVisible('body') // wait for the body to be rendered
144 |
145 | compareElementsAndSimmer(browser, '#BodyDiv div:nth-child(1)')
146 | },
147 |
148 | 'can analyze an element with identical siblings (neither unique ID nor class)': function (
149 | browser
150 | ) {
151 | browser
152 | .url('localhost:3993') // visit the local url
153 | .waitForElementVisible('body') // wait for the body to be rendered
154 |
155 | compareElementsAndSimmer(browser, '.Edge:nth-child(5)')
156 | },
157 |
158 | 'can analyze an element with a parent which has identical siblings': function (
159 | browser
160 | ) {
161 | browser
162 | .url('localhost:3993') // visit the local url
163 | .waitForElementVisible('body') // wait for the body to be rendered
164 |
165 | compareElementsAndSimmer(browser, '#helper') // parent of #helper!
166 | },
167 |
168 | 'can analyze an image whose parent has an invalid ID': function (browser) {
169 | browser
170 | .url('localhost:3993') // visit the local url
171 | .waitForElementVisible('body') // wait for the body to be rendered
172 |
173 | compareElementsAndSimmer(browser, '#BodyDiv div:nth-child(1) img')
174 | },
175 |
176 | 'can analyze an image with a non unique src attribute': function (browser) {
177 | browser
178 | .url('localhost:3993') // visit the local url
179 | .waitForElementVisible('body') // wait for the body to be rendered
180 |
181 | compareElementsAndSimmer(browser, '#BodyDiv div:nth-child(2) img')
182 | },
183 |
184 | 'can analyze an image with a unique src attribute': function (browser) {
185 | browser
186 | .url('localhost:3993') // visit the local url
187 | .waitForElementVisible('body') // wait for the body to be rendered
188 |
189 | compareElementsAndSimmer(browser, 'img[src="./image/Pixel.png"]')
190 | },
191 |
192 | 'can analyze a child of a hierarchy with identical siblings and no unique IDs': function (
193 | browser
194 | ) {
195 | browser
196 | .url('localhost:3993') // visit the local url
197 | .waitForElementVisible('body') // wait for the body to be rendered
198 |
199 | compareElementsAndSimmer(browser, 'tr:nth-child(2) td:nth-child(3)')
200 | },
201 |
202 | 'can analyze an anchor (link) with a unique href attribute': function (
203 | browser
204 | ) {
205 | browser
206 | .url('localhost:3993') // visit the local url
207 | .waitForElementVisible('body') // wait for the body to be rendered
208 |
209 | compareElementsAndSimmer(browser, 'table tr th:nth-child(2) a')
210 | },
211 |
212 | 'can analyze a link with a non unique href attribute and a heirarchy which has no unique IDs': function (
213 | browser
214 | ) {
215 | browser
216 | .url('localhost:3993') // visit the local url
217 | .waitForElementVisible('body') // wait for the body to be rendered
218 |
219 | compareElementsAndSimmer(browser, 'table tr th:nth-child(3) a')
220 | },
221 |
222 | 'can analyze an element with identical siblings, no IDs and sibling with the same class': function (
223 | browser
224 | ) {
225 | browser
226 | .url('localhost:3993') // visit the local url
227 | .waitForElementVisible('body') // wait for the body to be rendered
228 |
229 | compareElementsAndSimmer(browser, 'td span:nth-child(3)')
230 | },
231 |
232 | 'can analyze an element with identical siblings, no ID and same class as sibling but different class order': function (
233 | browser
234 | ) {
235 | browser
236 | .url('localhost:3993') // visit the local url
237 | .waitForElementVisible('body') // wait for the body to be rendered
238 |
239 | compareElementsAndSimmer(browser, 'td span:nth-child(4)')
240 | },
241 |
242 | 'can analyze an element with identical siblings, no ID and different classes completely': function (
243 | browser
244 | ) {
245 | browser
246 | .url('localhost:3993') // visit the local url
247 | .waitForElementVisible('body') // wait for the body to be rendered
248 |
249 | compareElementsAndSimmer(browser, 'td span:nth-child(2)')
250 | },
251 |
252 | 'cannot parse an element with an identical hierarchy whithin the Simmers default configured depth': function (
253 | browser
254 | ) {
255 | browser
256 | .url('localhost:3993') // visit the local url
257 | .waitForElementVisible('body') // wait for the body to be rendered
258 |
259 | compareElementsAndSimmerWithReconfiguration(
260 | browser,
261 | 'placeholderId',
262 | { depth: 7 },
263 | selector => {
264 | browser.assert.equal(
265 | selector,
266 | "SPAN[id='deepId'] > SPAN > SPAN > SPAN > SPAN > P > A"
267 | )
268 | browser.end()
269 | }
270 | )
271 | },
272 |
273 | 'can analyze an element with a parent which has an invalid Tag name': function (
274 | browser
275 | ) {
276 | browser
277 | .url('localhost:3993') // visit the local url
278 | .waitForElementVisible('body') // wait for the body to be rendered
279 |
280 | compareElementsAndSimmer(browser, '.invalid_child_tag span')
281 | },
282 |
283 | 'can analyze an element based only on child elements when specific enough at level 1': function (
284 | browser
285 | ) {
286 | browser
287 | .url('localhost:3993') // visit the local url
288 | .waitForElementVisible('body') // wait for the body to be rendered
289 |
290 | compareElementsAndSimmer(browser, '.uniqueClassName', selector => {
291 | // make sure the selector is one level deep
292 | browser.assert.equal(selector.split('>').length, 1)
293 | browser.end()
294 | })
295 | },
296 |
297 | 'can analyze an element based only on child elements when specific enough at level 2': function (
298 | browser
299 | ) {
300 | browser
301 | .url('localhost:3993') // visit the local url
302 | .waitForElementVisible('body') // wait for the body to be rendered
303 |
304 | compareElementsAndSimmer(
305 | browser,
306 | '.secondLevelUniqueClassName div',
307 | selector => {
308 | // make sure the selector is one level deep
309 | browser.assert.equal(selector.split('>').length, 2)
310 | browser.end()
311 | }
312 | )
313 | },
314 |
315 | 'can analyze an element with a valid ID that ends with numbers in its ID': function (
316 | browser
317 | ) {
318 | browser
319 | .url('localhost:3993') // visit the local url
320 | .waitForElementVisible('body') // wait for the body to be rendered
321 |
322 | compareElementsAndSimmer(browser, '#a111', selector => {
323 | browser.assert.equal(selector.match('a111')[0], 'a111')
324 | browser.end()
325 | })
326 | },
327 |
328 | 'can analyze an element with a valid ID that has numbers in the middle of its ID': function (
329 | browser
330 | ) {
331 | browser
332 | .url('localhost:3993') // visit the local url
333 | .waitForElementVisible('body') // wait for the body to be rendered
334 |
335 | compareElementsAndSimmer(browser, '#a111a', selector => {
336 | browser.assert.equal(selector.match('a111a')[0], 'a111a')
337 | browser.end()
338 | })
339 | },
340 |
341 | 'can analyze an element with an invalid ID (not unique)': function (browser) {
342 | browser
343 | .url('localhost:3993') // visit the local url
344 | .waitForElementVisible('body') // wait for the body to be rendered
345 |
346 | compareElementsAndSimmer(browser, '.invalidIDElement', selector => {
347 | // make sure the invalid ID is ommited
348 | browser.assert.equal(selector.match(':asd'), null)
349 | browser.end()
350 | })
351 | },
352 |
353 | 'cant analyze an element which is longer than the selectorMaxLength chars': function (
354 | browser
355 | ) {
356 | browser
357 | .url('localhost:3993') // visit the local url
358 | .waitForElementVisible('body') // wait for the body to be rendered
359 |
360 | assertUnqueriableBySimmer(
361 | browser,
362 | '#aZROCRDPX41Qkden3aiC3o9Tkl0xqENUjIgNSWbe6pSddw86ogN018T9lD67zAF1YHaLkRngy8YVq88IBfqdvtO9aXZZbD1NsSBiUo6txcv22ufrkRs9AZKkxIkTF1gNAZ3Oh4M6TcYWRARVJqOZwo3dQufTDm904ep3yHZ5vdHqIyFqTFdZYPWYumx5gJBmWn7GbZQ3O3HodzmHYIHhCYg4dCDfSN8iCHzezerdHbzWUKR7pzMDOzvq017a63LSqYkSJ0gWxrgJFj45HR25eJj5szEFmuQlCfkbWpCwYopeNhy1toC9PvSfVCnHpI7EXeqVcspP0aQISflgD0pBMgg2ieITRa5gXRnKoDdem1yXvHjcDBXJFoUy63zDwg6tTtRR6rijcvoxNzGjWCgQhdqzlv6CW2CVgK2aa0VSX9RMSUTSKXmru7mvZUXJxv7RO7n1Zw9meFygwHwgNrZgeRWVYhsXBtEG8Bak7sPQ7x37QXgIgbJRcbhqMK2F5baa'
363 | )
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/dist/simmer.js:
--------------------------------------------------------------------------------
1 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.owns=function(d,k){return Object.prototype.hasOwnProperty.call(d,k)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(d,k,e){d!=Array.prototype&&d!=Object.prototype&&(d[k]=e.value)};
2 | $jscomp.getGlobal=function(d){return"undefined"!=typeof window&&window===d?d:"undefined"!=typeof global&&null!=global?global:d};$jscomp.global=$jscomp.getGlobal(this);$jscomp.polyfill=function(d,k,e,h){if(k){e=$jscomp.global;d=d.split(".");for(h=0;he&&(e=Math.max(0,k+e));if(null==h||h>k)h=k;h=Number(h);0>h&&(h=Math.max(0,k+h));for(e=Number(e||0);e=b;b=b&&!aa(a)}return b}function aa(a){a=Z(a)?na.call(a):"";return"[object Function]"==a||"[object GeneratorFunction]"==a}function Z(a){var b="undefined"===typeof a?"undefined":l(a);return!!a&&("object"==b||"function"==b)}function mb(a,b){return 0=b)throw Error("Simmer: An invalid depth of "+b+" has been specified");return Array(b-1).fill().reduce(function(a,b){a[a.length-1].parent()&&(b=a[a.length-1].parent(),a.push(b));return a},[a])}function sa(){return ta({},zb,0=g.specificityThreshold&&!e.verified&&(e.verified=h(e))}catch(Bb){a.call(eb,Bb,b)}if(void 0===e.verified||e.specificityc?-1:1):c===c?c:0):c=0===c?c:0,b=c%1,c=c===c?b?c-b:c:0);b=c;c=0;var d=0>b?0:b;b=-1;var e=a.length;0>c&&(c=-c>e?0:e+c);d=d>e?
24 | e:d;0>d&&(d+=e);e=c>d?0:d-c>>>0;c>>>=0;for(d=Array(e);++ba)return!1;a==b.length-1?b.pop():Gb.call(b,
27 | a,1);return!0};C.prototype.get=function(a){var b=this.__data__;a=O(b,a);return 0>a?void 0:b[a][1]};C.prototype.has=function(a){return-1d?c.push([a,b]):c[d][1]=b;return this};D.prototype.clear=function(){this.__data__={hash:new x,map:new (Hb||C),string:new x}};D.prototype["delete"]=function(a){return P(this,a)["delete"](a)};D.prototype.get=function(a){return P(this,a).get(a)};D.prototype.has=function(a){return P(this,
28 | a).has(a)};D.prototype.set=function(a,b){P(this,a).set(a,b);return this};N.prototype.add=N.prototype.push=function(a){this.__data__.set(a,"__lodash_hash_undefined__");return this};N.prototype.has=function(a){return this.__data__.has(a)};var y=function(a,b){b=ha(void 0===b?a.length-1:b,0);return function(){for(var c=arguments,d=-1,e=ha(c.length-b,0),h=Array(e);++dp))return!1;if((k=h.get(a))&&h.get(b))return k==b;var k=-1,t=!0,u=g&1?new x:void 0;h.set(a,b);for(h.set(b,a);++k=a}function ia(a){var b="undefined"===typeof a?"undefined":l(a);return!!a&&("object"==b||"function"==b)}function ya(a){return!!a&&"object"==("undefined"===typeof a?"undefined":l(a))}function Wa(a){return"symbol"==
43 | ("undefined"===typeof a?"undefined":l(a))||ya(a)&&"[object Symbol]"==U.call(a)}function za(a){if(Aa(a)){if(F(a)||Va(a)){var b=a.length;for(var c=String,f=-1,d=Array(b);++fa)return!1;a==b.length-1?b.pop():Ja.call(b,a,1);return!0};v.prototype.get=
49 | function(a){var b=this.__data__;a=y(b,a);return 0>a?void 0:b[a][1]};v.prototype.has=function(a){return-1d?c.push([a,b]):c[d][1]=b;return this};z.prototype.clear=function(){this.__data__={hash:new r,map:new (ja||v),string:new r}};z.prototype["delete"]=function(a){return Ba(this,a)["delete"](a)};z.prototype.get=function(a){return Ba(this,a).get(a)};z.prototype.has=function(a){return Ba(this,a).has(a)};z.prototype.set=
50 | function(a,b){Ba(this,a).set(a,b);return this};x.prototype.add=x.prototype.push=function(a){this.__data__.set(a,"__lodash_hash_undefined__");return this};x.prototype.has=function(a){return this.__data__.has(a)};E.prototype.clear=function(){this.__data__=new v};E.prototype["delete"]=function(a){return this.__data__["delete"](a)};E.prototype.get=function(a){return this.__data__.get(a)};E.prototype.has=function(a){return this.__data__.has(a)};E.prototype.set=function(a,b){var c=this.__data__;if(c instanceof
51 | v){c=c.__data__;if(!ja||199>c.length)return c.push([a,b]),this;c=this.__data__=new z(c)}c.set(a,b);return this};var Qa=function(a,b){return function(c,d){if(null==c)return c;if(!Aa(c))return a(c,d);for(var f=c.length,e=b?f:-1,g=Object(c);(b?e--:++e=d.specificityThreshold&&c(a)&&(a.verified=!0);a.verified||0!==g||(a.stack[g].pop(),a.specificity-=100);return a}),Eb(b,1)[0]||a)},b)});w.addMethod(function(a,b){return a.reduce(function(a,b,d){[b.el.nodeName].filter(e).forEach(function(b){a.stack[d].splice(0,0,b);a.specificity+=10});return a},b)});w.addMethod(function(a,b,c){a=a[0];var d=a.el.nodeName;la[d]&&(b=la[d](b,a),c(b)?b.verified=!0:
56 | b.stack[0].pop());return b});w.addMethod(function(a,b){return a.reduce(function(a,b,d){b=Fb(b.getClasses(),10).filter(h).map(function(a){return"."+a});b.length&&(a.stack[d].push(b.join("")),a.specificity+=10*b.length);return a},b)});w.addMethod(function(a,b,c){return a.reduce(function(a,b,d){if(!a.verified){var e=b.prevAll(),g=b.nextAll(),h=e.length+1;!e.length&&!g.length||mb(b,[].concat(ca(e),ca(g)))||(a.stack[d].push(":nth-child("+h+")"),a.verified=c(a))}return a},b)});var Fa=1/0,ra=0/0,sb=/^\s+|\s+$/g,
57 | wb=/^[-+]0x[0-9a-f]+$/i,tb=/^0b[01]+$/i,ub=/^0o[0-7]+$/i,vb=parseInt,rb=Object.prototype.toString,Ib=function(a,b,c){var d=a?a.length:0;if(!d)return[];c||void 0===b?b=1:(b?(b=qb(b),b=b===Fa||b===-Fa?1.7976931348623157e+308*(0>b?-1:1):b===b?b:0):b=0===b?b:0,c=b%1,b=b===b?c?b-c:b:0);b=d-b;b=0>b?0:b;var e=d,d=-1;c=a.length;0>b&&(b=-b>c?0:c+b);e=e>c?c:e;0>e&&(e+=c);c=b>e?0:e-b>>>0;b>>>=0;for(e=Array(c);++de)return!1;g=c(g,d);if(g=1===g.length&&(void 0!==a.el?g[0]===a.el:g[0]===a))b.verificationDepth=h}return g}},zb={queryEngine:null,specificityThreshold:100,depth:3,errorHandling:!1,selectorMaxLength:512};
59 | (function(a,b){var c=a.Simmer;a.Simmer=b;b.noConflict=function(){a.Simmer=c;return b}})(window,ba(window))})();
60 |
--------------------------------------------------------------------------------