<html>
<head></head>
<body>
<div id="description"></div>
<input type="file" id="fileInput" multiple></input>
<div id="console"></div>
<script src="resources/postmessage-test.js"></script>
<script>
document.getElementById("description").innerHTML = "Tests that we clone object hierarchies";

globalThis.heapZero = 100000000000000000000000000000000000000000n - 100000000000000000000000000000000000000000n;
tryPostMessage('null');
tryPostMessage('undefined');
tryPostMessage('1');
tryPostMessage('true');
tryPostMessage('"1"');
tryPostMessage('0n');
tryPostMessage('globalThis.heapZero');
tryPostMessage('-20n');
tryPostMessage('4294967295n');
tryPostMessage('-4294967295n');
tryPostMessage('2147483647n');
tryPostMessage('2147483648n');
tryPostMessage('-2147483648n');
tryPostMessage('-2147483649n');
tryPostMessage('68719476735n');
tryPostMessage('-68719476735n');
tryPostMessage('1000000000000000000000000000000000n');
tryPostMessage('-1000000000000000000000000000000000n');
tryPostMessage('1461501637330902918203684832716283019655932542975n');
tryPostMessage('-1461501637330902918203684832716283019655932542975n');
tryPostMessage('4722366482869645213695n');
tryPostMessage('-4722366482869645213695n');
tryPostMessage('({})');
tryPostMessage('({a:1})');
tryPostMessage('({a:"a"})');
tryPostMessage('({b:"a", a:"b"})');
tryPostMessage('({p0:"string0", p1:"string1", p2:"string2", p3:"string3", p4:"string4", p5:"string5", p6:"string6", p7:"string7", p8:"string8", p9:"string9", p10:"string10", p11:"string11", p12:"string12", p13:"string13", p14:"string14", p15:"string15", p16:"string16", p17:"string17", p18:"string18", p19:"string19"})');
tryPostMessage('({p0:"string1", p1:"string1", p2:"string2", p3:"string3", p4:"string4", p5:"string5", p6:"string6", p7:"string7", p8:"string8", p9:"string9", p10:"string10", p11:"string11", p12:"string12", p13:"string13", p14:"string14", p15:"string15", p16:"string16", p17:"string17", p18:"string18", p19:"string19"})');
tryPostMessage('({a:""})');
tryPostMessage('({a:0})');
tryPostMessage('({a:1})');
tryPostMessage('[]');
tryPostMessage('["a", "a", "b", "a", "b"]');
tryPostMessage('["a", "a", "b", {a:"b", b:"a"}]');
tryPostMessage('[1,2,3]');
tryPostMessage('[,,1]');
tryPostMessage('(function(){})', true, null, DOMException.DATA_CLONE_ERR);
tryPostMessage('var x = 0; try { eval("badref"); } catch(e) { x = e; } x');
tryPostMessage('new Date(1234567890000)');
tryPostMessage('new ConstructorWithPrototype("foo")', false, '({field:"foo"})');
tryPostMessage('new Boolean(true)');
tryPostMessage('new Boolean(false)');
tryPostMessage('new String("gnirts")');
tryPostMessage('new Number(42.0)');
tryPostMessage('Object(0n)');
tryPostMessage('Object(-20n)');
tryPostMessage('Object(4294967295n)');
tryPostMessage('Object(-4294967295n)');
tryPostMessage('Object(2147483647n)');
tryPostMessage('Object(2147483648n)');
tryPostMessage('Object(-2147483648n)');
tryPostMessage('Object(-2147483649n)');
tryPostMessage('Object(68719476735n)');
tryPostMessage('Object(-68719476735n)');
tryPostMessage('Object(1000000000000000000000000000000000n)');
tryPostMessage('Object(-1000000000000000000000000000000000n)');
tryPostMessage('Object(1461501637330902918203684832716283019655932542975n)');
tryPostMessage('Object(-1461501637330902918203684832716283019655932542975n)');
tryPostMessage('Object(4722366482869645213695n)');
tryPostMessage('Object(-4722366482869645213695n)');
cyclicObject={};
cyclicObject.self = cyclicObject;
tryPostMessage('cyclicObject', false, "cyclicObject");
cyclicArray=[];
cyclicArray[0] = cyclicArray;
tryPostMessage('cyclicArray', false, "cyclicArray");
objectGraph = {};
object = {};
objectGraph.graph1 = object;
objectGraph.graph2 = object;
tryPostMessage('objectGraph', false, "objectGraph");
arrayGraph = [object, object];
tryPostMessage('arrayGraph', false, "arrayGraph");
tryPostMessage('window', true);
tryPostMessage('({get a() { throw "x" }})', true);

