| 'use strict'; |
| |
| // Test that objects created by the TextEncoderStream and TextDecoderStream APIs |
| // are created in the correct realm. The tests work by creating an iframe for |
| // each realm and then posting Javascript to them to be evaluated. Inputs and |
| // outputs are passed around via global variables in each realm's scope. |
| |
| // Async setup is required before creating any tests, so require done() to be |
| // called. |
| setup({explicit_done: true}); |
| |
| function createRealm() { |
| let iframe = document.createElement('iframe'); |
| const scriptEndTag = '<' + '/script>'; |
| iframe.srcdoc = `<!doctype html> |
| <script> |
| onmessage = event => { |
| if (event.source !== window.parent) { |
| throw new Error('unexpected message with source ' + event.source); |
| } |
| eval(event.data); |
| }; |
| ${scriptEndTag}`; |
| iframe.style.display = 'none'; |
| document.body.appendChild(iframe); |
| let realmPromiseResolve; |
| const realmPromise = new Promise(resolve => { |
| realmPromiseResolve = resolve; |
| }); |
| iframe.onload = () => { |
| realmPromiseResolve(iframe.contentWindow); |
| }; |
| return realmPromise; |
| } |
| |
| async function createRealms() { |
| // All realms are visible on the global object so they can access each other. |
| |
| // The realm that the constructor function comes from. |
| window.constructorRealm = await createRealm(); |
| |
| // The realm in which the constructor object is called. |
| window.constructedRealm = await createRealm(); |
| |
| // The realm in which reading happens. |
| window.readRealm = await createRealm(); |
| |
| // The realm in which writing happens. |
| window.writeRealm = await createRealm(); |
| |
| // The realm that provides the definitions of Readable and Writable methods. |
| window.methodRealm = await createRealm(); |
| |
| await evalInRealmAndWait(methodRealm, ` |
| window.ReadableStreamDefaultReader = |
| new ReadableStream().getReader().constructor; |
| window.WritableStreamDefaultWriter = |
| new WritableStream().getWriter().constructor; |
| `); |
| window.readMethod = methodRealm.ReadableStreamDefaultReader.prototype.read; |
| window.writeMethod = methodRealm.WritableStreamDefaultWriter.prototype.write; |
| } |
| |
| // In order for values to be visible between realms, they need to be |
| // global. To prevent interference between tests, variable names are generated |
| // automatically. |
| const id = (() => { |
| let nextId = 0; |
| return () => { |
| return `realmsId${nextId++}`; |
| }; |
| })(); |
| |
| // Eval string "code" in the content of realm "realm". Evaluation happens |
| // asynchronously, meaning it hasn't happened when the function returns. |
| function evalInRealm(realm, code) { |
| realm.postMessage(code, window.origin); |
| } |
| |
| // Same as evalInRealm() but returns a Promise which will resolve when the |
| // function has actually. |
| async function evalInRealmAndWait(realm, code) { |
| const resolve = id(); |
| const waitOn = new Promise(r => { |
| realm[resolve] = r; |
| }); |
| evalInRealm(realm, code); |
| evalInRealm(realm, `${resolve}();`); |
| await waitOn; |
| } |
| |
| // The same as evalInRealmAndWait but returns the result of evaluating "code" as |
| // an expression. |
| async function evalInRealmAndReturn(realm, code) { |
| const myId = id(); |
| await evalInRealmAndWait(realm, `window.${myId} = ${code};`); |
| return realm[myId]; |
| } |
| |
| // Constructs an object in constructedRealm and copies it into readRealm and |
| // writeRealm. Returns the id that can be used to access the object in those |
| // realms. |what| can contain constructor arguments. |
| async function constructAndStore(what) { |
| const objId = id(); |
| // Call |constructorRealm|'s constructor from inside |constructedRealm|. |
| writeRealm[objId] = await evalInRealmAndReturn( |
| constructedRealm, `new parent.constructorRealm.${what}`); |
| readRealm[objId] = writeRealm[objId]; |
| return objId; |
| } |
| |
| // Calls read() on the readable side of the TransformStream stored in |
| // readRealm[objId]. Locks the readable side as a side-effect. |
| function readInReadRealm(objId) { |
| return evalInRealmAndReturn(readRealm, ` |
| parent.readMethod.call(window.${objId}.readable.getReader())`); |
| } |
| |
| // Calls write() on the writable side of the TransformStream stored in |
| // writeRealm[objId], passing |value|. Locks the writable side as a |
| // side-effect. |
| function writeInWriteRealm(objId, value) { |
| const valueId = id(); |
| writeRealm[valueId] = value; |
| return evalInRealmAndReturn(writeRealm, ` |
| parent.writeMethod.call(window.${objId}.writable.getWriter(), |
| window.${valueId})`); |
| } |
| |
| window.onload = () => { |
| createRealms().then(() => { |
| runGenericTests('TextEncoderStream'); |
| runTextEncoderStreamTests(); |
| runGenericTests('TextDecoderStream'); |
| runTextDecoderStreamTests(); |
| done(); |
| }); |
| }; |
| |
| function runGenericTests(classname) { |
| promise_test(async () => { |
| const obj = await evalInRealmAndReturn( |
| constructedRealm, `new parent.constructorRealm.${classname}()`); |
| assert_equals(obj.constructor, constructorRealm[classname], |
| 'obj should be in constructor realm'); |
| }, `a ${classname} object should be associated with the realm the ` + |
| 'constructor came from'); |
| |
| promise_test(async () => { |
| const objId = await constructAndStore(classname); |
| const readableGetterId = id(); |
| readRealm[readableGetterId] = Object.getOwnPropertyDescriptor( |
| methodRealm[classname].prototype, 'readable').get; |
| const writableGetterId = id(); |
| writeRealm[writableGetterId] = Object.getOwnPropertyDescriptor( |
| methodRealm[classname].prototype, 'writable').get; |
| const readable = await evalInRealmAndReturn( |
| readRealm, `${readableGetterId}.call(${objId})`); |
| const writable = await evalInRealmAndReturn( |
| writeRealm, `${writableGetterId}.call(${objId})`); |
| assert_equals(readable.constructor, constructorRealm.ReadableStream, |
| 'readable should be in constructor realm'); |
| assert_equals(writable.constructor, constructorRealm.WritableStream, |
| 'writable should be in constructor realm'); |
| }, `${classname}'s readable and writable attributes should come from the ` + |
| 'same realm as the constructor definition'); |
| } |
| |
| function runTextEncoderStreamTests() { |
| promise_test(async () => { |
| const objId = await constructAndStore('TextEncoderStream'); |
| const writePromise = writeInWriteRealm(objId, 'A'); |
| const result = await readInReadRealm(objId); |
| await writePromise; |
| assert_equals(result.constructor, constructorRealm.Object, |
| 'result should be in constructor realm'); |
| assert_equals(result.value.constructor, constructorRealm.Uint8Array, |
| 'chunk should be in constructor realm'); |
| }, 'the output chunks when read is called after write should come from the ' + |
| 'same realm as the constructor of TextEncoderStream'); |
| |
| promise_test(async () => { |
| const objId = await constructAndStore('TextEncoderStream'); |
| const chunkPromise = readInReadRealm(objId); |
| writeInWriteRealm(objId, 'A'); |
| // Now the read() should resolve. |
| const result = await chunkPromise; |
| assert_equals(result.constructor, constructorRealm.Object, |
| 'result should be in constructor realm'); |
| assert_equals(result.value.constructor, constructorRealm.Uint8Array, |
| 'chunk should be in constructor realm'); |
| }, 'the output chunks when write is called with a pending read should come ' + |
| 'from the same realm as the constructor of TextEncoderStream'); |
| |
| // There is not absolute consensus regarding what realm exceptions should be |
| // created in. Implementations may vary. The expectations in exception-related |
| // tests may change in future once consensus is reached. |
| promise_test(async t => { |
| const objId = await constructAndStore('TextEncoderStream'); |
| // Read first to relieve backpressure. |
| const readPromise = readInReadRealm(objId); |
| |
| await promise_rejects_js(t, constructorRealm.TypeError, |
| writeInWriteRealm(objId, { |
| toString() { return {}; } |
| }), |
| 'write TypeError should come from constructor realm'); |
| |
| return promise_rejects_js(t, constructorRealm.TypeError, readPromise, |
| 'read TypeError should come from constructor realm'); |
| }, 'TypeError for unconvertable chunk should come from constructor realm ' + |
| 'of TextEncoderStream'); |
| } |
| |
| function runTextDecoderStreamTests() { |
| promise_test(async () => { |
| const objId = await constructAndStore('TextDecoderStream'); |
| const writePromise = writeInWriteRealm(objId, new Uint8Array([65])); |
| const result = await readInReadRealm(objId); |
| await writePromise; |
| assert_equals(result.constructor, constructorRealm.Object, |
| 'result should be in constructor realm'); |
| // A string is not an object, so doesn't have an associated realm. Accessing |
| // string properties will create a transient object wrapper belonging to the |
| // current realm. So checking the realm of result.value is not useful. |
| }, 'the result object when read is called after write should come from the ' + |
| 'same realm as the constructor of TextDecoderStream'); |
| |
| promise_test(async () => { |
| const objId = await constructAndStore('TextDecoderStream'); |
| const chunkPromise = readInReadRealm(objId); |
| writeInWriteRealm(objId, new Uint8Array([65])); |
| // Now the read() should resolve. |
| const result = await chunkPromise; |
| assert_equals(result.constructor, constructorRealm.Object, |
| 'result should be in constructor realm'); |
| // A string is not an object, so doesn't have an associated realm. Accessing |
| // string properties will create a transient object wrapper belonging to the |
| // current realm. So checking the realm of result.value is not useful. |
| }, 'the result object when write is called with a pending ' + |
| 'read should come from the same realm as the constructor of TextDecoderStream'); |
| |
| promise_test(async t => { |
| const objId = await constructAndStore('TextDecoderStream'); |
| // Read first to relieve backpressure. |
| const readPromise = readInReadRealm(objId); |
| await promise_rejects_js( |
| t, constructorRealm.TypeError, |
| writeInWriteRealm(objId, {}), |
| 'write TypeError should come from constructor realm' |
| ); |
| |
| return promise_rejects_js( |
| t, constructorRealm.TypeError, readPromise, |
| 'read TypeError should come from constructor realm' |
| ); |
| }, 'TypeError for chunk with the wrong type should come from constructor ' + |
| 'realm of TextDecoderStream'); |
| |
| promise_test(async t => { |
| const objId = |
| await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`); |
| // Read first to relieve backpressure. |
| const readPromise = readInReadRealm(objId); |
| |
| await promise_rejects_js( |
| t, constructorRealm.TypeError, |
| writeInWriteRealm(objId, new Uint8Array([0xff])), |
| 'write TypeError should come from constructor realm' |
| ); |
| |
| return promise_rejects_js( |
| t, constructorRealm.TypeError, readPromise, |
| 'read TypeError should come from constructor realm' |
| ); |
| }, 'TypeError for invalid chunk should come from constructor realm ' + |
| 'of TextDecoderStream'); |
| |
| promise_test(async t => { |
| const objId = |
| await constructAndStore(`TextDecoderStream('utf-8', {fatal: true})`); |
| // Read first to relieve backpressure. |
| readInReadRealm(objId); |
| // Write an unfinished sequence of bytes. |
| const incompleteBytesId = id(); |
| writeRealm[incompleteBytesId] = new Uint8Array([0xf0]); |
| |
| return promise_rejects_js( |
| t, constructorRealm.TypeError, |
| // Can't use writeInWriteRealm() here because it doesn't make it possible |
| // to reuse the writer. |
| evalInRealmAndReturn(writeRealm, ` |
| (() => { |
| const writer = window.${objId}.writable.getWriter(); |
| parent.writeMethod.call(writer, window.${incompleteBytesId}); |
| return parent.methodRealm.WritableStreamDefaultWriter.prototype |
| .close.call(writer); |
| })(); |
| `), |
| 'close TypeError should come from constructor realm' |
| ); |
| }, 'TypeError for incomplete input should come from constructor realm ' + |
| 'of TextDecoderStream'); |
| } |