<!DOCTYPE html>
<!-- Copyright © 2017 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). -->
<meta charset="utf-8">
<title>Test for PaymentRequest Constructor</title>
<link rel="help" href="https://w3c.github.io/browser-payment-api/#constructor">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";
const testMethod = Object.freeze({
  supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
});
const defaultMethods = Object.freeze([testMethod]);
const defaultAmount = Object.freeze({
  currency: "USD",
  value: "1.0",
});
const defaultNumberAmount = Object.freeze({
  currency: "USD",
  value: 1.0,
});
const defaultTotal = Object.freeze({
  label: "Default Total",
  amount: defaultAmount,
});
const defaultNumberTotal = Object.freeze({
  label: "Default Number Total",
  amount: defaultNumberAmount,
});
const defaultDetails = Object.freeze({
  total: defaultTotal,
  displayItems: [
    {
      label: "Default Display Item",
      amount: defaultAmount,
    },
  ],
});
const defaultNumberDetails = Object.freeze({
  total: defaultNumberTotal,
  displayItems: [
    {
      label: "Default Display Item",
      amount: defaultNumberAmount,
    },
  ],
});

// Avoid false positives, this should always pass
function smokeTest() {
  new PaymentRequest(defaultMethods, defaultDetails);
  new PaymentRequest(defaultMethods, defaultNumberDetails);
}
test(() => {
  smokeTest();
  const request = new PaymentRequest(defaultMethods, defaultDetails);
  assert_true(Boolean(request.id), "must be some truthy value");
}, "If details.id is missing, assign an identifier");

test(() => {
  smokeTest();
  const request1 = new PaymentRequest(defaultMethods, defaultDetails);
  const request2 = new PaymentRequest(defaultMethods, defaultDetails);
  assert_not_equals(request1.id, request2.id, "UA generated ID must be unique");
  const seen = new Set();
  // Let's try creating lots of requests, and make sure they are all unique
  for (let i = 0; i < 1024; i++) {
    const request = new PaymentRequest(defaultMethods, defaultDetails);
    assert_false(
      seen.has(request.id),
      `UA generated ID must be unique, but got duplicate! (${request.id})`
    );
    seen.add(request.id);
  }
}, "If details.id is missing, assign a unique identifier");

test(() => {
  smokeTest();
  const newDetails = Object.assign({}, defaultDetails, { id: "test123" });
  const request1 = new PaymentRequest(defaultMethods, newDetails);
  const request2 = new PaymentRequest(defaultMethods, newDetails);
  assert_equals(request1.id, newDetails.id, `id must be ${newDetails.id}`);
  assert_equals(request2.id, newDetails.id, `id must be ${newDetails.id}`);
  assert_equals(request1.id, request2.id, "ids need to be the same");
}, "If the same id is provided, then use it");

test(() => {
  smokeTest();
  const newDetails = Object.assign({}, defaultDetails, {
    id: "".padStart(1024, "a"),
  });
  const request = new PaymentRequest(defaultMethods, newDetails);
  assert_equals(
    request.id,
    newDetails.id,
    `id must be provided value, even if very long and contain spaces`
  );
}, "Use ids even if they are strange");

test(() => {
  smokeTest();
  const request = new PaymentRequest(
    defaultMethods,
    Object.assign({}, defaultDetails, { id: "foo" })
  );
  assert_equals(request.id, "foo");
}, "Use provided request ID");

test(() => {
  smokeTest();
  assert_throws_js(TypeError, () => new PaymentRequest([], defaultDetails));
}, "If the length of the methodData sequence is zero, then throw a TypeError");

test(() => {
  smokeTest();
  const duplicateMethods = [
    {
      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    },
    {
      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
    },
  ];
  assert_throws_js(RangeError, () => new PaymentRequest(duplicateMethods, defaultDetails));
}, "If payment method is duplicate, then throw a RangeError");

test(() => {
  smokeTest();
  const JSONSerializables = [[], { object: {} }];
  for (const data of JSONSerializables) {
    try {
      const methods = [
        {
          supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
          data,
        },
      ];
      new PaymentRequest(methods, defaultDetails);
    } catch (err) {
      assert_unreached(
        `Unexpected error parsing stringifiable JSON: ${JSON.stringify(
          data
        )}: ${err.message}`
      );
    }
  }
}, "Modifier method data must be JSON-serializable object");