var map = new Map;
var set = new Set;
map.expando1 = {};
map.expando2 = {};
map.aSet = set;
map.set(1, 2.5)
map.set("entry", map.expando1);
map.set(true, set);
map.set(map.expando2, map);
set.add(false)
set.add(map)
tryPostMessage("map", false, "evalThunk", function (v) {
    newMap = v
    doPassFail(newMap.get("entry") === newMap.expando1, "String keyed entry was cloned correctly");
    doPassFail(newMap.get(newMap.expando2) === newMap, "Object key entry was cloned correctly");
    shouldBe("newMap.get(true)", "newMap.aSet")
    shouldBe("newMap.aSet.has(newMap)", "true")
    newMap.forEach(function (value, key) { 
        console.innerHTML += "LOG: " + value + " => " + key + "<br>"
    })
})

if (window.eventSender) {
    var fileInput = document.getElementById("fileInput");
    var fileRect = fileInput.getClientRects()[0];
    var targetX = fileRect.left + fileRect.width / 2;
    var targetY = fileRect.top + fileRect.height / 2;
    eventSender.beginDragWithFiles(['resources/file1.txt', 'resources/file2.txt']);
    eventSender.mouseMoveTo(targetX, targetY);
    eventSender.mouseUp();
}
var imageData = document.createElement("canvas").getContext("2d").createImageData(10,10);
for (var i = 0; i < imageData.data.length * 4; i++)
    imageData.data[i] = i % 256;
var mutatedImageData = document.createElement("canvas").getContext("2d").createImageData(10,10);
for (var i = 0; i < imageData.data.length * 4; i++)
    mutatedImageData.data[i] = i % 256;
tryPostMessage('imageData', false, imageData);
tryPostMessage('imageData.data', false, imageData.data)
tryPostMessage('mutatedImageData', false, imageData);
tryPostMessage('mutatedImageData.data', false, imageData.data)
for (var i = 0; i < imageData.data.length * 4; i++)
    mutatedImageData.data[i] = 0;

function thunk(s) {
    return "(function() {" + s + "})()";
}
tryPostMessage(thunk('return 42;'), false, '42');
tryPostMessage(thunk('return 42;'), false, thunk('return 40 + 2;'));
tryPostMessage(thunk('return 42;'), false, "evalThunk",
               function(v) { doPassFail(v == 42, "evalThunk OK"); })

// Only enumerable properties should be serialized.
tryPostMessage(thunk('var o = {x:"hello"}; Object.defineProperty(o, "y", {value:"goodbye"}); return o;'), false, '({x:"hello"})');

// It's unclear what we should do if an accessor modifies an object out from under us
// while we're serializing it; the standard does mandate certain aspects about evaluation
// order, though, including that properties must be processed in their enumeration order.
tryPostMessage(thunk(
        'var a = [0, 1, 2]; ' +
        'var b = { get x() { a[0] = 40; a[2] = 42; a.push(43); return 41; }}; ' +
        'a[1] = b; ' +
        'return a;'
    ), false, "evalThunk", function(v) {
        doPassFail(v.length === 3, "length correct"); // undefined
        doPassFail(v[0] === 0, "evaluation order OK"); // mandatory
        doPassFail(v[1].x === 41, "evaluation order OK/accessor reached"); // mandatory
        doPassFail(v[2] === 42, "evaluation order OK"); // mandatory
    });

tryPostMessage(thunk(
        'var a = [0, 1, 2]; ' +
        'var b = { get x() { a.pop(); return 41; } }; ' +
        'a[1] = b; ' +
        'return a;'
    ), false, "evalThunk", function(v) {
        doPassFail(v.length === 3 || v.length === 2, "length correct"); // undefined
        doPassFail(v[0] === 0, "index 0 OK"); // mandatory
        doPassFail(v[1].x === 41, "accessor reached"); // mandatory
        doPassFail(v[2] === undefined, "index 2 undefined"); // undefined
    });

