Best JavaScript code snippet using tracetest
test-bind-impl.js
Source:test-bind-impl.js  
1/**2 * Copyright 2016 The AMP HTML Authors. All Rights Reserved.3 *4 * Licensed under the Apache License, Version 2.0 (the "License");5 * you may not use this file except in compliance with the License.6 * You may obtain a copy of the License at7 *8 *      http://www.apache.org/licenses/LICENSE-2.09 *10 * Unless required by applicable law or agreed to in writing, software11 * distributed under the License is distributed on an "AS-IS" BASIS,12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13 * See the License for the specific language governing permissions and14 * limitations under the License.15 */16/**17 * @fileoverview "Unit" test for bind-impl.js. Runs as an integration test18 * because it requires building web-worker binary.19 */20import * as fakeTimers from '@sinonjs/fake-timers';21import {AmpEvents} from '#core/constants/amp-events';22import {Bind} from '../../bind-impl';23import {BindEvents} from '../../bind-events';24import {Deferred} from '#core/data-structures/promise';25import {RAW_OBJECT_ARGS_KEY} from '#core/constants/action-constants';26import {Services} from '#service';27import {chunkInstanceForTesting} from '../../../../../src/chunk';28import {dev, user} from '../../../../../src/log';29import {toArray} from '#core/types/array';30/**31 * @param {!Object} env32 * @param {?Element} container33 * @param {string} binding34 * @param {string=} opts.tag Tag name of element (default is <p>).35 * @param {boolean=} opts.amp Is this an AMP element?36 * @param {boolean=} opts.insertInHead Add element to document <head>?37 * @param {boolean=} opts.insertQuerySelectorAttr Add i-amphtml-binding attribute to element?38 * @return {!Element}39 */40function createElement(env, container, binding, opts = {}) {41  const {42    amp = false,43    insertInHead = false,44    insertQuerySelectorAttr = false,45    tag = 'p',46  } = opts;47  const div = env.win.document.createElement('div');48  div.innerHTML = `<${tag} ${binding}></${tag}>`;49  const element = div.firstElementChild;50  if (amp) {51    element.className = 'i-amphtml-foo -amp-foo amp-foo';52    element.mutatedAttributesCallback = () => {};53  }54  if (insertQuerySelectorAttr) {55    element.setAttribute('i-amphtml-binding', '');56  }57  if (insertInHead) {58    env.win.document.head.appendChild(element);59  } else if (container) {60    container.appendChild(element);61  }62  return element;63}64/**65 * @param {!Object} env66 * @param {?Element} container67 * @param {string} id68 * @param {!Promise} valuePromise69 */70function addAmpState(env, container, id, fetchingPromise) {71  const ampState = env.win.document.createElement('amp-state');72  ampState.setAttribute('id', id);73  ampState.createdCallback = () => {};74  ampState.getImpl = () =>75    Promise.resolve({76      getFetchingPromise() {77        return fetchingPromise;78      },79      parseAndUpdate: () => {},80      element: ampState,81    });82  container.appendChild(ampState);83}84/**85 * @param {!Object} env86 * @param {!Bind} bind87 * @return {!Promise}88 */89function onBindReady(env, bind) {90  return bind.initializePromiseForTesting().then(() => {91    env.flushVsync();92  });93}94/**95 * @param {!Object} env96 * @param {!Bind} bind97 * @param {!Object} state98 * @param {boolean=} skipAmpState99 * @return {!Promise}100 */101function onBindReadyAndSetState(env, bind, state, skipAmpState) {102  return bind103    .initializePromiseForTesting()104    .then(() => {105      return bind.setState(state, {skipAmpState});106    })107    .then(() => {108      env.flushVsync();109      return bind.setStatePromiseForTesting();110    });111}112/**113 * @param {!Object} env114 * @param {!Bind} bind115 * @param {string} expression116 * @param {!Object} scope117 * @return {!Promise}118 */119function onBindReadyAndSetStateWithExpression(env, bind, expression, scope) {120  return bind121    .initializePromiseForTesting()122    .then(() => {123      return bind.setStateWithExpression(expression, scope);124    })125    .then(() => {126      env.flushVsync();127      return bind.setStatePromiseForTesting();128    });129}130/**131 * @param {!Object} env132 * @param {!Bind} bind133 * @param {!JsonObject} state134 * @return {!Promise}135 */136function onBindReadyAndSetStateWithObject(env, bind, state) {137  return bind138    .initializePromiseForTesting()139    .then(() => {140      return bind.setStateWithObject(state);141    })142    .then(() => {143      env.flushVsync();144      return bind.setStatePromiseForTesting();145    });146}147/**148 * @param {!Object} env149 * @param {!Bind} bind150 * @param {string} name151 * @return {!Promise}152 */153function onBindReadyAndGetState(env, bind, name) {154  return bind.initializePromiseForTesting().then(() => {155    env.flushVsync();156    return bind.getState(name);157  });158}159/**160 * @param {!Object} env161 * @param {!Bind} bind162 * @param {!Array<!Element>} added163 * @param {!Array<!Element>} removed164 * @param {!BindRescanOptions=} options165 * @return {!Promise}166 */167function onBindReadyAndRescan(env, bind, added, removed, options) {168  return bind169    .initializePromiseForTesting()170    .then(() => {171      return bind.rescan(added, removed, options);172    })173    .then(() => {174      env.flushVsync();175    });176}177/**178 * @param {!Object} env179 * @param {string} name180 * @return {!Promise}181 */182function waitForEvent(env, name) {183  return new Promise((resolve) => {184    const callback = () => {185      resolve();186      env.win.removeEventListener(name, callback);187    };188    env.win.addEventListener(name, callback);189  });190}191const FORM_VALUE_CHANGE_EVENT_ARGUMENTS = {192  type: AmpEvents.FORM_VALUE_CHANGE,193  bubbles: true,194};195const chromed = describes.sandboxed.configure().ifChrome();196chromed.run('Bind', {}, function () {197  describes.repeated(198    'Walker',199    {200      'using TreeWalker': {useQuerySelector: false},201      'using querySelectorAll': {useQuerySelector: true},202    },203    (name, variant) => {204      describe(name, function () {205        testSuite.call(this, variant.useQuerySelector);206      });207    }208  );209  function testSuite(useQuerySelector) {210    // Give more than default 2000ms timeout for local testing.211    const TIMEOUT = Math.max(window.ampTestRuntimeConfig.mochaTimeout, 4000);212    this.timeout(TIMEOUT);213    describes.realWin(214      'in FIE',215      {216        amp: {217          ampdoc: 'fie',218          runtimeOn: false,219        },220        mockFetch: false,221      },222      (env) => {223        let fieBind;224        let fieWindow, fieBody;225        let hostWindow, hostBody;226        beforeEach(() => {227          // Make sure we have a chunk instance for testing.228          chunkInstanceForTesting(env.ampdoc);229          if (useQuerySelector) {230            env.win.document.documentElement.setAttribute(231              'i-amphtml-binding',232              ''233            );234          }235          fieBind = new Bind(env.ampdoc);236          fieWindow = env.ampdoc.win;237          fieBody = env.ampdoc.getBody();238          hostWindow = env.parentWin;239          hostBody = env.parentAmpdoc.getBody();240        });241        it('should scan for bindings when ampdoc is ready', () => {242          createElement(env, fieBody, '[text]="1+1"', {243            insertQuerySelectorAttr: useQuerySelector,244          });245          expect(fieBind.numberOfBindings()).to.equal(0);246          return onBindReady(env, fieBind).then(() => {247            expect(fieBind.numberOfBindings()).to.equal(1);248          });249        });250        it('should not update host document title for <title> elements', () => {251          createElement(env, fieBody, '[text]="\'bar\'"', {252            tag: 'title',253            amp: false,254            insertInHead: true,255            insertQuerySelectorAttr: useQuerySelector,256          });257          fieWindow.document.title = 'foo';258          hostWindow.document.title = 'foo';259          return onBindReadyAndSetState(env, fieBind, {}).then(() => {260            // Make sure it does not update the host window's document title.261            expect(fieWindow.document.title).to.equal('bar');262            expect(hostWindow.document.title).to.equal('foo');263          });264        });265        describe('with Bind in host window', () => {266          let hostBind;267          beforeEach(() => {268            hostBind = new Bind(env.parentAmpdoc);269          });270          it('should only scan elements in provided window', () => {271            createElement(env, fieBody, '[text]="1+1"', {272              insertQuerySelectorAttr: useQuerySelector,273            });274            createElement(env, hostBody, '[text]="2+2"', {275              insertQuerySelectorAttr: useQuerySelector,276            });277            return Promise.all([278              onBindReady(env, fieBind),279              onBindReady(env, hostBind),280            ]).then(() => {281              expect(fieBind.numberOfBindings()).to.equal(1);282              expect(hostBind.numberOfBindings()).to.equal(1);283            });284          });285          it('should not be able to access variables from other windows', () => {286            const element = createElement(env, fieBody, '[text]="foo + bar"', {287              insertQuerySelectorAttr: useQuerySelector,288            });289            const parentElement = createElement(290              env,291              hostBody,292              '[text]="foo + bar"',293              {insertQuerySelectorAttr: useQuerySelector}294            );295            const promises = [296              onBindReadyAndSetState(env, fieBind, {foo: '123', bar: '456'}),297              onBindReadyAndSetState(env, hostBind, {foo: 'ABC', bar: 'DEF'}),298            ];299            return Promise.all(promises).then(() => {300              // `element` only sees `foo` and `parentElement` only sees `bar`.301              expect(element.textContent).to.equal('123456');302              expect(parentElement.textContent).to.equal('ABCDEF');303            });304          });305        });306      }307    );308    describes.realWin(309      'in shadow ampdoc',310      {311        amp: {312          ampdoc: 'shadow',313          runtimeOn: false,314        },315        mockFetch: false,316      },317      (env) => {318        let bind;319        let container;320        beforeEach(() => {321          // Make sure we have a chunk instance for testing.322          chunkInstanceForTesting(env.ampdoc);323          if (useQuerySelector) {324            env.win.document.documentElement.setAttribute(325              'i-amphtml-binding',326              ''327            );328          }329          bind = new Bind(env.ampdoc);330          container = env.ampdoc.getBody();331        });332        it('should scan for bindings when ampdoc is ready', () => {333          createElement(env, container, '[text]="1+1"', {334            insertQuerySelectorAttr: useQuerySelector,335          });336          expect(bind.numberOfBindings()).to.equal(0);337          return onBindReady(env, bind).then(() => {338            expect(bind.numberOfBindings()).to.equal(1);339          });340        });341        it('should not update document title for <title> elements', () => {342          createElement(env, container, '[text]="\'bar\'"', {343            tag: 'title',344            amp: false,345            insertInHead: true,346            insertQuerySelectorAttr: useQuerySelector,347          });348          env.win.document.title = 'foo';349          return onBindReadyAndSetState(env, bind, {}).then(() => {350            // Make sure does not update the host window's document title.351            expect(env.win.document.title).to.equal('foo');352          });353        });354      }355    );356    describes.realWin(357      'in single ampdoc',358      {359        amp: {360          ampdoc: 'single',361          runtimeOn: false,362        },363        mockFetch: false,364      },365      (env) => {366        let bind;367        let clock;368        let container;369        let history;370        let viewer;371        beforeEach(() => {372          const {ampdoc, win} = env;373          // Make sure we have a chunk instance for testing.374          chunkInstanceForTesting(ampdoc);375          viewer = Services.viewerForDoc(ampdoc);376          env.sandbox.stub(viewer, 'sendMessage');377          if (useQuerySelector) {378            env.win.document.documentElement.setAttribute(379              'i-amphtml-binding',380              ''381            );382          }383          bind = new Bind(ampdoc);384          // Connected <div> element created by describes.js.385          container = win.document.getElementById('parent');386          history = bind.historyForTesting();387          clock = fakeTimers.withGlobal(win).install({388            toFake: ['Date', 'setTimeout'],389          });390        });391        afterEach(() => {392          clock.uninstall();393        });394        it('should send "bindReady" to viewer on init', () => {395          expect(viewer.sendMessage).to.not.be.called;396          return onBindReady(env, bind).then(() => {397            expect(viewer.sendMessage).to.be.calledOnce;398            expect(viewer.sendMessage).to.be.calledWith('bindReady');399          });400        });401        it('should not send "bindReady" until all <amp-state> are built', () => {402          const element = createElement(env, container, '', {403            tag: 'amp-state',404            amp: true,405            insertQuerySelectorAttr: useQuerySelector,406          });407          // Makes whenUpgradedToCustomElement() resolve immediately.408          element.createdCallback = () => {};409          const parseAndUpdate = env.sandbox.spy();410          element.getImpl = () => {411            expect(viewer.sendMessage).to.not.be.called;412            return Promise.resolve({parseAndUpdate});413          };414          return onBindReady(env, bind).then(() => {415            expect(parseAndUpdate).to.be.calledOnce;416            expect(viewer.sendMessage).to.be.calledOnce;417            expect(viewer.sendMessage).to.be.calledWith('bindReady');418          });419        });420        it('should scan for bindings when ampdoc is ready', () => {421          createElement(env, container, '[text]="1+1"', {422            insertQuerySelectorAttr: useQuerySelector,423          });424          expect(bind.numberOfBindings()).to.equal(0);425          return onBindReady(env, bind).then(() => {426            expect(bind.numberOfBindings()).to.equal(1);427          });428        });429        it('should skip amp-list children during scan', () => {430          // <div>431          //   <amp-list [foo]="1+1">432          //     <h1 [text]="2+2"></h1>433          //   </amp-list>434          // </div>435          // <p>436          //   <span [text]="3+3"></span>437          // </p>438          const parent = document.createElement('div');439          container.appendChild(parent);440          const uncle = document.createElement('p');441          container.appendChild(uncle);442          const list = createElement(env, parent, `[class]="'x'"`, {443            tag: 'amp-list',444            insertQuerySelectorAttr: useQuerySelector,445          });446          const child = createElement(env, list, '[text]="2+2"', {447            tag: 'h1',448            insertQuerySelectorAttr: useQuerySelector,449          });450          const cousin = createElement(env, uncle, '[text]="3+3"', {451            tag: 'span',452            insertQuerySelectorAttr: useQuerySelector,453          });454          expect(bind.numberOfBindings()).to.equal(0);455          return onBindReadyAndSetState(env, bind, {}).then(() => {456            // Children of amp-list should be skipped.457            expect(child.textContent).to.equal('');458            // But cousins and the amp-list itself shouldn't be skipped.459            expect(list.className).to.equal('x');460            expect(cousin.textContent).to.equal('6');461          });462        });463        it('should not update attribute for non-primitive new values', () => {464          const list = createElement(env, container, `[src]="x"`, {465            tag: 'amp-list',466            insertQuerySelectorAttr: useQuerySelector,467          });468          list.setAttribute('src', 'https://foo.example/data.json');469          return onBindReadyAndSetState(env, bind, {470            x: [1, 2, 3],471          }).then(() => {472            expect(list.getAttribute('src')).to.equal(473              'https://foo.example/data.json'474            );475          });476        });477        it('should scan fixed layer for bindings', () => {478          // Mimic FixedLayer by creating a sibling <body> element.479          const doc = env.win.document;480          const pseudoFixedLayer = doc.body.cloneNode(false);481          doc.documentElement.appendChild(pseudoFixedLayer);482          // Make sure that the sibling <body> is scanned for bindings.483          createElement(env, pseudoFixedLayer, '[text]="1+1"', {484            insertQuerySelectorAttr: useQuerySelector,485          });486          return onBindReady(env, bind).then(() => {487            expect(bind.numberOfBindings()).to.equal(1);488          });489        });490        it('should support data-amp-bind-* syntax', () => {491          const element = createElement(492            env,493            container,494            'data-amp-bind-text="1+1"',495            {insertQuerySelectorAttr: useQuerySelector}496          );497          expect(bind.numberOfBindings()).to.equal(0);498          expect(element.textContent).to.equal('');499          return onBindReadyAndSetState(env, bind, {}).then(() => {500            expect(bind.numberOfBindings()).to.equal(1);501            expect(element.textContent).to.equal('2');502          });503        });504        it('should prefer [foo] over data-amp-bind-foo', () => {505          const element = createElement(506            env,507            container,508            '[text]="1+1" data-amp-bind-text="2+2"',509            {insertQuerySelectorAttr: useQuerySelector}510          );511          expect(bind.numberOfBindings()).to.equal(0);512          expect(element.textContent).to.equal('');513          return onBindReadyAndSetState(env, bind, {}).then(() => {514            expect(bind.numberOfBindings()).to.equal(1);515            expect(element.textContent).to.equal('2');516          });517        });518        if (!useQuerySelector) {519          it('should call createTreeWalker() with all params', () => {520            const spy = env.sandbox.spy(env.win.document, 'createTreeWalker');521            createElement(env, container, '[text]="1+1"', {522              insertQuerySelectorAttr: useQuerySelector,523            });524            return onBindReady(env, bind).then(() => {525              // createTreeWalker() on IE does not support optional arguments.526              expect(spy.callCount).to.equal(1);527              expect(spy.firstCall.args.length).to.equal(4);528            });529          });530        }531        it('should have same state after removing + re-adding a subtree', () => {532          for (let i = 0; i < 5; i++) {533            createElement(env, container, '[text]="1+1"', {534              insertQuerySelectorAttr: useQuerySelector,535            });536          }537          expect(bind.numberOfBindings()).to.equal(0);538          return onBindReady(env, bind)539            .then(() => {540              expect(bind.numberOfBindings()).to.equal(5);541              return bind.removeBindingsForNodes_([container]);542            })543            .then(() => {544              expect(bind.numberOfBindings()).to.equal(0);545              return bind.addBindingsForNodes_([container]);546            })547            .then(() => {548              expect(bind.numberOfBindings()).to.equal(5);549            });550        });551        it('should dynamically detect new bindings under dynamic tags', () => {552          const doc = env.win.document;553          const dynamicTag = doc.createElement('div');554          container.appendChild(dynamicTag);555          return onBindReady(env, bind)556            .then(() => {557              expect(bind.numberOfBindings()).to.equal(0);558              const element = createElement(env, container, '[text]="1+1"', {559                insertQuerySelectorAttr: useQuerySelector,560              });561              dynamicTag.appendChild(element);562              dynamicTag.dispatchEvent(563                new Event(AmpEvents.DOM_UPDATE, {bubbles: true})564              );565              return waitForEvent(env, BindEvents.RESCAN_TEMPLATE);566            })567            .then(() => {568              expect(bind.numberOfBindings()).to.equal(1);569            });570        });571        it('should NOT apply expressions on first load', () => {572          const element = createElement(env, container, '[text]="1+1"', {573            insertQuerySelectorAttr: useQuerySelector,574          });575          expect(element.textContent).to.equal('');576          return onBindReady(env, bind).then(() => {577            expect(element.textContent).to.equal('');578          });579        });580        it('should verify class bindings in dev mode', () => {581          window.__AMP_MODE = {582            development: true,583            test: true,584          };585          createElement(env, container, '[class]="\'foo\'" class="foo"', {586            insertQuerySelectorAttr: useQuerySelector,587          });588          createElement(env, container, '[class]="\'foo\'" class=" foo "', {589            insertQuerySelectorAttr: useQuerySelector,590          });591          createElement(env, container, '[class]="\'\'"', {592            insertQuerySelectorAttr: useQuerySelector,593          });594          createElement(env, container, '[class]="\'bar\'" class="qux"', {595            insertQuerySelectorAttr: useQuerySelector,596          });597          // Error598          const warnSpy = env.sandbox.spy(user(), 'warn');599          return onBindReady(env, bind).then(() => {600            expect(warnSpy).to.be.calledOnce;601            expect(warnSpy).calledWithMatch('amp-bind', /\[class\]/);602          });603        });604        it('should verify string attribute bindings in dev mode', () => {605          window.__AMP_MODE = {606            development: true,607            test: true,608          };609          // Only the initial value for [a] binding does not match.610          createElement(611            env,612            container,613            '[text]="\'a\'" [class]="\'b\'" class="b"',614            {insertQuerySelectorAttr: useQuerySelector}615          );616          const warnSpy = env.sandbox.spy(user(), 'warn');617          return onBindReady(env, bind).then(() => {618            expect(warnSpy).to.be.calledOnce;619            expect(warnSpy).calledWithMatch('amp-bind', /\[text\]/);620          });621        });622        it('should verify boolean attribute bindings in dev mode', () => {623          window.__AMP_MODE = {624            development: true,625            test: true,626          };627          createElement(env, container, '[disabled]="true" disabled', {628            tag: 'button',629            insertQuerySelectorAttr: useQuerySelector,630          });631          createElement(env, container, '[disabled]="false"', {632            tag: 'button',633            insertQuerySelectorAttr: useQuerySelector,634          });635          createElement(env, container, '[disabled]="true"', {636            tag: 'button',637            insertQuerySelectorAttr: useQuerySelector,638          });639          // Mismatch.640          const warnSpy = env.sandbox.spy(user(), 'warn');641          return onBindReady(env, bind).then(() => {642            expect(warnSpy).to.be.calledOnce;643            expect(warnSpy).calledWithMatch('amp-bind', /\[disabled\]/);644          });645        });646        it('should skip digest if specified in setState()', () => {647          const element = createElement(env, container, '[text]="1+1"', {648            insertQuerySelectorAttr: useQuerySelector,649          });650          expect(element.textContent).to.equal('');651          return onBindReady(env, bind).then(() => {652            bind.setState({}, /* opt_skipDigest */ true);653            env.flushVsync();654            expect(element.textContent).to.equal('');655          });656        });657        it('should support binding to string attributes', () => {658          const element = createElement(env, container, '[text]="1+1"', {659            insertQuerySelectorAttr: useQuerySelector,660          });661          expect(element.textContent).to.equal('');662          return onBindReadyAndSetState(env, bind, {}).then(() => {663            expect(element.textContent).to.equal('2');664          });665        });666        it('should support binding to boolean attributes', () => {667          const element = createElement(668            env,669            container,670            '[checked]="true" [disabled]="false" disabled',671            {672              tag: 'input',673              insertQuerySelectorAttr: useQuerySelector,674            }675          );676          expect(element.getAttribute('checked')).to.equal(null);677          expect(element.getAttribute('disabled')).to.equal('');678          return onBindReadyAndSetState(env, bind, {}).then(() => {679            expect(element.getAttribute('checked')).to.equal('');680            expect(element.getAttribute('disabled')).to.equal(null);681          });682        });683        it('should update values first, then attributes', () => {684          const spy = env.sandbox.spy();685          const element = createElement(env, container, '[value]="foo"', {686            tag: 'input',687            insertQuerySelectorAttr: useQuerySelector,688          });689          env.sandbox.stub(element, 'value').set(spy);690          env.sandbox.stub(element, 'setAttribute').callsFake(spy);691          return onBindReadyAndSetState(env, bind, {'foo': '2'}).then(() => {692            // Note: This tests a workaround for a browser bug. There is nothing693            // about the element itself we can verify. Only the order of operations694            // matters.695            expect(spy.firstCall).to.be.calledWithExactly('2');696            expect(spy.secondCall).to.be.calledWithExactly('value', '2');697          });698        });699        it('should update properties for empty strings', function* () {700          const element = createElement(env, container, '[value]="foo"', {701            tag: 'input',702            insertQuerySelectorAttr: useQuerySelector,703          });704          yield onBindReadyAndSetState(env, bind, {'foo': 'bar'});705          expect(element.value).to.equal('bar');706          yield onBindReadyAndSetState(env, bind, {'foo': ''});707          expect(element.value).to.equal('');708        });709        it('should support binding to Node.textContent', () => {710          const element = createElement(env, container, '[text]="\'abc\'"', {711            insertQuerySelectorAttr: useQuerySelector,712          });713          expect(element.textContent).to.equal('');714          return onBindReadyAndSetState(env, bind, {}).then(() => {715            expect(element.textContent).to.equal('abc');716          });717        });718        it('should set value for [text] in <textarea>', () => {719          const element = createElement(env, container, '[text]="\'abc\'"', {720            tag: 'textarea',721            insertQuerySelectorAttr: useQuerySelector,722          });723          expect(element.textContent).to.equal('');724          expect(element.value).to.equal('');725          return onBindReadyAndSetState(env, bind, {}).then(() => {726            expect(element.textContent).to.equal('');727            expect(element.value).to.equal('abc');728          });729        });730        it('should set textContent for [defaultText] in <textarea>', () => {731          const element = createElement(732            env,733            container,734            '[defaultText]="\'abc\'"',735            {736              tag: 'textarea',737              insertQuerySelectorAttr: useQuerySelector,738            }739          );740          expect(element.textContent).to.equal('');741          expect(element.value).to.equal('');742          // Setting `textContent` will also update `value` before interaction.743          element.value = '123';744          return onBindReadyAndSetState(env, bind, {}).then(() => {745            expect(element.textContent).to.equal('abc');746            expect(element.value).to.equal('123');747          });748        });749        it('should update document title for <title> elements', () => {750          const element = createElement(env, container, '[text]="\'bar\'"', {751            tag: 'title',752            amp: false,753            insertInHead: true,754            insertQuerySelectorAttr: useQuerySelector,755          });756          element.textContent = 'foo';757          env.win.document.title = 'foo';758          return onBindReadyAndSetState(env, bind, {}).then(() => {759            expect(element.textContent).to.equal('bar');760            expect(env.win.document.title).to.equal('bar');761          });762        });763        it('should not update document title for <title> elements in body', () => {764          // Add a <title> element to <head> because if we don't, setting765          // `textContent` on a <title> element in the <body> will strangely update766          // `document.title`.767          const title = env.win.document.createElement('title');768          title.textContent = 'foo';769          env.win.document.head.appendChild(title);770          // Add <title [text]="'bar'"> to <body>.771          createElement(env, container, '[text]="\'bar\'"', {772            tag: 'title',773            insertQuerySelectorAttr: useQuerySelector,774          });775          return onBindReadyAndSetState(env, bind, {}).then(() => {776            expect(env.win.document.title).to.equal('foo');777          });778        });779        it('should support binding to CSS classes with strings', () => {780          const element = createElement(env, container, '[class]="[\'abc\']"', {781            insertQuerySelectorAttr: useQuerySelector,782          });783          expect(toArray(element.classList)).to.deep.equal([]);784          return onBindReadyAndSetState(env, bind, {}).then(() => {785            expect(toArray(element.classList)).to.deep.equal(['abc']);786          });787        });788        it('should support binding to CSS classes with arrays', () => {789          const element = createElement(790            env,791            container,792            "[class]=\"['a','b']\"",793            {insertQuerySelectorAttr: useQuerySelector}794          );795          expect(toArray(element.classList)).to.deep.equal([]);796          return onBindReadyAndSetState(env, bind, {}).then(() => {797            expect(toArray(element.classList)).to.deep.equal(['a', 'b']);798          });799        });800        it('should support binding to CSS classes with a null value', () => {801          const element = createElement(env, container, '[class]="null"', {802            insertQuerySelectorAttr: useQuerySelector,803          });804          expect(toArray(element.classList)).to.deep.equal([]);805          return onBindReadyAndSetState(env, bind, {}).then(() => {806            expect(toArray(element.classList)).to.deep.equal([]);807          });808        });809        it('should support binding to CSS classes for svg tags', () => {810          const element = createElement(env, container, '[class]="[\'abc\']"', {811            tag: 'svg',812            insertQuerySelectorAttr: useQuerySelector,813          });814          expect(toArray(element.classList)).to.deep.equal([]);815          return onBindReadyAndSetState(env, bind, {}).then(() => {816            expect(toArray(element.classList)).to.deep.equal(['abc']);817          });818        });819        it('supports binding to CSS classes for svg tags with a null value', () => {820          const element = createElement(env, container, '[class]="null"', {821            tag: 'svg',822            insertQuerySelectorAttr: useQuerySelector,823          });824          expect(toArray(element.classList)).to.deep.equal([]);825          return onBindReadyAndSetState(env, bind, {}).then(() => {826            expect(toArray(element.classList)).to.deep.equal([]);827          });828        });829        it('should support handling actions with invoke()', () => {830          env.sandbox.stub(bind, 'setStateWithExpression');831          env.sandbox.stub(bind, 'pushStateWithExpression');832          const invocation = {833            method: 'setState',834            args: {835              [RAW_OBJECT_ARGS_KEY]: '{foo: bar}',836            },837            event: {838              detail: {bar: 123},839            },840            sequenceId: 0,841          };842          bind.invoke(invocation);843          expect(bind.setStateWithExpression).to.be.calledOnce;844          expect(bind.setStateWithExpression).to.be.calledWithExactly(845            '{foo: bar}',846            env.sandbox.match({event: {bar: 123}})847          );848          invocation.method = 'pushState';849          invocation.sequenceId++;850          bind.invoke(invocation);851          expect(bind.pushStateWithExpression).to.be.calledOnce;852          expect(bind.pushStateWithExpression).to.be.calledWithExactly(853            '{foo: bar}',854            env.sandbox.match({event: {bar: 123}})855          );856        });857        // TODO(choumx, #16721): Causes browser crash for some reason.858        it.skip('should only allow one action per event in invoke()', () => {859          env.sandbox.stub(bind, 'setStateWithExpression');860          const userError = env.sandbox.stub(user(), 'error');861          const invocation = {862            method: 'setState',863            args: {864              [RAW_OBJECT_ARGS_KEY]: '{foo: bar}',865            },866            event: {867              detail: {bar: 123},868            },869            sequenceId: 0,870          };871          bind.invoke(invocation);872          expect(bind.setStateWithExpression).to.be.calledOnce;873          expect(bind.setStateWithExpression).to.be.calledWithExactly(874            '{foo: bar}',875            env.sandbox.match({event: {bar: 123}})876          );877          // Second invocation with the same sequenceId should fail.878          bind.invoke(invocation);879          expect(bind.setStateWithExpression).to.be.calledOnce;880          expect(userError).to.be.calledWith(881            'amp-bind',882            'One state action allowed per event.'883          );884          // Invocation with the same sequenceid will be allowed after 5 seconds,885          // which is how long it takes for stored sequenceIds to be purged.886          clock.tick(5000);887          bind.invoke(invocation);888          expect(bind.setStateWithExpression).to.be.calledTwice;889        });890        it('should support parsing exprs in setStateWithExpression()', () => {891          const element = createElement(env, container, '[text]="onePlusOne"', {892            insertQuerySelectorAttr: useQuerySelector,893          });894          expect(element.textContent).to.equal('');895          const promise = onBindReadyAndSetStateWithExpression(896            env,897            bind,898            '{"onePlusOne": one + one}',899            {one: 1}900          );901          return promise.then(() => {902            expect(element.textContent).to.equal('2');903          });904        });905        describe('history', () => {906          beforeEach(() => {907            env.sandbox.spy(history, 'replace');908            env.sandbox.spy(history, 'push');909            env.sandbox.stub(viewer, 'isEmbedded').returns(true);910          });911          describe('with untrusted viewer', () => {912            it('should not replace history on AMP.setState()', () => {913              const promise = onBindReadyAndSetStateWithExpression(914                env,915                bind,916                '{"onePlusOne": one + one}',917                {one: 1}918              );919              return promise.then(() => {920                // Shouldn't call replace() with null `data`.921                expect(history.replace).to.not.be.called;922              });923            });924            it('should push history (no data) on AMP.pushState()', () => {925              const promise = bind.pushStateWithExpression(926                '{"foo": "bar"}',927                {}928              );929              return promise.then(() => {930                expect(history.push).to.be.called;931                // `data` param should be null on untrusted viewers.932                expect(history.push).to.be.calledWith(933                  env.sandbox.match.func,934                  null935                );936              });937            });938          });939          describe('with trusted viewer', () => {940            beforeEach(() => {941              env.sandbox942                .stub(viewer, 'isTrustedViewer')943                .returns(Promise.resolve(true));944            });945            it('should replace history on AMP.setState()', () => {946              const promise = onBindReadyAndSetStateWithExpression(947                env,948                bind,949                '{"onePlusOne": one + one}',950                {one: 1}951              );952              return promise.then(() => {953                expect(history.replace).calledOnce;954                // `data` param should exist on trusted viewers.955                expect(history.replace).calledWith({956                  data: {'amp-bind': {'onePlusOne': 2}},957                  title: '',958                });959              });960            });961            it('should push history on AMP.pushState()', () => {962              const promise = bind.pushStateWithExpression(963                '{"foo": "bar"}',964                {}965              );966              return promise.then(() => {967                expect(history.push).calledOnce;968                // `data` param should exist on trusted viewers.969                expect(history.push).calledWith(env.sandbox.match.func, {970                  data: {'amp-bind': {foo: 'bar'}},971                  title: '',972                });973              });974            });975          });976        });977        it('should support setting object state in setStateWithObject()', () => {978          const element = createElement(979            env,980            container,981            '[text]="mystate.mykey"',982            {insertQuerySelectorAttr: useQuerySelector}983          );984          expect(element.textContent).to.equal('');985          const promise = onBindReadyAndSetStateWithObject(env, bind, {986            mystate: {mykey: 'myval'},987          });988          return promise.then(() => {989            expect(element.textContent).to.equal('myval');990          });991        });992        it('should support getting state with getState()', () => {993          const promise = onBindReadyAndSetStateWithObject(env, bind, {994            mystate: {mykey: 'myval'},995          });996          return promise.then(() => {997            return onBindReadyAndGetState(env, bind, 'mystate.mykey').then(998              (result) => {999                expect(result).to.equal('myval');1000              }1001            );1002          });1003        });1004        describe('getStateAsync', () => {1005          it('should reject if there is no associated "amp-state"', async () => {1006            await onBindReadyAndSetState(env, bind, {1007              mystate: {mykey: 'myval'},1008            });1009            const state = bind.getStateAsync('mystate.mykey');1010            return expect(state).to.eventually.rejectedWith(/#mystate/);1011          });1012          it('should not wait if the still-loading state is irrelevant', async () => {1013            await onBindReadyAndSetState(env, bind, {1014              mystate: {myKey: 'myval'},1015            });1016            addAmpState(env, container, 'mystate', Promise.resolve());1017            addAmpState(1018              // never resolves1019              env,1020              container,1021              'irrelevant',1022              new Promise((unused) => {})1023            );1024            const state = await bind.getStateAsync('mystate.myKey');1025            expect(state).to.equal('myval');1026          });1027          it('should wait for a relevant key', async () => {1028            const {promise, resolve} = new Deferred();1029            addAmpState(env, container, 'mystate', promise);1030            await onBindReady(env, bind);1031            const statePromise = bind.getStateAsync('mystate.mykey');1032            await bind.setState({mystate: {mykey: 'myval'}}).then(resolve);1033            expect(await statePromise).to.equal('myval');1034          });1035          it('should stop waiting for a key if its fetch rejects', async () => {1036            const {promise, reject} = new Deferred();1037            addAmpState(env, container, 'mystate', promise);1038            await onBindReady(env, bind);1039            const statePromise = bind.getStateAsync('mystate.mykey');1040            reject();1041            expect(await statePromise).to.equal(undefined);1042          });1043        });1044        it('should support pushStateWithExpression()', () => {1045          env.sandbox.spy(history, 'push');1046          const element = createElement(env, container, '[text]="foo"', {1047            insertQuerySelectorAttr: useQuerySelector,1048          });1049          expect(element.textContent).to.equal('');1050          const promise = bind.pushStateWithExpression('{"foo": "bar"}', {});1051          return promise1052            .then(() => {1053              env.flushVsync();1054              expect(element.textContent).to.equal('bar');1055              expect(history.push).calledOnce;1056              // Pop callback should restore `foo` to original value (null).1057              const onPopCallback = history.push.firstCall.args[0];1058              return onPopCallback();1059            })1060            .then(() => {1061              expect(element.textContent).to.equal('null');1062            });1063        });1064        it('pushStateWithExpression() should work with nested objects', () => {1065          env.sandbox.spy(history, 'push');1066          const element = createElement(env, container, '[text]="foo.bar"', {1067            insertQuerySelectorAttr: useQuerySelector,1068          });1069          expect(element.textContent).to.equal('');1070          return bind1071            .pushStateWithExpression('{foo: {bar: 0}}', {})1072            .then(() => {1073              env.flushVsync();1074              expect(element.textContent).to.equal('0');1075              return bind.pushStateWithExpression('{foo: {bar: 1}}', {});1076            })1077            .then(() => {1078              env.flushVsync();1079              expect(element.textContent).to.equal('1');1080              expect(history.push).calledTwice;1081              // Pop callback should restore `foo.bar` to second pushed value (0).1082              const onPopCallback = history.push.secondCall.args[0];1083              return onPopCallback();1084            })1085            .then(() => {1086              expect(element.textContent).to.equal('0');1087            });1088        });1089        it('should ignore <amp-state> updates if specified in setState()', () => {1090          const element = createElement(env, container, '[src]="foo"', {1091            tag: 'amp-state',1092            insertQuerySelectorAttr: useQuerySelector,1093          });1094          // Makes whenUpgradedToCustomElement() resolve immediately.1095          element.createdCallback = () => {};1096          element.getImpl = () =>1097            Promise.resolve({parseAndUpdate: env.sandbox.spy()});1098          expect(element.getAttribute('src')).to.be.null;1099          const promise = onBindReadyAndSetState(1100            env,1101            bind,1102            {foo: '/foo'},1103            /* opt_isAmpStateMutation */ true1104          );1105          return promise.then(() => {1106            // Should _not_ be updated if `opt_isAmpStateMutation` is true.1107            expect(element.getAttribute('src')).to.be.null;1108          });1109        });1110        it('should support NOT override internal AMP CSS classes', () => {1111          const element = createElement(env, container, '[class]="[\'abc\']"', {1112            amp: true,1113            insertQuerySelectorAttr: useQuerySelector,1114          });1115          expect(toArray(element.classList)).to.deep.equal([1116            'i-amphtml-foo',1117            '-amp-foo',1118            'amp-foo',1119          ]);1120          return onBindReadyAndSetState(env, bind, {}).then(() => {1121            expect(toArray(element.classList)).to.deep.equal([1122              'i-amphtml-foo',1123              '-amp-foo',1124              'amp-foo',1125              'abc',1126            ]);1127          });1128        });1129        it('should call mutatedAttributesCallback on AMP elements', () => {1130          const binding =1131            '[text]="1+1" [value]="\'4\'" value="4" ' +1132            'checked [checked]="false" [disabled]="true" [multiple]="false"';1133          const element = createElement(env, container, binding, {1134            tag: 'input',1135            amp: true,1136            insertQuerySelectorAttr: useQuerySelector,1137          });1138          const spy = env.sandbox.spy(element, 'mutatedAttributesCallback');1139          return onBindReadyAndSetState(env, bind, {}).then(() => {1140            expect(spy).calledWithMatch({1141              checked: false,1142              disabled: true,1143            });1144            // Callback shouldn't include global attributes (text, class) or those1145            // whose values haven't changed.1146            expect(1147              spy.neverCalledWithMatch({1148                text: 2,1149                value: 4,1150                multiple: false,1151              })1152            ).to.be.true;1153          });1154        });1155        it('should support scope variable references', () => {1156          const binding = '[text]="foo + bar + baz.qux.join(\',\')"';1157          const element = createElement(env, container, binding, {1158            insertQuerySelectorAttr: useQuerySelector,1159          });1160          expect(element.textContent).to.equal('');1161          return onBindReadyAndSetState(env, bind, {1162            foo: 'abc',1163            bar: 123,1164            baz: {1165              qux: ['x', 'y', 'z'],1166            },1167          }).then(() => {1168            expect(element.textContent).to.equal('abc123x,y,z');1169          });1170        });1171        it('should NOT mutate elements if expression result is unchanged', () => {1172          const binding =1173            '[value]="foo" [class]="\'abc\'" [text]="\'a\'+\'b\'"';1174          const element = createElement(env, container, binding, {1175            tag: 'input',1176            insertQuerySelectorAttr: useQuerySelector,1177          });1178          element.mutatedAttributesCallback = env.sandbox.spy();1179          return onBindReadyAndSetState(env, bind, {foo: {bar: [1]}})1180            .then(() => {1181              expect(element.textContent.length).to.not.equal(0);1182              expect(element.classList.length).to.not.equal(0);1183              expect(element.attributes.length).to.not.equal(0);1184              expect(element.mutatedAttributesCallback).to.be.calledOnce;1185              element.textContent = '';1186              element.className = '';1187              while (element.attributes.length > 0) {1188                element.removeAttribute(element.attributes[0].name);1189              }1190              return onBindReadyAndSetState(env, bind, {});1191            })1192            .then(() => {1193              // Attributes should not be updated and mutatedAttributesCallback1194              // should not be called since the expression results haven't changed.1195              expect(element.textContent).to.equal('');1196              expect(element.className).to.equal('');1197              expect(element.attributes.length).to.equal(0);1198              expect(element.mutatedAttributesCallback).to.be.calledOnce;1199            });1200        });1201        it('should NOT evaluate expression if binding is NOT allowed', () => {1202          const element = createElement(1203            env,1204            container,1205            '[invalidBinding]="1+1"',1206            {insertQuerySelectorAttr: useQuerySelector}1207          );1208          return onBindReadyAndSetState(env, bind, {}).then(() => {1209            expect(element.getAttribute('invalidbinding')).to.be.null;1210          });1211        });1212        it('should rewrite attribute values regardless of result type', () => {1213          const withString = createElement(env, container, '[href]="foo"', {1214            tag: 'a',1215            insertQuerySelectorAttr: useQuerySelector,1216          });1217          const withArray = createElement(env, container, '[href]="bar"', {1218            tag: 'a',1219            insertQuerySelectorAttr: useQuerySelector,1220          });1221          return onBindReadyAndSetState(env, bind, {1222            foo: '?__amp_source_origin',1223            bar: ['?__amp_source_origin'],1224          }).then(() => {1225            expect(withString.getAttribute('href')).to.equal(null);1226            expect(withArray.getAttribute('href')).to.equal(null);1227          });1228        });1229        it('should stop scanning once max number of bindings is reached', () => {1230          bind.setMaxNumberOfBindingsForTesting(2);1231          const errorStub = env.sandbox.stub(dev(), 'expectedError');1232          const foo = createElement(env, container, '[text]="foo"', {1233            insertQuerySelectorAttr: useQuerySelector,1234          });1235          const bar = createElement(1236            env,1237            container,1238            '[text]="bar" [class]="baz"',1239            {insertQuerySelectorAttr: useQuerySelector}1240          );1241          const qux = createElement(env, container, '[text]="qux"', {1242            insertQuerySelectorAttr: useQuerySelector,1243          });1244          return onBindReadyAndSetState(env, bind, {1245            foo: 1,1246            bar: 2,1247            baz: 3,1248            qux: 4,1249          }).then(() => {1250            expect(foo.textContent).to.equal('1');1251            expect(bar.textContent).to.equal('2');1252            // Max number of bindings exceeded with [baz].1253            expect(bar.className).to.be.equal('');1254            expect(qux.textContent).to.be.equal('');1255            expect(errorStub).to.have.been.calledWith(1256              'amp-bind',1257              env.sandbox.match(/Maximum number of bindings reached/)1258            );1259          });1260        });1261        it('should update premutate keys that are overridable', () => {1262          bind.addOverridableKey('foo');1263          bind.addOverridableKey('bar');1264          const foo = createElement(env, container, '[text]="foo"', {1265            insertQuerySelectorAttr: useQuerySelector,1266          });1267          const bar = createElement(env, container, '[text]="bar"', {1268            insertQuerySelectorAttr: useQuerySelector,1269          });1270          const baz = createElement(env, container, '[text]="baz"', {1271            insertQuerySelectorAttr: useQuerySelector,1272          });1273          const qux = createElement(env, container, '[text]="qux"', {1274            insertQuerySelectorAttr: useQuerySelector,1275          });1276          return onBindReadyAndSetState(env, bind, {1277            foo: 1,1278            bar: 2,1279            baz: 3,1280            qux: 4,1281          }).then(() => {1282            return viewer1283              .receiveMessage('premutate', {1284                state: {1285                  foo: 'foo',1286                  bar: 'bar',1287                  baz: 'baz',1288                  qux: 'qux',1289                },1290              })1291              .then(() => {1292                expect(foo.textContent).to.equal('foo');1293                expect(bar.textContent).to.equal('bar');1294                expect(baz.textContent).to.be.equal('3');1295                expect(qux.textContent).to.be.equal('4');1296              });1297          });1298        });1299        describe('rescan()', () => {1300          let toRemove;1301          let toAdd;1302          beforeEach(async () => {1303            toRemove = createElement(env, container, '[text]="foo"', {1304              insertQuerySelectorAttr: useQuerySelector,1305            });1306            // New elements to rescan() don't need to be attached to DOM.1307            toAdd = createElement(env, /* container */ null, '[text]="1+1"', {1308              insertQuerySelectorAttr: useQuerySelector,1309            });1310          });1311          it('{update: true, fast: true}', async () => {1312            const options = {update: true, fast: true};1313            await onBindReadyAndSetState(env, bind, {foo: 'foo'});1314            expect(toRemove.textContent).to.equal('foo');1315            // [i-amphtml-binding] necessary in {fast: true}.1316            toAdd.setAttribute('i-amphtml-binding', '');1317            // `toAdd` should be scanned and updated.1318            await onBindReadyAndRescan(env, bind, [toAdd], [toRemove], options);1319            expect(toAdd.textContent).to.equal('2');1320            await onBindReadyAndSetState(env, bind, {foo: 'bar'});1321            // The `toRemove` element's bindings should have been removed.1322            expect(toRemove.textContent).to.not.equal('bar');1323          });1324          it('{update: true, fast: false}', async () => {1325            const options = {update: true, fast: false};1326            await onBindReadyAndSetState(env, bind, {foo: 'foo'});1327            expect(toRemove.textContent).to.equal('foo');1328            // `toAdd` should be scanned and updated.1329            await onBindReadyAndRescan(env, bind, [toAdd], [toRemove], options);1330            expect(toAdd.textContent).to.equal('2');1331            await onBindReadyAndSetState(env, bind, {foo: 'bar'});1332            // The `toRemove` element's bindings should have been removed.1333            expect(toRemove.textContent).to.not.equal('bar');1334          });1335          it('{update: false, fast: true}', async () => {1336            const options = {update: false, fast: true};1337            await onBindReadyAndSetState(env, bind, {foo: 'foo'});1338            expect(toRemove.textContent).to.equal('foo');1339            // [i-amphtml-binding] necessary in {fast: true}.1340            toAdd.setAttribute('i-amphtml-binding', '');1341            // `toAdd` should be scanned but not updated.1342            await onBindReadyAndRescan(env, bind, [toAdd], [toRemove], options);1343            expect(toAdd.textContent).to.equal('');1344            await onBindReadyAndSetState(env, bind, {foo: 'bar'});1345            // Now that `toAdd` is scanned, it should be updated on setState().1346            expect(toAdd.textContent).to.equal('2');1347            // The `toRemove` element's bindings should have been removed.1348            expect(toRemove.textContent).to.not.equal('bar');1349          });1350          it('{update: false, fast: false}', async () => {1351            const options = {update: false, fast: false};1352            await onBindReadyAndSetState(env, bind, {foo: 'foo'});1353            expect(toRemove.textContent).to.equal('foo');1354            // `toAdd` should be scanned but not updated.1355            await onBindReadyAndRescan(env, bind, [toAdd], [toRemove], options);1356            expect(toAdd.textContent).to.equal('');1357            await onBindReadyAndSetState(env, bind, {foo: 'bar'});1358            // Now that `toAdd` is scanned, it should be updated on setState().1359            expect(toAdd.textContent).to.equal('2');1360            // The `toRemove` element's bindings should have been removed.1361            expect(toRemove.textContent).to.not.equal('bar');1362          });1363          it('{update: "evaluate"}', async () => {1364            const options = {update: 'evaluate', fast: false};1365            toAdd = createElement(env, /* container */ null, '[text]="x"', {1366              insertQuerySelectorAttr: useQuerySelector,1367            });1368            // `toRemove` is updated normally before removal.1369            await onBindReadyAndSetState(env, bind, {foo: 'foo', x: '1'});1370            expect(toRemove.textContent).to.equal('foo');1371            // `toAdd` should be scanned but not updated. With {update: 'evaluate'},1372            // its expression "x" is now cached on the element.1373            await onBindReadyAndRescan(env, bind, [toAdd], [toRemove], options);1374            expect(toAdd.textContent).to.equal('');1375            await onBindReadyAndSetState(env, bind, {foo: 'bar', x: '1'});1376            // `toAdd` should _not_ update since the value of its expression "x"1377            // hasn't changed (due to caching).1378            expect(toAdd.textContent).to.equal('');1379            // `toRemove`'s bindings have been removed and remains unchanged.1380            expect(toRemove.textContent).to.equal('foo');1381            await onBindReadyAndSetState(env, bind, {x: '2'});1382            // toAdd changes now that its expression "x"'s value has changed.1383            expect(toAdd.textContent).to.equal('2');1384          });1385        });1386        describe('AmpEvents.FORM_VALUE_CHANGE', () => {1387          it('should dispatch FORM_VALUE_CHANGE on <input [value]> changes', () => {1388            const element = createElement(env, container, '[value]="foo"', {1389              tag: 'input',1390              insertQuerySelectorAttr: useQuerySelector,1391            });1392            const spy = env.sandbox.spy(element, 'dispatchEvent');1393            return onBindReadyAndSetState(env, bind, {foo: 'bar'}).then(() => {1394              expect(spy).to.have.been.calledOnce;1395              expect(spy).calledWithMatch(FORM_VALUE_CHANGE_EVENT_ARGUMENTS);1396            });1397          });1398          it('should dispatch FORM_VALUE_CHANGE on <input [checked]> changes', () => {1399            const element = createElement(env, container, '[checked]="foo"', {1400              tag: 'input',1401              insertQuerySelectorAttr: useQuerySelector,1402            });1403            const spy = env.sandbox.spy(element, 'dispatchEvent');1404            return onBindReadyAndSetState(env, bind, {foo: 'checked'}).then(1405              () => {1406                expect(spy).to.have.been.calledOnce;1407                expect(spy).calledWithMatch(FORM_VALUE_CHANGE_EVENT_ARGUMENTS);1408              }1409            );1410          });1411          it('should dispatch FORM_VALUE_CHANGE at parent <select> on <option [selected]> changes', () => {1412            const select = env.win.document.createElement('select');1413            const optgroup = createElement(env, select, '', {1414              tag: 'optgroup',1415            });1416            createElement(env, optgroup, '[selected]="foo"', {1417              tag: 'option',1418              insertQuerySelectorAttr: useQuerySelector,1419            });1420            container.appendChild(select);1421            const spy = env.sandbox.spy(select, 'dispatchEvent');1422            return onBindReadyAndSetState(env, bind, {foo: 'selected'}).then(1423              () => {1424                expect(spy).to.have.been.calledOnce;1425                expect(spy).calledWithMatch({1426                  type: AmpEvents.FORM_VALUE_CHANGE,1427                  bubbles: true,1428                });1429              }1430            );1431          });1432          it('should dispatch FORM_VALUE_CHANGE on <textarea [text]> changes', () => {1433            const element = createElement(env, container, '[text]="foo"', {1434              tag: 'textarea',1435              insertQuerySelectorAttr: useQuerySelector,1436            });1437            const spy = env.sandbox.spy(element, 'dispatchEvent');1438            return onBindReadyAndSetState(env, bind, {foo: 'bar'}).then(() => {1439              expect(spy).to.have.been.calledOnce;1440              expect(spy).calledWithMatch({1441                type: AmpEvents.FORM_VALUE_CHANGE,1442                bubbles: true,1443              });1444            });1445          });1446          it('should NOT dispatch FORM_VALUE_CHANGE on other attributes changes', () => {1447            const element = createElement(env, container, '[name]="foo"', {1448              tag: 'input',1449              insertQuerySelectorAttr: useQuerySelector,1450            });1451            const spy = env.sandbox.spy(element, 'dispatchEvent');1452            return onBindReadyAndSetState(env, bind, {foo: 'name'}).then(() => {1453              expect(spy).to.not.have.been.called;1454            });1455          });1456          it('should NOT dispatch FORM_VALUE_CHANGE on other element changes', () => {1457            const element = createElement(env, container, '[text]="foo"', {1458              tag: 'p',1459              insertQuerySelectorAttr: useQuerySelector,1460            });1461            const spy = env.sandbox.spy(element, 'dispatchEvent');1462            return onBindReadyAndSetState(env, bind, {foo: 'selected'}).then(1463              () => {1464                expect(spy).to.not.have.been.called;1465              }1466            );1467          });1468        });1469      }1470    );1471  }...Using AI Code Generation
1var tracetest = require('tracetest');2var query = tracetest.useQuerySelector('div');3console.log(query);4exports.useQuerySelector = function(selector) {5  return document.querySelector(selector);6};7{8  "scripts": {9  },10}11{ [Function: useQuerySelector] [Symbol(nodejs.util.promisify.custom)]: [Function] }Using AI Code Generation
1var trace = require('trace');2var tracetest = new trace.TraceTest();3tracetest.useQuerySelector();4var trace = require('trace');5var TraceTest = function() {};6TraceTest.prototype.useQuerySelector = function() {7    console.log("useQuerySelector");8};9exports.TraceTest = TraceTest;10var trace = require('trace');11var tracetest = new trace.TraceTest();12tracetest.useQuerySelector();13var trace = require('trace');14var TraceTest = function() {};15TraceTest.prototype.useQuerySelector = function() {16    console.log("useQuerySelector");17};18exports.TraceTest = TraceTest;19var trace = require('trace');20var tracetest = new trace.TraceTest();21tracetest.useQuerySelector();22var trace = require('trace');23var TraceTest = function() {};24TraceTest.prototype.useQuerySelector = function() {25    console.log("useQuerySelector");26};27exports.TraceTest = TraceTest;28var trace = require('trace');29var tracetest = new trace.TraceTest();30tracetest.useQuerySelector();31var trace = require('trace');32var TraceTest = function() {};33TraceTest.prototype.useQuerySelector = function() {34    console.log("useQuerySelector");35};Using AI Code Generation
1var tracetest = require('tracetest');2var trace = tracetest.trace;3var useQuerySelector = tracetest.useQuerySelector;4var traceResults = trace(useQuerySelector);5console.log(traceResults);6exports.useQuerySelector = function useQuerySelector() {7  var body = document.querySelector('body');8  var h1 = document.querySelector('h1');9  var p = document.querySelector('p');10  var a = document.querySelector('a');11  var img = document.querySelector('img');12  var span = document.querySelector('span');13  var input = document.querySelector('input');14  var button = document.querySelector('button');15  var div = document.querySelector('div');16}17exports.trace = function trace(fn) {18  var traceResults = [];19  var oldCall = Function.prototype.call;20  Function.prototype.call = function() {21    var functionName = this.name;22    var args = Array.prototype.slice.call(arguments);23    var element = args[0];24    var selector = args[1];25    var traceInfo = {26    };27    traceResults.push(traceInfo);28    return oldCall.apply(this, arguments);29  };30  fn();31  Function.prototype.call = oldCall;32  return traceResults;33}34[ { functionName: 'querySelector',35    selector: 'body' },36  { functionName: 'querySelector',37    selector: 'h1' },38  { functionName: 'querySelector',39    selector: 'p' },40  { functionName: 'querySelector',41    selector: 'a' },42  { functionName: 'querySelector',43    selector: 'img' },44  { functionName: 'querySelector',45    selector: 'span' },46  { functionName: 'querySelector',47    selector: 'input' },48  { functionName: 'querySelector',49    selector: 'button' },50  { functionName: 'querySelector',Using AI Code Generation
1var tracetest = require('tracetest');2tracetest.useQuerySelector('mySelector', 'myQuery');3var tracetest = require('tracetest');4tracetest.useQuerySelector('mySelector', 'myQuery');5var tracetest = require('tracetest');6tracetest.useQuerySelector('mySelector', 'myQuery');7var tracetest = require('tracetest');8tracetest.useQuerySelector('mySelector', 'myQuery');9var tracetest = require('tracetest');10tracetest.useQuerySelector('mySelector', 'myQuery');11var tracetest = require('tracetest');12tracetest.useQuerySelector('mySelector', 'myQuery');13var tracetest = require('tracetest');14tracetest.useQuerySelector('mySelector', 'myQuery');15var tracetest = require('tracetest');16tracetest.useQuerySelector('mySelector', 'myQuery');17var tracetest = require('tracetest');18tracetest.useQuerySelector('mySelector', 'myQuery');19var tracetest = require('tracetest');20tracetest.useQuerySelector('mySelector', 'myQuery');21var tracetest = require('tracetest');22tracetest.useQuerySelector('mySelector', 'myQuery');23var tracetest = require('tracetest');Using AI Code Generation
1const { useQuerySelector } = require('./tracetest.js');2const query = useQuerySelector('div');3console.log(query);4const useQuerySelector = (selector) => {5  const query = document.querySelector(selector);6  return query;7};8module.exports = { useQuerySelector };9const { useQuerySelector } = require('./tracetest.js');10const query = useQuerySelector('div');Using AI Code Generation
1var trace = require('trace');2var tracetest = new trace();3tracetest.useQuerySelector('div');4var trace = function() {5    this.useQuerySelector = function(selector) {6        return document.querySelector(selector);7    }8}9module.exports = trace;Using AI Code Generation
1var element = tracetest.useQuerySelector("#myElement");2var element2 = tracetest.useGetElementById("myElement");3var element3 = tracetest.useGetElementByTagName("myElement");4var element4 = tracetest.useGetElementByName("myElement");5var element5 = tracetest.useGetElementByClassName("myElement");6var element6 = tracetest.useGetElementsByTagName("myElement");7var element7 = tracetest.useGetElementsByClassName("myElement");8var element8 = tracetest.useGetElementsByName("myElement");9var element9 = tracetest.useGetElementsBySelector("#myElement");10var element = tracetest.useQuerySelector("#myElement");11var element2 = tracetest.useGetElementById("myElement");Using AI Code Generation
1var tracetest = require('tracetest');2var query = "SELECT * FROM traces WHERE id = 1";3var result = tracetest.useQuerySelector(query);4result;5var trace = require('trace');6var traceDB = trace.openDatabase('trace.db');7var useQuerySelector = function(query) {8  var result = traceDB.querySelector(query);9  return result;10};11exports.useQuerySelector = useQuerySelector;12var trace = {};13trace.openDatabase = function(dbName) {14  var traceDB = {};15  traceDB.querySelector = function(query) {16    var result = "SELECT * FROM traces WHERE id = 1";17    return result;18  };19  return traceDB;20};21module.exports = trace;22var tracetest = require('tracetest');23var query = "SELECT * FROM traces WHERE id = 1";24var result = tracetest.useQuerySelector(query);25result;26var trace = require('trace');27var traceDB = trace.openDatabase('trace.db');28var useQuerySelector = function(query) {29  var result = traceDB.querySelector(query);30  return result;31};32exports.useQuerySelector = useQuerySelector;33var trace = {};34trace.openDatabase = function(dbName) {35  var traceDB = {};36  traceDB.querySelector = function(query) {37    var result = "SELECT * FROM traces WHERE id = 1";38    return result;39  };40  return traceDB;41};42module.exports = trace;Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!