test(() => {
  smokeTest();
  const recursiveDictionary = {};
  recursiveDictionary.foo = recursiveDictionary;
  assert_throws_js(TypeError, () => {
    const methods = [
      {
        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
        data: recursiveDictionary,
      },
    ];
    new PaymentRequest(methods, defaultDetails);
  });
  assert_throws_js(TypeError, () => {
    const methods = [
      {
        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
        data: "a string",
      },
    ];
    new PaymentRequest(methods, defaultDetails);
  });
  assert_throws_js(
    TypeError,
    () => {
      const methods = [
        {
          supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
          data: null,
        },
      ];
      new PaymentRequest(methods, defaultDetails);
    },
    "Even though null is JSON-serializable, it's not type 'Object' per ES spec"
  );
}, "Rethrow any exceptions of JSON-serializing paymentMethod.data into a string");

// process total
const invalidAmounts = [
  "-",
  "notdigits",
  "ALSONOTDIGITS",
  "10.",
  ".99",
  "-10.",
  "-.99",
  "10-",
  "1-0",
  "1.0.0",
  "1/3",
  "",
  null,
  " 1.0  ",
  " 1.0 ",
  "1.0 ",
  "USD$1.0",
  "$1.0",
  {
    toString() {
      return " 1.0";
    },
  },
];
const invalidTotalAmounts = invalidAmounts.concat([
  "-1",
  "-1.0",
  "-1.00",
  "-1000.000",
  -10,
]);
test(() => {
  smokeTest();
  for (const invalidAmount of invalidTotalAmounts) {
    const invalidDetails = {
      total: {
        label: "",
        amount: {
          currency: "USD",
          value: invalidAmount,
        },
      },
    };
    assert_throws_js(
      TypeError,
      () => {
        new PaymentRequest(defaultMethods, invalidDetails);
      },
      `Expect TypeError when details.total.amount.value is ${invalidAmount}`
    );
  }
}, `If details.total.amount.value is not a valid decimal monetary value, then throw a TypeError`);

test(() => {
  smokeTest();
  for (const prop in ["displayItems", "shippingOptions", "modifiers"]) {
    try {
      const details = Object.assign({}, defaultDetails, { [prop]: [] });
      new PaymentRequest(defaultMethods, details);
      assert_unreached(`PaymentDetailsBase.${prop} can be zero length`);
    } catch (err) {}
  }
}, `PaymentDetailsBase members can be 0 length`);

test(() => {
  smokeTest();
  assert_throws_js(TypeError, () => {
    new PaymentRequest(defaultMethods, {
      total: {
        label: "",
        amount: {
          currency: "USD",
          value: "-1.00",
        },
      },
    });
  });
}, "If the first character of details.total.amount.value is U+002D HYPHEN-MINUS, then throw a TypeError");

test(() => {
  smokeTest();
  for (const invalidAmount of invalidAmounts) {
    const invalidDetails = {
      total: defaultAmount,
      displayItems: [
        {
          label: "",
          amount: {
            currency: "USD",
            value: invalidAmount,
          },
        },
      ],
    };
    assert_throws_js(
      TypeError,
      () => {
        new PaymentRequest(defaultMethods, invalidDetails);
      },
      `Expected TypeError when item.amount.value is "${invalidAmount}"`
    );
  }
}, `For each item in details.displayItems: if item.amount.value is not a valid decimal monetary value, then throw a TypeError`);

test(() => {
  smokeTest();
  try {
    new PaymentRequest(
      [
        {
          supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
        },
      ],
      {
        total: defaultTotal,
        displayItems: [
          {
            label: "",
            amount: {
              currency: "USD",
              value: "-1000",
            },
          },
          {
            label: "",
            amount: {
              currency: "AUD",
              value: "-2000.00",
            },
          },
        ],
      }
    );
  } catch (err) {
    assert_unreached(
      `shouldn't throw when given a negative value: ${err.message}`
    );
  }
}, "Negative values are allowed for displayItems.amount.value, irrespective of total amount");

test(() => {
  smokeTest();
  const largeMoney = "1".repeat(510);
  try {
    new PaymentRequest(defaultMethods, {
      total: {
        label: "",
        amount: {
          currency: "USD",
          value: `${largeMoney}.${largeMoney}`,
        },
      },
      displayItems: [
        {
          label: "",
          amount: {
            currency: "USD",
            value: `-${largeMoney}`,
          },
        },
        {
          label: "",
          amount: {
            currency: "AUD",
            value: `-${largeMoney}.${largeMoney}`,
          },
        },
      ],
    });
  } catch (err) {
    assert_unreached(
      `shouldn't throw when given absurd monetary values: ${err.message}`
    );
  }
}, "it handles high precision currency values without throwing");

// Process shipping options:

const defaultShippingOption = Object.freeze({
  id: "default",
  label: "",
  amount: defaultAmount,
  selected: false,
});
const defaultShippingOptions = Object.freeze([
  Object.assign({}, defaultShippingOption),
]);