tryPostMessage(thunk(
        'var a = [0, 1, 2]; ' +
        'var b = { get x() { a.pop(); return 41; } }; ' +
        'a[2] = b; ' +
        'return a;'
    ), false, "evalThunk", function(v) {
        doPassFail(v.length === 3, "length correct"); // undefined
        doPassFail(v[0] === 0, "index 0 OK"); // mandatory
        doPassFail(v[1] === 1, "index 1 OK"); // mandatory
        doPassFail(v[2].x === 41, "index 2 OK"); // undefined
    });

// Now with objects! This is a little more tricky because the standard does not
// define an enumeration order.
tryPostMessage(thunk(
        'var a = {p0: 0, p1: 1}; ' +
        'Object.defineProperty(a, "p2", {get:function() {delete a.p3; return 42; }, enumerable: true, configurable: true}); ' +
        'Object.defineProperty(a, "p3", {get:function() {delete a.p2; return 43; }, enumerable: true, configurable: true}); ' +
        'Object.defineProperty(a, "p4", {get:function() { a.p5 = 45; return 44; }, enumerable: true, configurable: true}); ' +
        'return a;'
    ), false, "evalThunk", function(v) {
        doPassFail(v.p0 === 0 && v.p1 === 1, "basic properties OK"); // mandatory
        doPassFail(v.p2 === undefined && v.p3 !== undefined ||
                   v.p2 !== undefined && v.p3 === undefined, "one accessor was run"); // mandatory
        doPassFail(v.p2 !== undefined || Object.getOwnPropertyDescriptor(v, "p2") === undefined, "property was removed"); // undefined
        doPassFail(v.p3 !== undefined || Object.getOwnPropertyDescriptor(v, "p3") === undefined, "property was removed"); // undefined
        doPassFail(v.p4 === 44, "accessor was run"); // mandatory
        doPassFail(Object.getOwnPropertyDescriptor(v, "p5") === undefined, "dynamic property not sent"); // undefined
    });

// Objects returned from accessors should still be coalesced.
tryPostMessage(thunk(
        'var obja = {get p() { return 42; }}; ' +
        'var msg = {get a() { return obja; }, b: obja}; ' +
        'return msg;'
    ), false, "evalThunk", function(v) {
        // Interestingly, the standard admits two answers here!
        doPassFail(v.a === v.b, "reference equality preserved");
        doPassFail((v.b.p === 42 && v.a.p === 42) ||
            (v.b.p === null && v.a.p === null), "accessors used");
    });
tryPostMessage(thunk(
        'var obja = {get p() { return 42; }}; ' +
        'var msg = {a: obja, get b() { return obja; }}; ' +
        'return msg;'
    ), false, "evalThunk", function(v) {
        // Interestingly, the standard admits two answers here!
        doPassFail(v.a === v.b, "reference equality preserved (opposite order)");
        doPassFail((v.b.p === 42 && v.a.p === 42) ||
                   (v.b.p === null && v.a.p === null), "accessors used (opposite order)");
    });
// We should nullify the results from accessors more than one level deep,
// but leave other fields untouched.
tryPostMessage(thunk(
        'var obja = {get p() { return 42; }, q: 43}; ' +
        'return {get a() { return obja; }};'
    ), false, "evalThunk", function(v) {
        doPassFail(v.a.p === null, "accessor value was nullified");
        doPassFail(v.a.q === 43, "non-accessor value was not nullified");
    });
tryPostMessage(thunk(
        'var objb = {get r() { return 44; }, t: 45}; ' +
        'var obja = {get p() { return 42; }, q: 43, s: objb}; ' +
        'return {get a() { return obja; }};'
    ), false, "evalThunk", function(v) {
        doPassFail(v.a.p === null, "accessor value was nullified");
        doPassFail(v.a.q === 43, "non-accessor value was not nullified");
        doPassFail(v.s !== null, "non-accessor value was not nullified");
        doPassFail(v.a.s.r === null, "accessor value was nullified");
        doPassFail(v.a.s.t === 45, "non-accessor value was not nullified");
    });
tryPostMessage(thunk(
        'var objb = {get r() { return 44; }, t: 45}; ' +
        'var obja = {get p() { return 42; }, q: 43, s: [objb]}; ' +
        'return {get c() { return 47; }, get a() { return obja; }, get b() { return 46; } };'
    ), false, "evalThunk", function(v) {
        doPassFail(v.b === 46, "accessor value was not nullified");
        doPassFail(v.c === 47, "accessor value was not nullified");
        doPassFail(v.a.p === null, "accessor value was nullified");
        doPassFail(v.a.q === 43, "non-accessor value was not nullified");
        doPassFail(v.a.s !== null, "non-accessor value was not nullified");
        doPassFail(v.a.s !== undefined, "non-accessor value is defined");
        doPassFail(v.a.s[0] !== null, "non-accessor value was not nullified");
        doPassFail(v.a.s[0] !== undefined, "non-accessor value is defined");
        doPassFail(v.a.s[0].r === null, "accessor value was nullified");
        doPassFail(v.a.s[0].t === 45, "non-accessor value was not nullified");
    });

