/**
 * Copyright 2018 Google Inc. All rights reserved.
 * Modifications copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const utils = require('./utils');
const path = require('path');
const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS, CHANNEL} = testOptions;
describe('Page.evaluate', function() {
  it('should work', async({page, server}) => {
    const result = await page.evaluate(() => 7 * 3);
    expect(result).toBe(21);
  });
  it('should transfer NaN', async({page, server}) => {
    const result = await page.evaluate(a => a, NaN);
    expect(Object.is(result, NaN)).toBe(true);
  });
  it('should transfer -0', async({page, server}) => {
    const result = await page.evaluate(a => a, -0);
    expect(Object.is(result, -0)).toBe(true);
  });
  it('should transfer Infinity', async({page, server}) => {
    const result = await page.evaluate(a => a, Infinity);
    expect(Object.is(result, Infinity)).toBe(true);
  });
  it('should transfer -Infinity', async({page, server}) => {
    const result = await page.evaluate(a => a, -Infinity);
    expect(Object.is(result, -Infinity)).toBe(true);
  });
  it('should roundtrip unserializable values', async({page}) => {
    const value = {
      infinity: Infinity,
      nInfinity: -Infinity,
      nZero: -0,
      nan: NaN,
    };
    const result = await page.evaluate(value => value, value);
    expect(result).toEqual(value);
  });
  it('should roundtrip promise to value', async({page}) => {
    {
      const result = await page.evaluate(value => Promise.resolve(value), null);
      expect(result === null).toBeTruthy();
    }
    {
      const result = await page.evaluate(value => Promise.resolve(value), Infinity);
      expect(result === Infinity).toBeTruthy();
    }
    {
      const result = await page.evaluate(value => Promise.resolve(value), -0);
      expect(result === -0).toBeTruthy();
    }
    {
      const result = await page.evaluate(value => Promise.resolve(value), undefined);
      expect(result === undefined).toBeTruthy();
    }
  });
  it('should roundtrip promise to unserializable values', async({page}) => {
    const value = {
      infinity: Infinity,
      nInfinity: -Infinity,
      nZero: -0,
      nan: NaN,
    };
    const result = await page.evaluate(value => Promise.resolve(value), value);
    expect(result).toEqual(value);
  });
  it('should transfer arrays', async({page, server}) => {
    const result = await page.evaluate(a => a, [1, 2, 3]);
    expect(result).toEqual([1,2,3]);
  });
  it('should transfer arrays as arrays, not objects', async({page, server}) => {
    const result = await page.evaluate(a => Array.isArray(a), [1, 2, 3]);
    expect(result).toBe(true);
  });
  it('should transfer maps as empty objects', async({page, server}) => {
    const result = await page.evaluate(a => a.x.constructor.name + ' ' + JSON.stringify(a.x), {x: new Map([[1, 2]])});
    expect(result).toBe('Object {}');
  });
  it('should modify global environment', async({page}) => {
    await page.evaluate(() => window.globalVar = 123);
    expect(await page.evaluate('globalVar')).toBe(123);
  });
  it('should evaluate in the page context', async({page, server}) => {
    await page.goto(server.PREFIX + '/global-var.html');
    expect(await page.evaluate('globalVar')).toBe(123);
  });
  it('should return undefined for objects with symbols', async({page, server}) => {
    expect(await page.evaluate(() => [Symbol('foo4')])).toEqual([undefined]);
    expect(await page.evaluate(() => {
      const a = { };
      a[Symbol('foo4')] = 42;
      return a;
    })).toEqual({});
    expect(await page.evaluate(() => {
      return { foo: [{ a: Symbol('foo4') }] };
    })).toEqual({ foo: [ { a: undefined } ] });
  });
  it('should work with function shorthands', async({page, server}) => {
    const a = {
      sum([a, b]) { return a + b; },
      async mult([a, b]) { return a * b; }
    };
    expect(await page.evaluate(a.sum, [1, 2])).toBe(3);
    expect(await page.evaluate(a.mult, [2, 4])).toBe(8);
  });
  it('should work with unicode chars', async({page, server}) => {
    const result = await page.evaluate(a => a['中文字符'], {'中文字符': 42});
    expect(result).toBe(42);
  });
  it('should throw when evaluation triggers reload', async({page, server}) => {
    let error = null;
    await page.evaluate(() => {
      location.reload();
      return new Promise(() => {});
    }).catch(e => error = e);
    expect(error.message).toContain('navigation');
  });
  it('should await promise', async({page, server}) => {
    const result = await page.evaluate(() => Promise.resolve(8 * 7));
    expect(result).toBe(56);
  });
  it('should work right after framenavigated', async({page, server}) => {
    let frameEvaluation = null;
    page.on('framenavigated', async frame => {
      frameEvaluation = frame.evaluate(() => 6 * 7);
    });
    await page.goto(server.EMPTY_PAGE);
    expect(await frameEvaluation).toBe(42);
  });
  it('should work right after a cross-origin navigation', async({page, server}) => {
      await page.goto(server.EMPTY_PAGE);
      let frameEvaluation = null;
      page.on('framenavigated', async frame => {
        frameEvaluation = frame.evaluate(() => 6 * 7);
      });
      await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
      expect(await frameEvaluation).toBe(42);
  });
  it('should work from-inside an exposed function', async({page, server}) => {
    // Setup inpage callback, which calls Page.evaluate
    await page.exposeFunction('callController', async function(a, b) {
      return await page.evaluate(({ a, b }) => a * b, { a, b });
    });
    const result = await page.evaluate(async function() {
      return await callController(9, 3);
    });
    expect(result).toBe(27);
  });
  it('should reject promise with exception', async({page, server}) => {
    let error = null;
    await page.evaluate(() => not_existing_object.property).catch(e => error = e);
    expect(error).toBeTruthy();
    expect(error.message).toContain('not_existing_object');
  });
  it('should support thrown strings as error messages', async({page, server}) => {
    let error = null;
    await page.evaluate(() => { throw 'qwerty'; }).catch(e => error = e);
    expect(error).toBeTruthy();
    expect(error.message).toContain('qwerty');
  });
  it('should support thrown numbers as error messages', async({page, server}) => {
    let error = null;
    await page.evaluate(() => { throw 100500; }).catch(e => error = e);
    expect(error).toBeTruthy();
    expect(error.message).toContain('100500');
  });
  it('should return complex objects', async({page, server}) => {
    const object = {foo: 'bar!'};
    const result = await page.evaluate(a => a, object);
    expect(result).not.toBe(object);
    expect(result).toEqual(object);
  });
  it('should return NaN', async({page, server}) => {
    const result = await page.evaluate(() => NaN);
    expect(Object.is(result, NaN)).toBe(true);
  });
  it('should return -0', async({page, server}) => {
    const result = await page.evaluate(() => -0);
    expect(Object.is(result, -0)).toBe(true);
  });
  it('should return Infinity', async({page, server}) => {
    const result = await page.evaluate(() => Infinity);
    expect(Object.is(result, Infinity)).toBe(true);
  });
  it('should return -Infinity', async({page, server}) => {
    const result = await page.evaluate(() => -Infinity);
    expect(Object.is(result, -Infinity)).toBe(true);
  });
  it('should work with overwritten Promise', async({page, server}) => {
    await page.evaluate(() => {
      const originalPromise = window.Promise;
      class Promise2 {
        static all(...arg) {
          return wrap(originalPromise.all(...arg));
        }
        static race(...arg) {
          return wrap(originalPromise.race(...arg));
        }
        static resolve(...arg) {
          return wrap(originalPromise.resolve(...arg));
        }
        constructor(f, r) {
          this._promise = new originalPromise(f, r);
        }
        then(f, r) {
          return wrap(this._promise.then(f, r));
        }
        catch(f) {
          return wrap(this._promise.catch(f));
        }
        finally(f) {
          return wrap(this._promise.finally(f));
        }
      };
      const wrap = p => {
        const result = new Promise2(() => {}, () => {});
        result._promise = p;
        return result;
      };
      window.Promise = Promise2;
      window.__Promise2 = Promise2;
    });
    // Sanity check.
    expect(await page.evaluate(() => {
      const p = Promise.all([Promise.race([]), new Promise(() => {}).then(() => {})]);
      return p instanceof window.__Promise2;
    })).toBe(true);
    // Now, the new promise should be awaitable.
    expect(await page.evaluate(() => Promise.resolve(42))).toBe(42);
  });
  it('should throw when passed more than one parameter', async({page, server}) => {
    const expectThrow = async f => {
      let error;
      await f().catch(e => error = e);
      expect('' + error).toContain('Too many arguments');
    }
    await expectThrow(() => page.evaluate((a, b) => false, 1, 2));
    await expectThrow(() => page.evaluateHandle((a, b) => false, 1, 2));
    await expectThrow(() => page.$eval('sel', (a, b) => false, 1, 2));
    await expectThrow(() => page.$$eval('sel', (a, b) => false, 1, 2));
    await expectThrow(() => page.evaluate((a, b) => false, 1, 2));
    const frame = page.mainFrame();
    await expectThrow(() => frame.evaluate((a, b) => false, 1, 2));
    await expectThrow(() => frame.evaluateHandle((a, b) => false, 1, 2));
    await expectThrow(() => frame.$eval('sel', (a, b) => false, 1, 2));
    await expectThrow(() => frame.$$eval('sel', (a, b) => false, 1, 2));
    await expectThrow(() => frame.evaluate((a, b) => false, 1, 2));
  });
  it('should accept "undefined" as one of multiple parameters', async({page, server}) => {
    const result = await page.evaluate(({ a, b }) => Object.is(a, undefined) && Object.is(b, 'foo'), { a: undefined, b: 'foo' });
    expect(result).toBe(true);
  });
  it('should properly serialize undefined arguments', async({page}) => {
    expect(await page.evaluate(x => ({a: x}), undefined)).toEqual({});
  });
  it('should properly serialize undefined fields', async({page}) => {
    expect(await page.evaluate(() => ({a: undefined}))).toEqual({});
  });
  it('should return undefined properties', async({page}) => {
    const value = await page.evaluate(() => ({a: undefined}));
    expect('a' in value).toBe(true);
  });
  it('should properly serialize null arguments', async({page}) => {
    expect(await page.evaluate(x => x, null)).toEqual(null);
  });
  it('should properly serialize null fields', async({page}) => {
    expect(await page.evaluate(() => ({a: null}))).toEqual({a: null});
  });
  it('should return undefined for non-serializable objects', async({page, server}) => {
    expect(await page.evaluate(() => window)).toBe(undefined);
  });
  it('should fail for circular object', async({page, server}) => {
    const result = await page.evaluate(() => {
      const a = {};
      const b = {a};
      a.b = b;
      return a;
    });
    expect(result).toBe(undefined);
  });
  it('should be able to throw a tricky error', async({page, server}) => {
    const windowHandle = await page.evaluateHandle(() => window);
    const errorText = await windowHandle.jsonValue().catch(e => e.message);
    const error = await page.evaluate(errorText => {
      throw new Error(errorText);
    }, errorText).catch(e => e);
    expect(error.message).toContain(errorText);
  });
  it('should accept a string', async({page, server}) => {
    const result = await page.evaluate('1 + 2');
    expect(result).toBe(3);
  });
  it('should accept a string with semi colons', async({page, server}) => {
    const result = await page.evaluate('1 + 5;');
    expect(result).toBe(6);
  });
  it('should accept a string with comments', async({page, server}) => {
    const result = await page.evaluate('2 + 5;\n// do some math!');
    expect(result).toBe(7);
  });
  it('should accept element handle as an argument', async({page, server}) => {
    await page.setContent('