test(() => {
  smokeTest();
  for (const amount of invalidAmounts) {
    const invalidAmount = Object.assign({}, defaultAmount, {
      value: amount,
    });
    const invalidShippingOption = Object.assign({}, defaultShippingOption, {
      amount: invalidAmount,
    });
    const details = Object.assign({}, defaultDetails, {
      shippingOptions: [invalidShippingOption],
    });
    assert_throws_js(
      TypeError,
      () => {
        new PaymentRequest(defaultMethods, details, { requestShipping: true });
      },
      `Expected TypeError for option.amount.value: "${amount}"`
    );
  }
}, `For each option in details.shippingOptions: if option.amount.value is not a valid decimal monetary value, then throw a TypeError`);

test(() => {
  smokeTest();
  const shippingOptions = [defaultShippingOption];
  const details = Object.assign({}, defaultDetails, { shippingOptions });
  const request = new PaymentRequest(defaultMethods, details);
  assert_equals(
    request.shippingOption,
    null,
    "shippingOption must be null, as requestShipping is missing"
  );
  // defaultDetails lacks shipping options
  const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
    requestShipping: true,
  });
  assert_equals(
    request2.shippingOption,
    null,
    `request2.shippingOption must be null`
  );
}, "If there is no selected shipping option, then PaymentRequest.shippingOption remains null");

test(() => {
  smokeTest();
  const selectedOption = Object.assign({}, defaultShippingOption, {
    selected: true,
    id: "the-id",
  });
  const shippingOptions = [selectedOption];
  const details = Object.assign({}, defaultDetails, { shippingOptions });
  const requestNoShippingRequested1 = new PaymentRequest(
    defaultMethods,
    details
  );
  assert_equals(
    requestNoShippingRequested1.shippingOption,
    null,
    "Must be null when no shipping is requested (defaults to false)"
  );
  const requestNoShippingRequested2 = new PaymentRequest(
    defaultMethods,
    details,
    { requestShipping: false }
  );
  assert_equals(
    requestNoShippingRequested2.shippingOption,
    null,
    "Must be null when requestShipping is false"
  );
  const requestWithShipping = new PaymentRequest(defaultMethods, details, {
    requestShipping: "truthy value",
  });
  assert_equals(
    requestWithShipping.shippingOption,
    "the-id",
    "Selected option must be 'the-id'"
  );
}, "If there is a selected shipping option, and requestShipping is set, then that option becomes synchronously selected");

test(() => {
  smokeTest();
  const failOption1 = Object.assign({}, defaultShippingOption, {
    selected: true,
    id: "FAIL1",
  });
  const failOption2 = Object.assign({}, defaultShippingOption, {
    selected: false,
    id: "FAIL2",
  });
  const passOption = Object.assign({}, defaultShippingOption, {
    selected: true,
    id: "the-id",
  });
  const shippingOptions = [failOption1, failOption2, passOption];
  const details = Object.assign({}, defaultDetails, { shippingOptions });
  const requestNoShipping = new PaymentRequest(defaultMethods, details, {
    requestShipping: false,
  });
  assert_equals(
    requestNoShipping.shippingOption,
    null,
    "shippingOption must be null, as requestShipping is false"
  );
  const requestWithShipping = new PaymentRequest(defaultMethods, details, {
    requestShipping: true,
  });
  assert_equals(
    requestWithShipping.shippingOption,
    "the-id",
    "selected option must 'the-id"
  );
}, "If requestShipping is set, and if there is a multiple selected shipping options, only the last is selected.");

test(() => {
  smokeTest();
  const selectedOption = Object.assign({}, defaultShippingOption, {
    selected: true,
  });
  const unselectedOption = Object.assign({}, defaultShippingOption, {
    selected: false,
  });
  const shippingOptions = [selectedOption, unselectedOption];
  const details = Object.assign({}, defaultDetails, { shippingOptions });
  const requestNoShipping = new PaymentRequest(defaultMethods, details);
  assert_equals(
    requestNoShipping.shippingOption,
    null,
    "shippingOption must be null, because requestShipping is false"
  );
  assert_throws_js(
    TypeError,
    () => {
      new PaymentRequest(defaultMethods, details, { requestShipping: true });
    },
    "Expected to throw a TypeError because duplicate IDs"
  );
}, "If there are any duplicate shipping option ids, and shipping is requested, then throw a TypeError");