// We need to pass out the exception raised from internal accessors.
tryPostMessage(thunk(
        'return {get a() { throw "accessor-exn"; }};'
    ), true, null, 'accessor-exn');
// We should still return the exception raised from nulled-out accessors.
tryPostMessage(thunk(
        'var obja = {get p() { throw "accessor-exn"; }}; ' +
        'return {get a() { return obja; }};'
    ), true, null, 'accessor-exn');
// We should run the first nullified accessor, but no more.
tryPostMessage(thunk(
        'window.bcalled = undefined; ' +
        'window.acalled = undefined; ' +
        'window.pcalled = undefined; ' +
        'var objb = {get b() { window.bcalled = true; return 42; }}; ' +
        'var obja = {get a() { window.acalled = true; return objb; }}; ' +
        'return { get p() { window.pcalled = true; return obja; }};'
    ), false, "evalThunk", function(v) {
        doPassFail(v.p.a === null, "accessor value was nullified");
        doPassFail(window.pcalled === true, "window.pcalled === true");
        doPassFail(window.acalled === true, "window.acalled === true");
        doPassFail(window.bcalled === undefined, "window.bcalled === undefined");
    });

// Reference equality between Boolean objects must be maintained.
tryPostMessage(thunk(
        'var t1 = new Boolean(true); ' +
        'var t2 = new Boolean(true); ' +
        'var f1 = new Boolean(false); ' +
        'var f2 = new Boolean(false); ' +
        'return [t1, t1, t2, f1, f1, f2];'
    ), false, "evalThunk", function(v) {
        doPassFail(equal(v[0], new Boolean(true)), "Boolean values correct (0)");
        doPassFail(equal(v[3], new Boolean(false)), "Boolean values correct (3)");
        doPassFail(equal(v[1], v[2]), "Boolean values correct (1,2)");
        doPassFail(equal(v[4], v[5]), "Boolean values correct (4,5)");
        doPassFail(v[0] === v[1], "References to Booleans correct (0,1)");
        doPassFail(v[3] === v[4], "References to Booleans correct (3,4)");
        doPassFail(v[0] !== v[2], "References to Booleans correct (0,2)");
        doPassFail(v[3] !== v[5], "References to Booleans correct (3,5)");
    });

// Reference equality between Number objects must be maintained.
tryPostMessage(thunk(
        'var n1 = new Number(42.0); ' +
        'var n2 = new Number(42.0); ' +
        'return [n1, n1, n2];'
    ), false, "evalThunk", function(v) {
        doPassFail(equal(v[0], new Number(42.0)), "Number values correct (0)");
        doPassFail(equal(v[0], v[2]), "Number values correct (0,2)");
        doPassFail(v[0] === v[1], "References to numbers correct (0,1)");
        doPassFail(v[0] !== v[2], "References to numbers correct (0,2)");
    });

// Reference equality between String objects must be maintained.
tryPostMessage(thunk(
        'var s1 = new String("gnirts"); ' +
        'var s2 = new String("gnirts"); ' +
        'return [s1, s1, s2];'
    ), false, "evalThunk", function(v) {
        doPassFail(equal(v[0], new String("gnirts")), "String values correct (0)");
        doPassFail(equal(v[0], v[2]), "String values correct (0,2)");
        doPassFail(v[0] === v[1], "References to strings correct (0,1)");
        doPassFail(v[0] !== v[2], "References to strings correct (0,2)");
    });

// Properties added to String, Boolean, Number, and BigInt objects should not be serialized.
tryPostMessage(thunk(
        'var s = new String("gnirts"); ' +
        'var n = new Number(42.0); ' +
        'var b = new Boolean(true); ' +
        'var i = Object(100000000000000000000n); ' +
        'var i32 = Object(10n); ' +
        's.foo = 1; n.foo = 2; b.foo = 3; i.foo = 4; i32.foo = 5; ' +
        'return [s, n, b, i, i32];'
    ), false, "evalThunk", function(v) {
        doPassFail(v[0].foo == undefined, "String object properties not serialized");
        doPassFail(v[1].foo == undefined, "Number object properties not serialized");
        doPassFail(v[2].foo == undefined, "Boolean object properties not serialized");
        doPassFail(v[3].foo == undefined, "BigInt object properties not serialized");
        doPassFail(v[4].foo == undefined, "BigInt (BigInt32) object properties not serialized");
    });

