forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			444 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			444 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const { Cc, Ci, Cu: ChromeUtils } = SpecialPowers;
 | |
| 
 | |
| /**
 | |
|  * Converts a property bag to object.
 | |
|  * @param {nsIPropertyBag} bag - The property bag to convert
 | |
|  * @returns {Object} - The object representation of the nsIPropertyBag
 | |
|  */
 | |
| function propBagToObject(bag) {
 | |
|   if (!(bag instanceof Ci.nsIPropertyBag)) {
 | |
|     throw new TypeError("Not a property bag");
 | |
|   }
 | |
|   let result = {};
 | |
|   for (let { name, value } of bag.enumerator) {
 | |
|     result[name] = value;
 | |
|   }
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| var modalType;
 | |
| var tabSubDialogsEnabled = SpecialPowers.Services.prefs.getBoolPref(
 | |
|   "prompts.tabChromePromptSubDialog",
 | |
|   false
 | |
| );
 | |
| var contentSubDialogsEnabled = SpecialPowers.Services.prefs.getBoolPref(
 | |
|   "prompts.contentPromptSubDialog",
 | |
|   false
 | |
| );
 | |
| var isSelectDialog = false;
 | |
| var isOSX = "nsILocalFileMac" in SpecialPowers.Ci;
 | |
| var isE10S = SpecialPowers.Services.appinfo.processType == 2;
 | |
| 
 | |
| var gChromeScript = SpecialPowers.loadChromeScript(
 | |
|   SimpleTest.getTestFileURL("chromeScript.js")
 | |
| );
 | |
| SimpleTest.registerCleanupFunction(() => gChromeScript.destroy());
 | |
| 
 | |
| async function runPromptCombinations(window, testFunc) {
 | |
|   let util = new PromptTestUtil(window);
 | |
|   let run = () => {
 | |
|     info(
 | |
|       `Running tests (modalType=${modalType}, usePromptService=${util.usePromptService}, useBrowsingContext=${util.useBrowsingContext}, useAsync=${util.useAsync})`
 | |
|     );
 | |
|     return testFunc(util);
 | |
|   };
 | |
| 
 | |
|   // Prompt service with dom window parent only supports window prompts
 | |
|   util.usePromptService = true;
 | |
|   util.useBrowsingContext = false;
 | |
|   util.modalType = Ci.nsIPrompt.MODAL_TYPE_WINDOW;
 | |
|   modalType = util.modalType;
 | |
|   util.useAsync = false;
 | |
|   await run();
 | |
| 
 | |
|   let modalTypes = [
 | |
|     Ci.nsIPrompt.MODAL_TYPE_WINDOW,
 | |
|     Ci.nsIPrompt.MODAL_TYPE_TAB,
 | |
|     Ci.nsIPrompt.MODAL_TYPE_CONTENT,
 | |
|   ];
 | |
| 
 | |
|   for (let type of modalTypes) {
 | |
|     util.modalType = type;
 | |
|     modalType = type;
 | |
| 
 | |
|     // Prompt service with browsing context sync
 | |
|     util.usePromptService = true;
 | |
|     util.useBrowsingContext = true;
 | |
|     util.useAsync = false;
 | |
|     await run();
 | |
| 
 | |
|     // Prompt service with browsing context async
 | |
|     util.usePromptService = true;
 | |
|     util.useBrowsingContext = true;
 | |
|     util.useAsync = true;
 | |
|     await run();
 | |
| 
 | |
|     // nsIPrompt
 | |
|     // modalType is set via nsIWritablePropertyBag (legacy)
 | |
|     util.usePromptService = false;
 | |
|     util.useBrowsingContext = false;
 | |
|     util.useAsync = false;
 | |
|     await run();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PromptTestUtil {
 | |
|   constructor(window) {
 | |
|     this.window = window;
 | |
|     this.browsingContext = SpecialPowers.wrap(
 | |
|       window
 | |
|     ).windowGlobalChild.browsingContext;
 | |
|     this.promptService = SpecialPowers.Services.prompt;
 | |
|     this.nsPrompt = Cc["@mozilla.org/prompter;1"]
 | |
|       .getService(Ci.nsIPromptFactory)
 | |
|       .getPrompt(window, Ci.nsIPrompt);
 | |
| 
 | |
|     this.usePromptService = null;
 | |
|     this.useBrowsingContext = null;
 | |
|     this.useAsync = null;
 | |
|     this.modalType = null;
 | |
|   }
 | |
| 
 | |
|   get _prompter() {
 | |
|     if (this.usePromptService) {
 | |
|       return this.promptService;
 | |
|     }
 | |
|     return this.nsPrompt;
 | |
|   }
 | |
| 
 | |
|   async prompt(funcName, promptArgs) {
 | |
|     if (
 | |
|       this.useBrowsingContext == null ||
 | |
|       this.usePromptService == null ||
 | |
|       this.useAsync == null ||
 | |
|       this.modalType == null
 | |
|     ) {
 | |
|       throw new Error("Not initialized");
 | |
|     }
 | |
|     let args = [];
 | |
|     if (this.usePromptService) {
 | |
|       if (this.useBrowsingContext) {
 | |
|         if (this.useAsync) {
 | |
|           funcName = `async${funcName[0].toUpperCase()}${funcName.substring(
 | |
|             1
 | |
|           )}`;
 | |
|         } else {
 | |
|           funcName += "BC";
 | |
|         }
 | |
|         args = [this.browsingContext, this.modalType];
 | |
|       } else {
 | |
|         args = [this.window];
 | |
|       }
 | |
|     } else {
 | |
|       let bag = this.nsPrompt.QueryInterface(Ci.nsIWritablePropertyBag2);
 | |
|       bag.setPropertyAsUint32("modalType", this.modalType);
 | |
|     }
 | |
|     // Append the prompt arguments
 | |
|     args = args.concat(promptArgs);
 | |
| 
 | |
|     let interfaceName = this.usePromptService ? "Services.prompt" : "prompt";
 | |
|     ok(
 | |
|       this._prompter[funcName],
 | |
|       `${interfaceName} should have method ${funcName}.`
 | |
|     );
 | |
| 
 | |
|     info(`Calling ${interfaceName}.${funcName}(${args})`);
 | |
|     let result = this._prompter[funcName](...args);
 | |
|     is(
 | |
|       this.useAsync,
 | |
|       result != null &&
 | |
|         result.constructor != null &&
 | |
|         result.constructor.name === "Promise",
 | |
|       "If method is async it should return a promise."
 | |
|     );
 | |
| 
 | |
|     if (this.useAsync) {
 | |
|       let propBag = await result;
 | |
|       return propBag && propBagToObject(propBag);
 | |
|     }
 | |
|     return result;
 | |
|   }
 | |
| }
 | |
| 
 | |
| function onloadPromiseFor(id) {
 | |
|   var iframe = document.getElementById(id);
 | |
|   return new Promise(resolve => {
 | |
|     iframe.addEventListener(
 | |
|       "load",
 | |
|       function(e) {
 | |
|         resolve(true);
 | |
|       },
 | |
|       { once: true }
 | |
|     );
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Take an action on the next prompt that appears without checking the state in advance.
 | |
|  * This is useful when the action doesn't depend on which prompt is shown and you
 | |
|  * are expecting multiple prompts at once in an indeterminate order.
 | |
|  * If you know the state of the prompt you expect you should use `handlePrompt` instead.
 | |
|  * @param {object} action defining how to handle the prompt
 | |
|  * @returns {Promise} resolving with the prompt state.
 | |
|  */
 | |
| function handlePromptWithoutChecks(action) {
 | |
|   return new Promise(resolve => {
 | |
|     gChromeScript.addMessageListener("promptHandled", function handled(msg) {
 | |
|       gChromeScript.removeMessageListener("promptHandled", handled);
 | |
|       resolve(msg.promptState);
 | |
|     });
 | |
|     gChromeScript.sendAsyncMessage("handlePrompt", { action, modalType });
 | |
|   });
 | |
| }
 | |
| 
 | |
| async function handlePrompt(state, action) {
 | |
|   let actualState = await handlePromptWithoutChecks(action);
 | |
|   checkPromptState(actualState, state);
 | |
| }
 | |
| 
 | |
| function checkPromptState(promptState, expectedState) {
 | |
|   info(`checkPromptState: Expected: ${expectedState.msg}`);
 | |
|   // XXX check title? OS X has title in content
 | |
|   is(promptState.msg, expectedState.msg, "Checking expected message");
 | |
| 
 | |
|   let isOldContentPrompt =
 | |
|     !promptState.isSubDialogPrompt &&
 | |
|     modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT;
 | |
| 
 | |
|   if (isOldContentPrompt && !promptState.showCallerOrigin) {
 | |
|     ok(
 | |
|       promptState.titleHidden,
 | |
|       "The title should be hidden for content prompts opened with tab modal prompt."
 | |
|     );
 | |
|   } else if (
 | |
|     isOSX ||
 | |
|     promptState.isSubDialogPrompt ||
 | |
|     promptState.showCallerOrigin
 | |
|   ) {
 | |
|     ok(
 | |
|       !promptState.titleHidden,
 | |
|       "Checking title always visible on OS X or when opened with common dialog"
 | |
|     );
 | |
|   } else {
 | |
|     is(
 | |
|       promptState.titleHidden,
 | |
|       expectedState.titleHidden,
 | |
|       "Checking title visibility"
 | |
|     );
 | |
|   }
 | |
|   is(
 | |
|     promptState.textHidden,
 | |
|     expectedState.textHidden,
 | |
|     "Checking textbox visibility"
 | |
|   );
 | |
|   is(
 | |
|     promptState.passHidden,
 | |
|     expectedState.passHidden,
 | |
|     "Checking passbox visibility"
 | |
|   );
 | |
|   is(
 | |
|     promptState.checkHidden,
 | |
|     expectedState.checkHidden,
 | |
|     "Checking checkbox visibility"
 | |
|   );
 | |
|   is(promptState.checkMsg, expectedState.checkMsg, "Checking checkbox label");
 | |
|   is(promptState.checked, expectedState.checked, "Checking checkbox checked");
 | |
|   if (
 | |
|     modalType === Ci.nsIPrompt.MODAL_TYPE_WINDOW ||
 | |
|     (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB && tabSubDialogsEnabled)
 | |
|   ) {
 | |
|     is(
 | |
|       promptState.iconClass,
 | |
|       expectedState.iconClass,
 | |
|       "Checking expected icon CSS class"
 | |
|     );
 | |
|   }
 | |
|   is(promptState.textValue, expectedState.textValue, "Checking textbox value");
 | |
|   is(promptState.passValue, expectedState.passValue, "Checking passbox value");
 | |
| 
 | |
|   if (expectedState.butt0Label) {
 | |
|     is(
 | |
|       promptState.butt0Label,
 | |
|       expectedState.butt0Label,
 | |
|       "Checking accept-button label"
 | |
|     );
 | |
|   }
 | |
|   if (expectedState.butt1Label) {
 | |
|     is(
 | |
|       promptState.butt1Label,
 | |
|       expectedState.butt1Label,
 | |
|       "Checking cancel-button label"
 | |
|     );
 | |
|   }
 | |
|   if (expectedState.butt2Label) {
 | |
|     is(
 | |
|       promptState.butt2Label,
 | |
|       expectedState.butt2Label,
 | |
|       "Checking extra1-button label"
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   // For prompts with a time-delay button.
 | |
|   if (expectedState.butt0Disabled) {
 | |
|     is(promptState.butt0Disabled, true, "Checking accept-button is disabled");
 | |
|     is(
 | |
|       promptState.butt1Disabled,
 | |
|       false,
 | |
|       "Checking cancel-button isn't disabled"
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   is(
 | |
|     promptState.defButton0,
 | |
|     expectedState.defButton == "button0",
 | |
|     "checking button0 default"
 | |
|   );
 | |
|   is(
 | |
|     promptState.defButton1,
 | |
|     expectedState.defButton == "button1",
 | |
|     "checking button1 default"
 | |
|   );
 | |
|   is(
 | |
|     promptState.defButton2,
 | |
|     expectedState.defButton == "button2",
 | |
|     "checking button2 default"
 | |
|   );
 | |
| 
 | |
|   if (
 | |
|     isOSX &&
 | |
|     expectedState.focused &&
 | |
|     expectedState.focused.startsWith("button") &&
 | |
|     !promptState.infoRowHidden
 | |
|   ) {
 | |
|     is(
 | |
|       promptState.focused,
 | |
|       "infoBody",
 | |
|       "buttons don't focus on OS X, but infoBody does instead"
 | |
|     );
 | |
|   } else {
 | |
|     is(promptState.focused, expectedState.focused, "Checking focused element");
 | |
|   }
 | |
| 
 | |
|   if (expectedState.hasOwnProperty("chrome")) {
 | |
|     is(
 | |
|       promptState.chrome,
 | |
|       expectedState.chrome,
 | |
|       "Dialog should be opened as chrome"
 | |
|     );
 | |
|   }
 | |
|   if (expectedState.hasOwnProperty("dialog")) {
 | |
|     is(
 | |
|       promptState.dialog,
 | |
|       expectedState.dialog,
 | |
|       "Dialog should be opened as a dialog"
 | |
|     );
 | |
|   }
 | |
|   if (expectedState.hasOwnProperty("chromeDependent")) {
 | |
|     is(
 | |
|       promptState.chromeDependent,
 | |
|       expectedState.chromeDependent,
 | |
|       "Dialog should be opened as dependent"
 | |
|     );
 | |
|   }
 | |
|   if (expectedState.hasOwnProperty("isWindowModal")) {
 | |
|     is(
 | |
|       promptState.isWindowModal,
 | |
|       expectedState.isWindowModal,
 | |
|       "Dialog should be modal"
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| function checkEchoedAuthInfo(expectedState, browsingContext) {
 | |
|   return SpecialPowers.spawn(
 | |
|     browsingContext,
 | |
|     [expectedState.user, expectedState.pass],
 | |
|     (expectedUser, expectedPass) => {
 | |
|       let doc = this.content.document;
 | |
| 
 | |
|       // The server echos back the HTTP auth info it received.
 | |
|       let username = doc.getElementById("user").textContent;
 | |
|       let password = doc.getElementById("pass").textContent;
 | |
|       let authok = doc.getElementById("ok").textContent;
 | |
| 
 | |
|       Assert.equal(authok, "PASS", "Checking for successful authentication");
 | |
|       Assert.equal(username, expectedUser, "Checking for echoed username");
 | |
|       Assert.equal(password, expectedPass, "Checking for echoed password");
 | |
|     }
 | |
|   );
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Create a Proxy to relay method calls on an nsIAuthPrompt[2] prompter to a chrome script which can
 | |
|  * perform the calls in the parent. Out and inout params will be copied back from the parent to
 | |
|  * content.
 | |
|  *
 | |
|  * @param chromeScript The reference to the chrome script that will listen to `proxyPrompter`
 | |
|  *                     messages in the parent and call the `methodName` method.
 | |
|  *                     The return value from the message handler should be an object with properties:
 | |
|  * `rv` - containing the return value of the method call.
 | |
|  * `args` - containing the array of arguments passed to the method since out or inout ones could have
 | |
|  *          been modified.
 | |
|  */
 | |
| function PrompterProxy(chromeScript) {
 | |
|   return new Proxy(
 | |
|     {},
 | |
|     {
 | |
|       get(target, prop, receiver) {
 | |
|         return (...args) => {
 | |
|           // Array of indices of out/inout params to copy from the parent back to the caller.
 | |
|           let outParams = [];
 | |
| 
 | |
|           switch (prop) {
 | |
|             case "prompt": {
 | |
|               outParams = [/* result */ 5];
 | |
|               break;
 | |
|             }
 | |
|             case "promptAuth": {
 | |
|               outParams = [];
 | |
|               break;
 | |
|             }
 | |
|             case "promptPassword": {
 | |
|               outParams = [/* pwd */ 4];
 | |
|               break;
 | |
|             }
 | |
|             case "promptUsernameAndPassword": {
 | |
|               outParams = [/* user */ 4, /* pwd */ 5];
 | |
|               break;
 | |
|             }
 | |
|             default: {
 | |
|               throw new Error("Unknown nsIAuthPrompt method");
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           let result;
 | |
|           chromeScript
 | |
|             .sendQuery("proxyPrompter", {
 | |
|               args,
 | |
|               methodName: prop,
 | |
|             })
 | |
|             .then(val => {
 | |
|               result = val;
 | |
|             });
 | |
|           SpecialPowers.Services.tm.spinEventLoopUntil(
 | |
|             "Test(prompt_common.js:get)",
 | |
|             () => result
 | |
|           );
 | |
| 
 | |
|           for (let outParam of outParams) {
 | |
|             // Copy the out or inout param value over the original
 | |
|             args[outParam].value = result.args[outParam].value;
 | |
|           }
 | |
| 
 | |
|           if (prop == "promptAuth") {
 | |
|             args[2].username = result.args[2].username;
 | |
|             args[2].password = result.args[2].password;
 | |
|             args[2].domain = result.args[2].domain;
 | |
|           }
 | |
| 
 | |
|           return result.rv;
 | |
|         };
 | |
|       },
 | |
|     }
 | |
|   );
 | |
| }
 | 