test(() => {
  smokeTest();
  const dupShipping1 = Object.assign({}, defaultShippingOption, {
    selected: true,
    id: "DUPLICATE",
    label: "Fail 1",
  });
  const dupShipping2 = Object.assign({}, defaultShippingOption, {
    selected: false,
    id: "DUPLICATE",
    label: "Fail 2",
  });
  const shippingOptions = [dupShipping1, defaultShippingOption, dupShipping2];
  const details = Object.assign({}, defaultDetails, { shippingOptions });
  const requestNoShipping = new PaymentRequest(defaultMethods, details);
  assert_equals(
    requestNoShipping.shippingOption,
    null,
    "shippingOption must be null, because requestShipping is false"
  );
  assert_throws_js(
    TypeError,
    () => {
      new PaymentRequest(defaultMethods, details, { requestShipping: true });
    },
    "Expected to throw a TypeError because duplicate IDs"
  );
}, "Throw when there are duplicate shippingOption ids, even if other values are different");

// Process payment details modifiers:
test(() => {
  smokeTest();
  for (const invalidTotal of invalidTotalAmounts) {
    const invalidModifier = {
      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
      total: {
        label: "",
        amount: {
          currency: "USD",
          value: invalidTotal,
        },
      },
    };
    assert_throws_js(
      TypeError,
      () => {
        new PaymentRequest(defaultMethods, {
          modifiers: [invalidModifier],
          total: defaultTotal,
        });
      },
      `Expected TypeError for modifier.total.amount.value: "${invalidTotal}"`
    );
  }
}, `Throw TypeError if modifier.total.amount.value is not a valid decimal monetary value`);

test(() => {
  smokeTest();
  for (const invalidAmount of invalidAmounts) {
    const invalidModifier = {
      supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
      total: defaultTotal,
      additionalDisplayItems: [
        {
          label: "",
          amount: {
            currency: "USD",
            value: invalidAmount,
          },
        },
      ],
    };
    assert_throws_js(
      TypeError,
      () => {
        new PaymentRequest(defaultMethods, {
          modifiers: [invalidModifier],
          total: defaultTotal,
        });
      },
      `Expected TypeError when given bogus modifier.additionalDisplayItems.amount of "${invalidModifier}"`
    );
  }
}, `If amount.value of additionalDisplayItems is not a valid decimal monetary value, then throw a TypeError`);

test(() => {
  smokeTest();
  const modifiedDetails = Object.assign({}, defaultDetails, {
    modifiers: [
      {
        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
        data: ["some-data"],
      },
    ],
  });
  try {
    new PaymentRequest(defaultMethods, modifiedDetails);
  } catch (err) {
    assert_unreached(
      `Unexpected exception thrown when given a list: ${err.message}`
    );
  }
}, "Modifier data must be JSON-serializable object (an Array in this case)");

test(() => {
  smokeTest();
  const modifiedDetails = Object.assign({}, defaultDetails, {
    modifiers: [
      {
        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
        data: {
          some: "data",
        },
      },
    ],
  });
  try {
    new PaymentRequest(defaultMethods, modifiedDetails);
  } catch (err) {
    assert_unreached(
      `shouldn't throw when given an object value: ${err.message}`
    );
  }
}, "Modifier data must be JSON-serializable object (an Object in this case)");

test(() => {
  smokeTest();
  const recursiveDictionary = {};
  recursiveDictionary.foo = recursiveDictionary;
  const modifiedDetails = Object.assign({}, defaultDetails, {
    modifiers: [
      {
        supportedMethods: "https://{{domains[nonexistent]}}/payment-request",
        data: recursiveDictionary,
      },
    ],
  });
  assert_throws_js(TypeError, () => {
    new PaymentRequest(defaultMethods, modifiedDetails);
  });
}, "Rethrow any exceptions of JSON-serializing modifier.data");

//Setting ShippingType attribute during construction
test(() => {
  smokeTest();
  assert_throws_js(TypeError, () => {
    new PaymentRequest(defaultMethods, defaultDetails, {
      shippingType: "invalid",
    });
  });
}, "Shipping type should be valid");

test(() => {
  smokeTest();
  const request = new PaymentRequest(defaultMethods, defaultDetails, {});
  assert_equals(request.shippingAddress, null, "must be null");
}, "PaymentRequest.shippingAddress must initially be null");

test(() => {
  smokeTest();
  const request1 = new PaymentRequest(defaultMethods, defaultDetails, {});
  assert_equals(request1.shippingType, null, "must be null");
  const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
    requestShipping: false,
  });
  assert_equals(request2.shippingType, null, "must be null");
}, "If options.requestShipping is not set, then request.shippingType attribute is null.");

test(() => {
  smokeTest();
  // option.shippingType defaults to 'shipping'
  const request1 = new PaymentRequest(defaultMethods, defaultDetails, {
    requestShipping: true,
  });
  assert_equals(request1.shippingType, "shipping", "must be shipping");
  const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
    requestShipping: true,
    shippingType: "delivery",
  });
  assert_equals(request2.shippingType, "delivery", "must be delivery");
}, "If options.requestShipping is true, request.shippingType will be options.shippingType.");

</script>
