Commit cad300b9 authored by Makoto Kato's avatar Makoto Kato
Browse files

Bug 1758800 - Watch the modification of child nodes in <select> element. r=agi,ohall

This is a kind of bug 1263887 for GeckoView.

We should update the prompt when child nodes in `<select>` element is modified.

I add `PromptInstanceDelegate.onPromptUpdate` to update the prompt's content.
Browser application should implement it if <select> element is updated during
showing the prompt.

Differential Revision: https://phabricator.services.mozilla.com/D144538
parent 5c2c4827
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -24,6 +24,13 @@ class GeckoViewPrompterChild extends GeckoViewActorChild {
    this.unregisterPrompt(prompt);
  }

  updatePrompt(message) {
    this.eventDispatcher.sendRequest({
      type: "GeckoView:Prompt:Update",
      prompt: message,
    });
  }

  unregisterPrompt(prompt) {
    this._prompts.delete(prompt.id);
    this.sendAsyncMessage("UnregisterPrompt", {
+39 −1
Original line number Diff line number Diff line
@@ -18,6 +18,12 @@ XPCOMUtils.defineLazyModuleGetters(this, {
  Services: "resource://gre/modules/Services.jsm",
});

ChromeUtils.defineModuleGetter(
  this,
  "DeferredTask",
  "resource://gre/modules/DeferredTask.jsm"
);

const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt");

class PromptFactory {
@@ -86,7 +92,7 @@ class PromptFactory {
    }
  }

  _handleSelect(aElement, aIsDropDown) {
  _generateSelectItems(aElement) {
    const win = aElement.ownerGlobal;
    let id = 0;
    const map = {};
@@ -118,10 +124,38 @@ class PromptFactory {
      return items;
    })(aElement);

    return [items, map, id];
  }

  _handleSelect(aElement, aIsDropDown) {
    const win = aElement.ownerGlobal;
    const [items] = this._generateSelectItems(aElement);

    if (aIsDropDown) {
      aElement.openInParentProcess = true;
    }

    const prompt = new GeckoViewPrompter(win);

    // Something changed the <select> while it was open.
    const deferredUpdate = new DeferredTask(() => {
      // Inner contents in choice prompt are updated.
      const [newItems] = this._generateSelectItems(aElement);
      prompt.update({
        type: "choice",
        mode: aElement.multiple ? "multiple" : "single",
        choices: newItems,
      });
    }, 0);
    const mut = new win.MutationObserver(() => {
      deferredUpdate.arm();
    });
    mut.observe(aElement, {
      childList: true,
      subtree: true,
      attributes: true,
    });

    prompt.asyncShowPrompt(
      {
        type: "choice",
@@ -129,6 +163,9 @@ class PromptFactory {
        choices: items,
      },
      result => {
        deferredUpdate.disarm();
        mut.disconnect();

        if (aIsDropDown) {
          aElement.openInParentProcess = false;
        }
@@ -138,6 +175,7 @@ class PromptFactory {
          return;
        }

        const [, map, id] = this._generateSelectItems(aElement);
        let dispatchEvents = false;
        if (!aElement.multiple) {
          const elem = map[result.choices[0]];
+6 −0
Original line number Diff line number Diff line
@@ -209,4 +209,10 @@ class GeckoViewPrompter {
    aMsg = undefined;
    aCallback = undefined;
  }

  update(aMsg) {
    this.message = aMsg;
    aMsg.id = this.id;
    this.prompterActor?.updatePrompt(aMsg);
  }
}
+1 −0
Original line number Diff line number Diff line
@@ -1423,6 +1423,7 @@ package org.mozilla.geckoview {

  public static interface GeckoSession.PromptDelegate.PromptInstanceDelegate {
    method @UiThread default public void onPromptDismiss(@NonNull GeckoSession.PromptDelegate.BasePrompt);
    method @UiThread default public void onPromptUpdate(@NonNull GeckoSession.PromptDelegate.BasePrompt);
  }

  public static class GeckoSession.PromptDelegate.PromptResponse {
+50 −0
Original line number Diff line number Diff line
@@ -381,6 +381,56 @@ class PromptDelegateTest : BaseSessionTest() {
        sessionRule.waitForResult(result)
    }

    @Test
    @WithDisplay(width = 100, height = 100)
    fun selectTestUpdate() {
        mainSession.loadTestPath(SELECT_HTML_PATH)
        sessionRule.waitForPageStop()

        val result = GeckoResult<PromptDelegate.PromptResponse>()
        val promptInstanceDelegate = object : PromptDelegate.PromptInstanceDelegate {
            override fun onPromptUpdate(prompt: PromptDelegate.BasePrompt) {
                val newPrompt: PromptDelegate.ChoicePrompt = prompt as PromptDelegate.ChoicePrompt
                assertThat("First choice is correct", newPrompt.choices[0].label, equalTo("foo"))
                assertThat("Second choice is correct", newPrompt.choices[1].label, equalTo("bar"))
                assertThat("Third choice is correct", newPrompt.choices[2].label, equalTo("baz"))
                result.complete(prompt.confirm(newPrompt.choices[2]))
            }
        }

        sessionRule.delegateUntilTestEnd(object: PromptDelegate {
            @AssertCalled(count = 1)
            override fun onChoicePrompt(session: GeckoSession, prompt: PromptDelegate.ChoicePrompt): GeckoResult<PromptDelegate.PromptResponse>? {
                assertThat("There should be two choices", prompt.choices.size, equalTo(2))
                prompt.setDelegate(promptInstanceDelegate)
                return result
            }
        })

        mainSession.evaluateJS("""
            document.querySelector("select").addEventListener("focus", () => {
                window.setTimeout(() => {
                    document.querySelector("select").innerHTML =
                        "<option>foo</option><option>bar</option><option>baz</option>";
                }, 100);
            }, { once: true })
        """.trimIndent())

        val promise = mainSession.evaluatePromiseJS("""
            new Promise(resolve => {
                document.querySelector("select").addEventListener("change", e => {
                    resolve(e.target.value);
                });
            })
        """.trimIndent())

        mainSession.synthesizeTap(10, 10)
        sessionRule.waitForResult(result)
        assertThat("Selected item should be as expected",
                   promise.value as String,
                   equalTo("baz"))
    }

    @Test
    fun onBeforeUnloadTest() {
        sessionRule.setPrefsUntilTestEnd(mapOf(
Loading