// Reference equality between dates must be maintained.
tryPostMessage(thunk(
        'var d1 = new Date(1,2,3); ' +
        'var d2 = new Date(1,2,3); ' +
        'return [d1,d1,d2];'
    ), false, "evalThunk", function(v) {
        doPassFail(equal(v[0], new Date(1,2,3)), "Date values correct (0)");
        doPassFail(equal(v[0], v[2]), "Date values correct (1)");
        doPassFail(v[0] === v[1], "References to dates correct (0)");
        doPassFail(v[0] !== v[2], "References to dates correct (1)");
    });

// Reference equality between regexps must be preserved.
tryPostMessage(thunk(
        'var rx1 = new RegExp("foo"); ' +
        'var rx2 = new RegExp("foo"); ' +
        'var rx3 = new RegExp("foo", "gim"); ' +
        'rx3.exec("foofoofoo"); ' +
        'doPassFail(rx3.lastIndex === 3, "lastIndex initially correct: was " + rx3.lastIndex); ' +
        'return [rx1,rx1,rx2,rx3];'
    ), false, "evalThunk", function(v) {
        doPassFail(v[0].source === "foo", "Regexp value correct (0)");
        doPassFail(v[0] === v[1], "References to regexps correct (0)");
        doPassFail(v[0] !== v[2], "References to regexps correct (1)");
        doPassFail(v[0].global === false, "global set (0)");
        doPassFail(v[3].global === true, "global set (1)");
        doPassFail(v[0].ignoreCase === false, "ignoreCase set (0)");
        doPassFail(v[3].ignoreCase === true, "ignoreCase set (1)");
        doPassFail(v[0].multiline === false, "multiline set (0)");
        doPassFail(v[3].multiline === true, "multiline set (1)");
        doPassFail(v[0].lastIndex === 0, "lastIndex correct (0)");
        doPassFail(v[3].lastIndex === 0, "lastIndex correct (1)");
    });

if (window.eventSender) {
    tryPostMessage(thunk(
        'window.fileList = fileInput.files; ' +
        'window.file0 = fileList[0]; ' +
        'window.file1 = fileList[1]; ' +
        'return window.fileList.length'), false, 2);
    doPassFail(window.fileList[0] === window.file0, "sanity on file reference equality")
    // The standard mandates that we do _not_ maintain reference equality for files in a transferred FileList.
    tryPostMessage(thunk('return [window.file0, window.file0];'
        ), false, "evalThunk", function(v) { doPassFail(v[0] === v[1], "file references transfer")});
    tryPostMessage(thunk('return [window.fileList, window.file0];'
        ), false, "evalThunk", function(v) { doPassFail(v[0][0] !== v[1], "FileList should not respect reference equality")});
    tryPostMessage(thunk('return [window.file0, window.fileList];'
        ), false, "evalThunk", function(v) { doPassFail(v[1][0] !== v[0], "FileList should not respect reference equality")});
    tryPostMessage(thunk('return [window.fileList, window.fileList];'
        ), false, "evalThunk", function(v) { doPassFail(v[0] === v[1], "FileList respects self-reference equality")});
    tryPostMessage(thunk('return [window.fileList, window.file0, window.file1]'
        ), false, "evalThunk", function(v) {
            doPassFail(v[0].length === window.fileList.length, "FileList length sent correctly");
            doPassFail(v[0][0] !== v[1], "FileList should not respect reference equality (0)");
            doPassFail(v[0][1] !== v[2], "FileList should not respect reference equality (1)");
            doPassFail(v[0][0].name == window.file0.name, "FileList preserves order and data (name0)");
            doPassFail(v[0][1].name == window.file1.name, "FileList preserves order and data (name1)");
            doPassFail(equal(v[0][0].lastModifiedDate, window.file0.lastModifiedDate), "FileList preserves order and data (date0)");
            doPassFail(equal(v[0][1].lastModifiedDate, window.file1.lastModifiedDate), "FileList preserves order and data (date1)");
        });
}
tryPostMessage('"done"');
</script>
</body>
</html>
