Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE

This commit is contained in:
Bogdan Tara 2019-01-10 19:27:05 +02:00
commit 3ca870b28b
75 changed files with 2587 additions and 1491 deletions

10
Cargo.lock generated
View File

@ -837,21 +837,21 @@ name = "encoding_c"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"encoding_rs 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)",
"encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "encoding_glue"
version = "0.1.0"
dependencies = [
"encoding_rs 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)",
"encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)",
"nserror 0.1.0",
"nsstring 0.1.0",
]
[[package]]
name = "encoding_rs"
version = "0.8.13"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1750,7 +1750,7 @@ name = "nsstring"
version = "0.1.0"
dependencies = [
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
"encoding_rs 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)",
"encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -3256,7 +3256,7 @@ dependencies = [
"checksum either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18785c1ba806c258137c937e44ada9ee7e69a37e3c72077542cd2f069d78562a"
"checksum ena 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "88dc8393b3c7352f94092497f6b52019643e493b6b890eb417cdb7c46117e621"
"checksum encoding_c 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "769ecb8b33323998e482b218c0d13cd64c267609023b4b7ec3ee740714c318ee"
"checksum encoding_rs 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1a8fa54e6689eb2549c4efed8d00d7f3b2b994a064555b0e8df4ae3764bcc4be"
"checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
"checksum env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0561146661ae44c579e993456bc76d11ce1e0c7d745e57b2fa7146b6e49fa2ad"
"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
"checksum euclid 0.19.4 (registry+https://github.com/rust-lang/crates.io-index)" = "dbbf962bb6f877239a34491f2e0a12c6b824f389bc789eb90f1d70d4780b0727"

View File

@ -251,8 +251,13 @@ void DocAccessibleWrap::UpdateFocusPathBounds() {
SessionAccessibility::GetInstanceFor(this)) {
nsTArray<AccessibleWrap*> accessibles(mFocusPath.Count());
for (auto iter = mFocusPath.Iter(); !iter.Done(); iter.Next()) {
accessibles.AppendElement(
static_cast<AccessibleWrap*>(iter.Data().get()));
Accessible* accessible = iter.Data();
if (!accessible || accessible->IsDefunct()) {
MOZ_ASSERT_UNREACHABLE("Focus path cached accessible is gone.");
continue;
}
accessibles.AppendElement(static_cast<AccessibleWrap*>(accessible));
}
sessionAcc->UpdateCachedBounds(accessibles);

View File

@ -3688,13 +3688,26 @@ function getClosestNonBucketNode(item) {
return getClosestNonBucketNode(parent);
}
function getNonPrototypeParentGripValue(item) {
function getParentGripNode(item) {
const parentNode = getParent(item);
if (!parentNode) {
return null;
}
const parentGripNode = getClosestGripNode(parentNode);
return getClosestGripNode(parentNode);
}
function getParentGripValue(item) {
const parentGripNode = getParentGripNode(item);
if (!parentGripNode) {
return null;
}
return getValue(parentGripNode);
}
function getNonPrototypeParentGripValue(item) {
const parentGripNode = getParentGripNode(item);
if (!parentGripNode) {
return null;
}
@ -3716,6 +3729,7 @@ module.exports = {
getClosestGripNode,
getClosestNonBucketNode,
getParent,
getParentGripValue,
getNonPrototypeParentGripValue,
getNumericalPropertiesCount,
getValue,
@ -6406,11 +6420,11 @@ function releaseActors(state, client) {
}
}
function invokeGetter(node, grip, getterName) {
function invokeGetter(node, targetGrip, receiverId, getterName) {
return async ({ dispatch, client, getState }) => {
try {
const objectClient = client.createObjectClient(grip);
const result = await objectClient.getPropertyValue(getterName);
const objectClient = client.createObjectClient(targetGrip);
const result = await objectClient.getPropertyValue(getterName, receiverId);
dispatch({
type: "GETTER_INVOKED",
data: {
@ -7004,6 +7018,7 @@ const {
nodeIsLongString,
nodeHasFullText,
nodeHasGetter,
getParentGripValue,
getNonPrototypeParentGripValue
} = Utils.node;
@ -7078,10 +7093,11 @@ class ObjectInspectorItem extends Component {
}
if (nodeHasGetter(item)) {
const parentGrip = getNonPrototypeParentGripValue(item);
if (parentGrip) {
const targetGrip = getParentGripValue(item);
const receiverGrip = getNonPrototypeParentGripValue(item);
if (targetGrip && receiverGrip) {
Object.assign(repProps, {
onInvokeGetterButtonClick: () => this.props.invokeGetter(item, parentGrip, item.name)
onInvokeGetterButtonClick: () => this.props.invokeGetter(item, targetGrip, receiverGrip.actor, item.name)
});
}
}

View File

@ -355,6 +355,7 @@ skip-if = true # Bug 1438979
[browser_webconsole_object_inspector_entries.js]
[browser_webconsole_object_inspector_getters.js]
[browser_webconsole_object_inspector_getters_prototype.js]
[browser_webconsole_object_inspector_getters_shadowed.js]
[browser_webconsole_object_inspector_key_sorting.js]
[browser_webconsole_object_inspector_local_session_storage.js]
[browser_webconsole_object_inspector_selected_text.js]

View File

@ -0,0 +1,79 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check evaluating shadowed getters in the console.
const TEST_URI = "data:text/html;charset=utf8,<h1>Object Inspector on Getters</h1>";
add_task(async function() {
const hud = await openNewTabAndConsole(TEST_URI);
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
const a = {
getter: "[A]",
__proto__: {
get getter() {
return "[B]";
},
__proto__: {
get getter() {
return "[C]";
},
},
},
};
const b = {
value: 1,
get getter() {
return `[A-${this.value}]`;
},
__proto__: {
value: 2,
get getter() {
return `[B-${this.value}]`;
},
},
};
content.wrappedJSObject.console.log("oi-test", a, b);
});
const node = await waitFor(() => findMessage(hud, "oi-test"));
const [a, b] = node.querySelectorAll(".tree");
await testObject(a, [null, "[B]", "[C]"]);
await testObject(b, ["[A-1]", "[B-1]"]);
});
async function testObject(oi, values) {
let node = oi.querySelector(".tree-node");
for (const value of values) {
await expand(node);
if (value != null) {
const getter = findObjectInspectorNodeChild(node, "getter");
await invokeGetter(getter);
ok(getter.textContent.includes(`getter: "${value}"`),
`Getter now has the expected "${value}" content`);
}
node = findObjectInspectorNodeChild(node, "<prototype>");
}
}
function expand(node) {
expandObjectInspectorNode(node);
return waitFor(() => getObjectInspectorChildrenNodes(node).length > 0);
}
function invokeGetter(node) {
getObjectInspectorInvokeGetterButton(node).click();
return waitFor(() => !getObjectInspectorInvokeGetterButton(node));
}
function findObjectInspectorNodeChild(node, nodeLabel) {
return getObjectInspectorChildrenNodes(node).find(child => {
const label = child.querySelector(".object-label");
return label && label.textContent === nodeLabel;
});
}

View File

@ -511,13 +511,26 @@ const proto = {
*
* @param {string} name
* The property we want the value of.
* @param {string|null} receiverId
* The actorId of the receiver to be used if the property is a getter.
* If null or invalid, the receiver will be the referent.
*/
propertyValue: function(name) {
propertyValue: function(name, receiverId) {
if (!name) {
return this.throwError("missingParameter", "no property name was specified");
}
const value = this.obj.getProperty(name);
let receiver;
if (receiverId) {
const receiverActor = this.conn.getActor(receiverId);
if (receiverActor) {
receiver = receiverActor.obj;
}
}
const value = receiver
? this.obj.getProperty(name, receiver)
: this.obj.getProperty(name);
return { value: this._buildCompletion(value) };
},

View File

@ -37,17 +37,17 @@ async function test_object_grip(debuggee, threadClient) {
});
`,
async objClient => {
const obj1 = (await objClient.getPropertyValue("obj1")).value.return;
const obj2 = (await objClient.getPropertyValue("obj2")).value.return;
const obj1 = (await objClient.getPropertyValue("obj1", null)).value.return;
const obj2 = (await objClient.getPropertyValue("obj2", null)).value.return;
const context = threadClient.pauseGrip(
(await objClient.getPropertyValue("context")).value.return,
(await objClient.getPropertyValue("context", null)).value.return,
);
const sum = threadClient.pauseGrip(
(await objClient.getPropertyValue("sum")).value.return,
(await objClient.getPropertyValue("sum", null)).value.return,
);
const error = threadClient.pauseGrip(
(await objClient.getPropertyValue("error")).value.return,
(await objClient.getPropertyValue("error", null)).value.return,
);
assert_response(await context.apply(obj1, [obj1]), {

View File

@ -35,7 +35,7 @@ async function test_object_grip(debuggee, threadClient) {
const objClient = threadClient.pauseGrip(obj);
const method = threadClient.pauseGrip(
(await objClient.getPropertyValue("method")).value.return,
(await objClient.getPropertyValue("method", null)).value.return,
);
// Ensure that we actually paused at the `debugger;` line.

View File

@ -33,7 +33,7 @@ async function test_object_grip(debuggee, threadClient) {
const objClient = threadClient.pauseGrip(obj);
const method = threadClient.pauseGrip(
(await objClient.getPropertyValue("method")).value.return,
(await objClient.getPropertyValue("method", null)).value.return,
);
try {

View File

@ -99,7 +99,7 @@ async function test_object_grip(debuggee, threadClient) {
};
for (const [key, expected] of Object.entries(expectedValues)) {
const { value } = await objClient.getPropertyValue(key);
const { value } = await objClient.getPropertyValue(key, null);
assert_completion(value, expected);
}

View File

@ -40,7 +40,7 @@ async function test_object_grip(debuggee, threadClient) {
Assert.equal(frame.where.line, 4);
Assert.equal(frame.where.column, 8);
}),
objClient.getPropertyValue("prop"),
objClient.getPropertyValue("prop", null),
]);
}

View File

@ -0,0 +1,74 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable no-shadow, max-nested-callbacks */
"use strict";
Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("security.allow_eval_with_system_principal");
});
add_task(threadClientTest(async ({ threadClient, debuggee, client }) => {
debuggee.eval(function stopMe() {
debugger;
}.toString());
await test_object_grip(debuggee, threadClient);
}));
async function test_object_grip(debuggee, threadClient) {
eval_and_resume(
debuggee,
threadClient,
`
var obj = {
get getter() {
return objects.indexOf(this);
},
};
var objects = [obj, {}, [], new Boolean(), new Number(), new String()];
stopMe(...objects);
`,
async frame => {
const grips = frame.arguments;
const objClient = threadClient.pauseGrip(grips[0]);
const classes = ["Object", "Object", "Array", "Boolean", "Number", "String"];
for (const [i, grip] of grips.entries()) {
Assert.equal(grip.class, classes[i]);
await check_getter(objClient, grip.actor, i);
}
await check_getter(objClient, null, 0);
await check_getter(objClient, "invalid receiver actorId", 0);
}
);
}
function eval_and_resume(debuggee, threadClient, code, callback) {
return new Promise((resolve, reject) => {
wait_for_pause(threadClient, callback).then(resolve, reject);
// This synchronously blocks until 'threadClient.resume()' above runs
// because the 'paused' event runs everthing in a new event loop.
debuggee.eval(code);
});
}
function wait_for_pause(threadClient, callback = () => {}) {
return new Promise((resolve, reject) => {
threadClient.addOneTimeListener("paused", function(event, packet) {
(async () => {
try {
return await callback(packet.frame);
} finally {
await threadClient.resume();
}
})().then(resolve, reject);
});
});
}
async function check_getter(objClient, receiverId, expected) {
const {value} = await objClient.getPropertyValue("getter", receiverId);
Assert.equal(value.return, expected);
}

View File

@ -165,6 +165,7 @@ reason = bug 1104838
[test_objectgrips-array-like-object.js]
[test_objectgrips-property-value-01.js]
[test_objectgrips-property-value-02.js]
[test_objectgrips-property-value-03.js]
[test_objectgrips-fn-apply-01.js]
[test_objectgrips-fn-apply-02.js]
[test_objectgrips-fn-apply-03.js]

View File

@ -189,11 +189,13 @@ ObjectClient.prototype = {
* Request the value of the object's specified property.
*
* @param name string The name of the requested property.
* @param receiverId string|null The actorId of the receiver to be used for getters.
* @param onResponse function Called with the request's response.
*/
getPropertyValue: DebuggerClient.requester({
type: "propertyValue",
name: arg(0),
receiverId: arg(1),
}),
/**

View File

@ -181,6 +181,7 @@ const objectSpec = generateActorSpec({
propertyValue: {
request: {
name: Arg(0, "string"),
receiverId: Arg(1, "nullable:string"),
},
response: RetVal("object.propertyValue"),
},

View File

@ -151,7 +151,17 @@ class Animation : public DOMEventTargetHelper,
virtual void Tick();
bool NeedsTicks() const {
return Pending() || PlayState() == AnimationPlayState::Running;
return Pending() ||
(PlayState() == AnimationPlayState::Running &&
// An animation with a zero playback rate doesn't need ticks even if
// it is running since it effectively behaves as if it is paused.
//
// It's important we return false in this case since a zero playback
// rate animation in the before or after phase that doesn't fill
// won't be relevant and hence won't be returned by GetAnimations().
// We don't want its timeline to keep it alive (which would happen
// if we return true) since otherwise it will effectively be leaked.
PlaybackRate() != 0.0);
}
/**

View File

@ -24,8 +24,7 @@ void TextEncoder::Encode(JSContext* aCx, JS::Handle<JSObject*> aObj,
// in the future.
// Uint8Array::Create takes uint32_t as the length.
CheckedInt<uint32_t> bufLen(aString.Length());
bufLen *= 3;
bufLen += 1; // plus one is part of the contract for ConvertUTF16toUTF8
bufLen *= 3; // from the contract for ConvertUTF16toUTF8
if (!bufLen.isValid()) {
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
return;

View File

@ -187,7 +187,9 @@ nsresult AudioSink::InitializeAudioStream(const PlaybackParams& aParams) {
// Set playback params before calling Start() so they can take effect
// as soon as the 1st DataCallback of the AudioStream fires.
mAudioStream->SetVolume(aParams.mVolume);
if (aParams.mVolume) {
mAudioStream->SetVolume(*aParams.mVolume);
}
mAudioStream->SetPlaybackRate(aParams.mPlaybackRate);
mAudioStream->SetPreservesPitch(aParams.mPreservesPitch);
return mAudioStream->Start();

View File

@ -29,7 +29,9 @@ const MediaSink::PlaybackParams& AudioSinkWrapper::GetPlaybackParams() const {
void AudioSinkWrapper::SetPlaybackParams(const PlaybackParams& aParams) {
AssertOwnerThread();
if (mAudioSink) {
mAudioSink->SetVolume(aParams.mVolume);
if (aParams.mVolume) {
mAudioSink->SetVolume(*aParams.mVolume);
}
mAudioSink->SetPlaybackRate(aParams.mPlaybackRate);
mAudioSink->SetPreservesPitch(aParams.mPreservesPitch);
}
@ -96,7 +98,7 @@ bool AudioSinkWrapper::HasUnplayedFrames(TrackType aType) const {
void AudioSinkWrapper::SetVolume(double aVolume) {
AssertOwnerThread();
mParams.mVolume = aVolume;
mParams.mVolume = Some(aVolume);
if (mAudioSink) {
mAudioSink->SetVolume(aVolume);
}
@ -196,6 +198,10 @@ void AudioSinkWrapper::Stop() {
if (mAudioSink) {
mAudioSinkEndedPromise.DisconnectIfExists();
// Reset volume to signal that it should
// not be updated, in case the volume
// has been changed outside MediaElement.
mParams.mVolume.reset();
mAudioSink->Shutdown();
mAudioSink = nullptr;
mEndedPromise = nullptr;

View File

@ -479,7 +479,7 @@ void DecodedStream::SetPlaying(bool aPlaying) {
void DecodedStream::SetVolume(double aVolume) {
AssertOwnerThread();
mParams.mVolume = aVolume;
mParams.mVolume = Some(aVolume);
}
void DecodedStream::SetPlaybackRate(double aPlaybackRate) {
@ -730,7 +730,8 @@ void DecodedStream::SendData() {
return;
}
SendAudio(mParams.mVolume, mSameOrigin, mPrincipalHandle);
MOZ_ASSERT(mParams.mVolume.isSome(), "Volume should exist at that point");
SendAudio(mParams.mVolume.value(), mSameOrigin, mPrincipalHandle);
SendVideo(mSameOrigin, mPrincipalHandle);
}

View File

@ -39,8 +39,8 @@ class MediaSink {
struct PlaybackParams {
PlaybackParams()
: mVolume(1.0), mPlaybackRate(1.0), mPreservesPitch(true) {}
double mVolume;
: mVolume(Some(1.0)), mPlaybackRate(1.0), mPreservesPitch(true) {}
Maybe<double> mVolume;
double mPlaybackRate;
bool mPreservesPitch;
RefPtr<AudioDeviceInfo> mSink;

View File

@ -606,12 +606,11 @@ nsresult MediaEngineWebRTCMicrophoneSource::Stop(
RefPtr<MediaEngineWebRTCMicrophoneSource> that = this;
NS_DispatchToMainThread(
media::NewRunnableFrom([that, stream = mStream, track = mTrackID]() {
media::NewRunnableFrom([that, stream = mStream]() {
if (stream->IsDestroyed()) {
return NS_OK;
}
stream->SetPullingEnabled(track, false);
stream->GraphImpl()->AppendMessage(MakeUnique<StartStopMessage>(
that->mInputProcessing, StartStopMessage::Stop));
CubebUtils::AudioDeviceID deviceID = that->mDeviceInfo->DeviceID();

View File

@ -9,14 +9,33 @@
<script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
<script type="application/javascript">
function addMouseEventListeners(aTarget) {
aTarget.addEventListener("mousemove", recordEvent, true);
aTarget.addEventListener("mouseover", recordEvent, true);
aTarget.addEventListener("mouseenter", recordEvent, true);
aTarget.addEventListener("mouseout", recordEvent, true);
aTarget.addEventListener("mouseleave", recordEvent, true);
}
function removeMouseEventListeners(aTarget) {
aTarget.removeEventListener("mousemove", recordEvent, true);
aTarget.removeEventListener("mouseover", recordEvent, true);
aTarget.removeEventListener("mouseenter", recordEvent, true);
aTarget.removeEventListener("mouseout", recordEvent, true);
aTarget.removeEventListener("mouseleave", recordEvent, true);
}
function longPressLink() {
synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
let target = document.getElementById("b");
addMouseEventListeners(target);
synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, function() {
dump("Finished synthesizing touch-start, waiting for events...\n");
});
}
var eventsFired = 0;
function recordEvent(e) {
let target = document.getElementById("b");
if (getPlatform() == "windows") {
// On Windows we get a mouselongtap event once the long-tap has been detected
// by APZ, and that's what we use as the trigger to lift the finger. That then
@ -28,12 +47,16 @@ function recordEvent(e) {
synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE);
break;
case 2: is(e.type, "touchend", "Got a touchend"); break;
case 3: is(e.type, "contextmenu", "Got a contextmenu"); e.preventDefault(); break;
case 3: is(e.type, "mouseover", "Got a mouseover"); break;
case 4: is(e.type, "mouseenter", "Got a mouseenter"); break;
case 5: is(e.type, "mousemove", "Got a mousemove"); break;
case 6: is(e.type, "contextmenu", "Got a contextmenu"); e.preventDefault(); break;
default: ok(false, "Got an unexpected event of type " + e.type); break;
}
eventsFired++;
if (eventsFired == 4) {
if (eventsFired == 7) {
removeMouseEventListeners(target);
dump("Finished waiting for events, doing an APZ flush to see if any more unexpected events come through...\n");
flushApzRepaints(function() {
dump("Done APZ flush, ending test...\n");
@ -46,14 +69,18 @@ function recordEvent(e) {
// event at all, and instead get a touchcancel.
switch (eventsFired) {
case 0: is(e.type, "touchstart", "Got a touchstart"); break;
case 1: is(e.type, "contextmenu", "Got a contextmenu"); e.preventDefault(); break;
case 2: is(e.type, "touchcancel", "Got a touchcancel"); break;
case 1: is(e.type, "mouseover", "Got a mouseover"); break;
case 2: is(e.type, "mouseenter", "Got a mouseenter"); break;
case 3: is(e.type, "mousemove", "Got a mousemove"); break;
case 4: is(e.type, "contextmenu", "Got a contextmenu"); e.preventDefault(); break;
case 5: is(e.type, "touchcancel", "Got a touchcancel"); break;
default: ok(false, "Got an unexpected event of type " + e.type); break;
}
eventsFired++;
if (eventsFired == 3) {
synthesizeNativeTouch(document.getElementById("b"), 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
if (eventsFired == 6) {
removeMouseEventListeners(target);
synthesizeNativeTouch(target, 5, 5, SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, function() {
dump("Finished synthesizing touch-end, doing an APZ flush to see if any more unexpected events come through...\n");
flushApzRepaints(function() {
dump("Done APZ flush, ending test...\n");

View File

@ -215,6 +215,17 @@ bool APZEventState::FireContextmenuEvents(
const nsCOMPtr<nsIPresShell>& aPresShell, const CSSPoint& aPoint,
const CSSToLayoutDeviceScale& aScale, Modifiers aModifiers,
const nsCOMPtr<nsIWidget>& aWidget) {
// Synthesize mousemove event for allowing users to emulate to move mouse
// cursor over the element. As a result, users can open submenu UI which
// is opened when mouse cursor is moved over a link (i.e., it's a case that
// users cannot stay in the page after tapping it). So, this improves
// accessibility in websites which are designed for desktop.
// Note that we don't need to check whether mousemove event is consumed or
// not because Chrome also ignores the result.
APZCCallbackHelper::DispatchSynthesizedMouseEvent(
eMouseMove, 0 /* time */, aPoint * aScale, aModifiers, 0 /* clickCount */,
aWidget);
// Converting the modifiers to DOM format for the DispatchMouseEvent call
// is the most useless thing ever because nsDOMWindowUtils::SendMouseEvent
// just converts them back to widget format, but that API has many callers,

View File

@ -22,153 +22,3 @@ For a debug webrender build:
Use a debug mozconfig (ac_add_options --enable-debug)
You can also use an opt build but make webrender less optimized by putting opt-level=0 in the [profile.release] section of your toolkit/library/rust/Cargo.toml file
See also https://groups.google.com/forum/#!topic/mozilla.dev.servo/MbeMcqqO1fs
--------------------------------------------------------------------------------
What if you have to pull in an update to webrender itself? You have two options,
listed below. Both options will give you a set of patches and the ability to do
try pushes to verify the update. After that, continue with the steps below to
actually land the update into the tree.
Option A:
Use a script to do the update for you. This will usually work, if you satisfy
all the assumptions the script is making. The script can be found at
https://github.com/staktrace/wrupdater/blob/master/try-latest-webrender.sh
and contains documentation on how to use it. Read the documentation carefully
before trying to use it.
Option B:
Do the update manually. This is a little more cumbersome but may be required
if the script doesn't work or the repos are in a state that violates hidden
assumptions in the script (e.g. if the webrender_bindings/Cargo.toml file is
no longer in the format expected by the script). The steps to do this are,
roughly:
- Update your mozilla-central checkout to the latest code on mozilla-central.
- Check out and update the webrender repo to the version you want
- Copy over the webrender repo to gfx/wr. The easiest way to do this is
simply delete gfx/wr, and use |cp -R| to copy it back, and then delete the
gfx/wr/.git/ subfolder. Update the revision in
gfx/webrender_bindings/revision.txt file with the git changeset hash.
- If you need to modify webrender_bindings/Cargo.toml file, do so now. Changes
at this step usually consist of:
(a) Updating version numbers. Go through the version numbers of ALL the
dependencies in the Cargo.toml file (webrender, euclid, etc.) and make
sure the version numbers listed match what's in the new
gfx/wr/webrender/Cargo.toml and gfx/wr/webrender_api/Cargo.toml files.
(b) Turning on or off any new features that were added in upstream WR. This
used to happen a lot but is pretty rare now.
- Go to toolkit/library/rust and run |cargo update -p webrender -p webrender_api|.
If it complains about version numbers of other crates not lining up, add those
as well, e.g. |cargo update -p webrender -p webrender_api -p gleam -p euclid|.
You may need to do this a few times until you get all the crates to make it
happy.
- Run the same cargo update command from the previous step in the
toolkit/library/gtest/rust folder.
- Commit your changes locally. You'll need to do this before the next step or
it will complain.
- At the top of the tree, run |mach vendor rust| to update the rust
dependencies in third_party/rust.
- Commit your changes locally.
- Build and test. You may need to make changes in bindings.rs or on the C++
side depending on what changed in webrender. This can potentially be quite
tricky if you don't fully understand the API changes on the webrender side.
Get help if you need it. For simplicity in bisecting, try to not use your
new features yet, just get the build working with the minimal changes.
- Commit any changes from the previous step, and do a try push to make sure
everything is good. Generally we do two try pushes, one for builds and
linux tests. This should be totally green. The other forces WR enabled on
Windows and runs reftests, which currently fails. However if it fails with
more than just regular reftest failures (e.g. it crashes or has an assertion
failure) then that's potentially going to be a problem for Windows users
running WebRender and will need investigation.
- You now have an updated webrender, so you can land it or write gecko
code against the new features.
Once you have followed either Option A or Option B and have a good update, you
might want to land it in the tree. To do this:
- Find the current wr-future-update bug, by going to https://bugzil.la/wr-future-update
- Clone this bug (there is a little dropdown in the bottom right corner of the
page which gives you an option to "Create a new bug ... as a clone of this bug").
- This will take you to a bug entry page with some stuff prepopulated. Do NOT
submit it yet, but make the following changes:
(a) Modify the "Description" to remove the SECOND instance of the text "+++ This
bug was initially created as a clone of ... +++". Keep the first instance
as it points to the bug you just cloned, and keep the rest of the text unless
you feel it needs changing.
(b) Add wr-future-update into the "Alias" field
(c) Clear the bugs in the "Depends on" field
(d) For each bug in the "Blocks" field, except for 1311790 and 1386670, go
to the bug and check the "See Also" link for the corresponding WR issue/PR,
if any. If there is a WR issue that is not yet resolved in the update you
are landing, leave the bug in the "Blocks" field of your clone. In a later
step you will remove the dependency from the update you are landing. At
end of this step the "Blocks" field should contain 1311790, 1386670, and
any bugs tracking upstream WR issues that are not fixed in the update.
(e) You still cannot submit the clone as a new bug, because you can't have two
bugs in the system with the same alias. So hold on a sec.
- Go back to the tab with the current wr-future-update bug, and click on the edit
button. Make the following changes:
(a) Assign the bug to yourself.
(b) Clear the "Alias" field.
(c) Remove bugs from the "Blocks" field that you kept in step (d), other than
1311790 and 1386670. In other words, update the "Blocks" field so that it
contains 1311790, 1386670, and any bugs that are actually fixed by the
update.
(d) Submit your changes to this bug.
- Now you can submit your changes to the clone bug which will create a new
wr-future-update bug.
- Update your patch queue so that the patches are properly formatted with
bug number, reviewer, etc. and push to MozReview. This is kind of important,
because you want these patches to land on autoland rather than inbound. If it
lands on inbound there's a high chance of it conflicting with the servo-vcs-sync
bot that is regularly pushing to autoland, and then you'll only find out about
it when the sheriff tries to do a merge and backs you out. If you push to
autoland you're likely to find out about the problem at push time, when the
patches won't rebase.
Troubleshooting tips:
1. Note that when webrender is built as part of gecko, it may end up using slightly
different versions of its dependencies than when it is built standalone from the
webrender repo. The reason is that the Cargo.lock files in m-c and in the WR
repo may reference different versions of the dependencies. Both builds will be
compatible in terms of semantic versioning, but may produce different results -
for example the standalone webrender might use euclid 0.10.4 while the
one in gecko uses euclid 0.10.3. Although both choices are "valid" per
the semantic versioning rules in webrender's Cargo.toml, the 0.2.3 may provide
a bugfix that is needed for correct behaviour in webrender. If this is the case,
the technically "correct" fix is to change the upstream webrender Cargo.toml
file to require the correct version. Alternnatively, you can update the
Cargo.lock files in m-c to pull in the new version. The way to do this is as
follows:
- Go to toolkit/library/rust and run |cargo update -p <package> --precise <version>|.
Repeat this for as many libraries as you need to update. Run the same commands
in toolkit/library/gtest/rust and js/src (ignore any errors about unmatched
packages). Commit all the changes locally.
- Run |mach vendor rust|, which will update the corresponding libraries in
third_party/rust to the versions you specified.
The reason we don't do this by default is to work around bug 1336528. Specifically,
there is another crate in m-c called mozjs_sys which is built separately but uses
the same folder to store its rust dependencies. If one of the libraries that is
required by both mozjs_sys and webrender is updated without updating the other
project's Cargo.lock file, that results in build bustage.
This means that any time you do this sort of manual update of packages, you need
to make sure that mozjs_sys also has its Cargo.lock file updated if needed, hence
the need to run the cargo update command in js/src as well. Hopefully this will
be resolved soon.
2. Sometimes autoland tip has changed enough from mozilla-central (because of the
servo vcs-sync-bot, which will sync servo into m-c and often re-vendor third-
party rust dependencies) that trying to land an update based on mozilla-central
will not work well. As in, you'll get conflicts in Cargo.lock files or in the
third_party/rust directory. This is best handled by running your update steps
on top of autoland tip rather than central. (The script-based update in option A
has an env var you can set to do this). In theory you can get the same
result by resolving the conflict manually but Cargo.lock files are usually not
trivial to merge by hand. If it's just the third_party/rust dir that has conflicts
you can delete it and run |mach vendor rust| again to repopulate it.
-------------------------------------------------------------------------------
The current WebRender revision can be found in gfx/webrender_bindings/revision.txt file.

View File

@ -1 +0,0 @@
d3edc30cf95d3c96fd8308969b22062698a0f6ce

View File

@ -1699,8 +1699,6 @@ pub extern "C" fn wr_api_capture(
let _ = create_dir_all(&path);
match File::create(path.join("wr.txt")) {
Ok(mut file) => {
let revision = include_bytes!("../revision.txt");
file.write(revision).unwrap();
// The Gecko HG revision is available at compile time
if let Some(moz_revision) = option_env!("GECKO_HEAD_REV") {
writeln!(file, "mozilla-central {}", moz_revision).unwrap();

View File

@ -1,6 +1,17 @@
# WebRender
GPU renderer for the Web content, used by Servo.
Note that the canonical home for this code is in gfx/wr folder of the
mozilla-central repository at https://hg.mozilla.org/mozilla-central. The
Github repository at https://github.com/servo/webrender should be considered
a downstream mirror, although it contains additional metadata (such as Github
wiki pages) that do not exist in mozilla-central. Pull requests against the
Github repository are still being accepted, although once reviewed, they will
be landed on mozilla-central first and then mirrored back. If you are familiar
with the mozilla-central contribution workflow, filing bugs in
[Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?product=Core&component=Graphics%3A%20WebRender)
and submitting patches there would be preferred.
## Update as a Dependency
After updating shaders in WebRender, go to servo and:

View File

@ -2266,14 +2266,16 @@ PresShell::IntraLineMove(bool aForward, bool aExtend) {
NS_IMETHODIMP
PresShell::PageMove(bool aForward, bool aExtend) {
nsIFrame* frame;
nsIFrame* frame = nullptr;
if (!aExtend) {
frame = do_QueryFrame(GetScrollableFrameToScroll(nsIPresShell::eVertical));
} else {
frame = mSelection->GetFrameToPageSelect();
// If there is no scrollable frame, get the frame to move caret instead.
}
if (!frame) {
return NS_OK;
frame = mSelection->GetFrameToPageSelect();
if (!frame) {
return NS_OK;
}
}
RefPtr<nsFrameSelection> frameSelection = mSelection;
frameSelection->CommonPageMove(aForward, aExtend, frame);

View File

@ -151,8 +151,6 @@ skip-if = e10s # bug 1020135, nested oop iframes not supported
support-files = bug921928_event_target_iframe_apps_oop.html
[test_event_target_radius.html]
skip-if = toolkit == 'android' # Bug 1355836
[test_expanding_selection_per_page.html]
support-files = window_empty_document.html
[test_flush_on_paint.html]
skip-if = true # Bug 688128
[test_frame_reconstruction_for_pseudo_elements.html]
@ -163,6 +161,8 @@ support-files =
file_getBoxQuads_convertPointRectQuad_frame1.html
file_getBoxQuads_convertPointRectQuad_frame2.html
[test_getClientRects_emptytext.html]
[test_moving_and_expanding_selection_per_page.html]
support-files = window_empty_document.html
[test_mozPaintCount.html]
skip-if = toolkit == 'android' # android: Requires plugin support
[test_preserve3d_sorting_hit_testing.html]

View File

@ -1,311 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test for expanding selection per page</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<pre id="test">
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
addLoadEvent(() => {
open("window_empty_document.html", "_blank", "width=500,height=500");
});
async function doTests(aWindow) {
const IS_WIN = navigator.platform.includes("Win");
// On macOS and Linux, Shift + PageUp/PageDown requires native event to
// resolve default action of PageDown and PageUp. Although macOS widget has
// nsIWidget::AttachNativeKeyEvent(), we cannot use synthesizeKey() for the
// following tests. So, use nsISelectionController.pageMove() instead on
// non-Windows platforms.
const kUseKeyboardEvent = IS_WIN;
let selectionController;
if (!kUseKeyboardEvent) {
selectionController = SpecialPowers.wrap(aWindow)
.docShell
.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
.getInterface(SpecialPowers.Ci.nsISelectionDisplay)
.QueryInterface(SpecialPowers.Ci.nsISelectionController);
}
// On Windows, per-page selection to start or end expands selection to same
// column of first or last line. On the other platforms, it expands selection
// to start or end of first or last line.
const kSelectToStartOrEnd = !IS_WIN;
await SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false]]});
await SimpleTest.promiseFocus(aWindow);
function getNodeDescription(aNode) {
function getElementDescription(aElement) {
if (aElement.getAttribute("id") !== null) {
return `${aElement.tagName.toLowerCase()}#${aElement.getAttribute("id")}`;
}
if (aElement.tagName === "BR") {
return `${getElementDescription(aElement.previousSibling)} + br`;
}
return aElement.tagName.toLowerCase();
}
switch (aNode.nodeType) {
case aNode.TEXT_NODE:
return `text node in ${getElementDescription(aNode.parentElement)}`;
case aNode.ELEMENT_NODE:
return getElementDescription(aNode);
default:
return "unknown node";
}
}
function doSelectPageDown() {
if (kUseKeyboardEvent) {
synthesizeKey("KEY_PageDown", {shiftKey: true}, aWindow);
} else {
selectionController.pageMove(true, true);
}
}
function doSelectPageUp() {
if (kUseKeyboardEvent) {
synthesizeKey("KEY_PageUp", {shiftKey: true}, aWindow);
} else {
selectionController.pageMove(false, true);
}
}
let doc = aWindow.document;
let body = doc.body;
let selection = doc.getSelection();
let container;
body.innerHTML = '<span id="s1">first line</span><br>' +
'<span id="s2">second line</span><br>' +
'<span id="s3">last line</span>';
container = doc.documentElement;
let description = "Expanding selection to forward in non-scrollable body: ";
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s1").firstChild, 3);
doSelectPageDown();
is(container.scrollTop, 0, description + "this test shouldn't create scrollable document");
let range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s1").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
is(range.endContainer, doc.getElementById("s3").firstChild,
`${description} selection should be expanded into the last line (got: ${getNodeDescription(range.endContainer)})`);
if (kSelectToStartOrEnd) {
is(range.endOffset, range.endContainer.length,
`${description} selection should be expanded to end of the last line`);
} else {
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the last line's 3rd insertion point`);
}
description = "Expanding selection to backward in non-scrollable body: ";
selection.collapse(doc.getElementById("s3").firstChild, 3);
doSelectPageUp();
is(container.scrollTop, 0, description + "this test shouldn't create scrollable document");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s1").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
if (kSelectToStartOrEnd) {
is(range.startOffset, 0,
`${description} selection should be expanded to start of the first line`);
} else {
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
}
is(range.endContainer, doc.getElementById("s3").firstChild,
`${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the last line's 3rd insertion point`);
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" style="height: 2em; line-height: 1em; overflow: auto;">' +
'<span id="s2">first line</span><br>' +
'<span id="s3">second line</span><br>' +
'<span id="s4">third line</span><br>' +
'<span id="s5">last line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d1");
description = "Expanding selection to forward in scrollable area in the body: ";
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s2").firstChild, 3);
doSelectPageDown();
isnot(container.scrollTop, 0, description + "should be scrolled down");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded into the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the 3rd line's 3rd insertion point`);
description = "Expanding selection to backward in scrollable area in the body: ";
selection.collapse(doc.getElementById("s4").firstChild, 3);
let previousScrollTop = container.scrollTop;
doSelectPageUp();
ok(container.scrollTop < previousScrollTop, description + "should be scrolled up");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded from the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the 3rd line's 3rd insertion point`);
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" contenteditable style="height: 2em; line-height: 1em; overflow: auto;">' +
'<span id="s2">first line</span><br>' +
'<span id="s3">second line</span><br>' +
'<span id="s4">third line</span><br>' +
'<span id="s5">last line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d1");
description = "Expanding selection to forward in scrollable editable div in the body: ";
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s2").firstChild, 3);
doSelectPageDown();
isnot(container.scrollTop, 0, description + "should be scrolled down");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded into the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the 3rd line's 3rd insertion point`);
description = "Expanding selection to backward in scrollable editable div in the body: ";
selection.collapse(doc.getElementById("s4").firstChild, 3);
previousScrollTop = container.scrollTop;
doSelectPageUp();
ok(container.scrollTop < previousScrollTop, description + "should be scrolled up");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded from the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the 3rd line's 3rd insertion point`);
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" contenteditable>' +
'<span id="s2">first line</span><br>' +
'<span id="s3">second line</span><br>' +
'<span id="s4">third line</span><br>' +
'<span id="s5">last line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d1");
description = "Expanding selection to forward in non-scrollable editable div in the body: ";
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s2").firstChild, 3);
doSelectPageDown();
is(container.scrollTop, 0, description + "editable div shouldn't be scrollable");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
is(range.endContainer, doc.getElementById("s5").firstChild,
`${description} selection should be expanded into the last line (got: ${getNodeDescription(range.endContainer)})`);
if (kSelectToStartOrEnd) {
is(range.endOffset, range.endContainer.length,
`${description} selection should be expanded to end of the last line`);
} else {
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the last line's 3rd insertion point`);
}
description = "Expanding selection to backward in non-scrollable editable div in the body: ";
selection.collapse(doc.getElementById("s5").firstChild, 3);
doSelectPageUp();
is(container.scrollTop, 0, description + "editable div shouldn't be scrollable");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
if (kSelectToStartOrEnd) {
is(range.startOffset, 0,
`${description} selection should be expanded to start of the first line`);
} else {
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
}
is(range.endContainer, doc.getElementById("s5").firstChild,
`${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the last line's 3rd insertion point`);
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" contenteditable>' +
'<span id="s2">first editable line</span><br>' +
'<div id="d2" style="height: 3em; line-height: 1em; overflow: auto;">' +
'<span id="s3">first line</span><br>' +
'<span id="s4">second line</span>' +
"</div>" +
'<span id="s5">last editable line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d2");
description = "Expanding selection to forward in scrollable div (but not scrollable along y-axis) in the editable div: ";
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s3").firstChild, 3);
doSelectPageDown();
is(container.scrollTop, 0, description + "scrollable div in the editable div (but not scrollable along y-axis) shouldn't be scrollable");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s3").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
is(range.endContainer, doc.getElementById("s5").firstChild,
`${description} selection should be expanded into the last editable line (got: ${getNodeDescription(range.endContainer)})`);
if (kSelectToStartOrEnd) {
is(range.endOffset, range.endContainer.length,
`${description} selection should be expanded to end of the last editable line`);
} else {
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the last editable line's 3rd insertion point`);
}
description = "Expanding selection to backward in scrollable div (but not scrollable along y-axis) in the editable div: ";
selection.collapse(doc.getElementById("s4").firstChild, 3);
doSelectPageUp();
is(container.scrollTop, 0, description + "scrollable div (but not scrollable along y-axis) in the editable div shouldn't be scrollable");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first editable line (got: ${getNodeDescription(range.startContainer)})`);
if (kSelectToStartOrEnd) {
is(range.startOffset, 0,
`${description} selection should be expanded to start of the first editable line`);
} else {
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first editable line's 3rd insertion point`);
}
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the last line's 3rd insertion point`);
aWindow.close();
SimpleTest.finish();
}
</script>
</html>

View File

@ -0,0 +1,357 @@
<!DOCTYPE html>
<html>
<head>
<title>Test for expanding selection per page</title>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<pre id="test">
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
addLoadEvent(() => {
open("window_empty_document.html", "_blank", "width=500,height=500");
});
async function doTests(aWindow) {
const IS_WIN = navigator.platform.includes("Win");
// On macOS and Linux, Shift + PageUp/PageDown requires native event to
// resolve default action of PageDown and PageUp. Although macOS widget has
// nsIWidget::AttachNativeKeyEvent(), we cannot use synthesizeKey() for the
// following tests. So, use nsISelectionController.pageMove() instead on
// non-Windows platforms.
const kUseKeyboardEvent = IS_WIN;
let selectionController = SpecialPowers.wrap(aWindow)
.docShell
.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
.getInterface(SpecialPowers.Ci.nsISelectionDisplay)
.QueryInterface(SpecialPowers.Ci.nsISelectionController);
// On Windows, per-page selection to start or end expands selection to same
// column of first or last line. On the other platforms, it expands selection
// to start or end of first or last line.
const kSelectToStartOrEnd = !IS_WIN;
await SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false]]});
await SimpleTest.promiseFocus(aWindow);
function getNodeDescription(aNode) {
function getElementDescription(aElement) {
if (aElement.getAttribute("id") !== null) {
return `${aElement.tagName.toLowerCase()}#${aElement.getAttribute("id")}`;
}
if (aElement.tagName === "BR") {
return `${getElementDescription(aElement.previousSibling)} + br`;
}
return aElement.tagName.toLowerCase();
}
switch (aNode.nodeType) {
case aNode.TEXT_NODE:
return `text node in ${getElementDescription(aNode.parentElement)}`;
case aNode.ELEMENT_NODE:
return getElementDescription(aNode);
default:
return "unknown node";
}
}
function doTest(aExpandSelection) {
// Note that when neither editor has focus nor in caret mode, key navigation
// does not call nsISelectionController::PageMove(). Therefore, in such
// cases, you need to call doPageDown() and doPageUp() with setting true
// to aUseSelectionController.
function doPageDown(aUseSelectionController) {
if (kUseKeyboardEvent && !aUseSelectionController) {
synthesizeKey("KEY_PageDown", {shiftKey: aExpandSelection}, aWindow);
} else {
selectionController.pageMove(true, aExpandSelection);
}
}
function doPageUp(aUseSelectionController) {
if (kUseKeyboardEvent && !aUseSelectionController) {
synthesizeKey("KEY_PageUp", {shiftKey: aExpandSelection}, aWindow);
} else {
selectionController.pageMove(false, aExpandSelection);
}
}
let doc = aWindow.document;
let body = doc.body;
let selection = doc.getSelection();
let container;
body.innerHTML = '<span id="s1">first line</span><br>' +
'<span id="s2">second line</span><br>' +
'<span id="s3">last line</span>';
container = doc.documentElement;
let description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in non-scrollable body: `;
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s1").firstChild, 3);
doPageDown(!aExpandSelection);
is(container.scrollTop, 0, description + "this test shouldn't create scrollable document");
let range = selection.getRangeAt(0);
if (aExpandSelection) {
is(range.startContainer, doc.getElementById("s1").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
is(range.endContainer, doc.getElementById("s3").firstChild,
`${description} selection should be expanded into the last line (got: ${getNodeDescription(range.endContainer)})`);
if (kSelectToStartOrEnd) {
is(range.endOffset, range.endContainer.length,
`${description} selection should be expanded to end of the last line`);
} else {
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the last line's 3rd insertion point`);
}
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in non-scrollable body: `;
selection.collapse(doc.getElementById("s3").firstChild, 3);
doPageUp(!aExpandSelection);
is(container.scrollTop, 0, description + "this test shouldn't create scrollable document");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s1").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
if (kSelectToStartOrEnd) {
is(range.startOffset, 0,
`${description} selection should be expanded to start of the first line`);
} else {
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
}
if (aExpandSelection) {
is(range.endContainer, doc.getElementById("s3").firstChild,
`${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the last line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" style="height: 2em; line-height: 1em; overflow: auto;">' +
'<span id="s2">first line</span><br>' +
'<span id="s3">second line</span><br>' +
'<span id="s4">third line</span><br>' +
'<span id="s5">last line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d1");
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in scrollable area in the body: `;
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s2").firstChild, 3);
doPageDown(!aExpandSelection);
isnot(container.scrollTop, 0, description + "should be scrolled down");
range = selection.getRangeAt(0);
if (aExpandSelection) {
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded into the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the 3rd line's 3rd insertion point`);
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in scrollable area in the body: `;
selection.collapse(doc.getElementById("s4").firstChild, 3);
let previousScrollTop = container.scrollTop;
doPageUp(!aExpandSelection);
ok(container.scrollTop < previousScrollTop, description + "should be scrolled up");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
if (aExpandSelection) {
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded from the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the 3rd line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" contenteditable style="height: 2em; line-height: 1em; overflow: auto;">' +
'<span id="s2">first line</span><br>' +
'<span id="s3">second line</span><br>' +
'<span id="s4">third line</span><br>' +
'<span id="s5">last line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d1");
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in scrollable editable div in the body: `;
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s2").firstChild, 3);
doPageDown();
isnot(container.scrollTop, 0, description + "should be scrolled down");
range = selection.getRangeAt(0);
if (aExpandSelection) {
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded into the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the 3rd line's 3rd insertion point`);
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in scrollable editable div in the body: `;
selection.collapse(doc.getElementById("s4").firstChild, 3);
previousScrollTop = container.scrollTop;
doPageUp();
ok(container.scrollTop < previousScrollTop, description + "should be scrolled up");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
if (aExpandSelection) {
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded from the 3rd line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the 3rd line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" contenteditable>' +
'<span id="s2">first line</span><br>' +
'<span id="s3">second line</span><br>' +
'<span id="s4">third line</span><br>' +
'<span id="s5">last line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d1");
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in non-scrollable editable div in the body: `;
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s2").firstChild, 3);
doPageDown();
is(container.scrollTop, 0, description + "editable div shouldn't be scrollable");
range = selection.getRangeAt(0);
if (aExpandSelection) {
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
is(range.endContainer, doc.getElementById("s5").firstChild,
`${description} selection should be expanded into the last line (got: ${getNodeDescription(range.endContainer)})`);
if (kSelectToStartOrEnd) {
is(range.endOffset, range.endContainer.length,
`${description} selection should be expanded to end of the last line`);
} else {
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the last line's 3rd insertion point`);
}
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in non-scrollable editable div in the body: `;
selection.collapse(doc.getElementById("s5").firstChild, 3);
doPageUp();
is(container.scrollTop, 0, description + "editable div shouldn't be scrollable");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`);
if (kSelectToStartOrEnd) {
is(range.startOffset, 0,
`${description} selection should be expanded to start of the first line`);
} else {
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first line's 3rd insertion point`);
}
if (aExpandSelection) {
is(range.endContainer, doc.getElementById("s5").firstChild,
`${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the last line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
body.innerHTML = '<span id="s1">first line in the body</span>' +
'<div id="d1" contenteditable>' +
'<span id="s2">first editable line</span><br>' +
'<div id="d2" style="height: 3em; line-height: 1em; overflow: auto;">' +
'<span id="s3">first line</span><br>' +
'<span id="s4">second line</span>' +
"</div>" +
'<span id="s5">last editable line</span>' +
"</div>" +
'<span id="s6">last line in the body</span>';
container = doc.getElementById("d2");
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in scrollable div (but not scrollable along y-axis) in the editable div: `;
is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization");
selection.collapse(doc.getElementById("s3").firstChild, 3);
doPageDown();
is(container.scrollTop, 0, description + "scrollable div in the editable div (but not scrollable along y-axis) shouldn't be scrollable");
range = selection.getRangeAt(0);
if (aExpandSelection) {
is(range.startContainer, doc.getElementById("s3").firstChild,
`${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`);
is(range.startOffset, 3,
`${description} selection should be expanded from the first line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
is(range.endContainer, doc.getElementById("s5").firstChild,
`${description} selection should be expanded into the last editable line (got: ${getNodeDescription(range.endContainer)})`);
if (kSelectToStartOrEnd) {
is(range.endOffset, range.endContainer.length,
`${description} selection should be expanded to end of the last editable line`);
} else {
isfuzzy(range.endOffset, 3, 2,
`${description} selection should be expanded to around the last editable line's 3rd insertion point`);
}
description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in scrollable div (but not scrollable along y-axis) in the editable div: `;
selection.collapse(doc.getElementById("s4").firstChild, 3);
doPageUp();
is(container.scrollTop, 0, description + "scrollable div (but not scrollable along y-axis) in the editable div shouldn't be scrollable");
range = selection.getRangeAt(0);
is(range.startContainer, doc.getElementById("s2").firstChild,
`${description} selection should be expanded into the first editable line (got: ${getNodeDescription(range.startContainer)})`);
if (kSelectToStartOrEnd) {
is(range.startOffset, 0,
`${description} selection should be expanded to start of the first editable line`);
} else {
isfuzzy(range.startOffset, 3, 2,
`${description} selection should be expanded to around the first editable line's 3rd insertion point`);
}
if (aExpandSelection) {
is(range.endContainer, doc.getElementById("s4").firstChild,
`${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`);
is(range.endOffset, 3,
`${description} selection should be expanded from the last line's 3rd insertion point`);
} else {
ok(range.collapsed, `${description} selection should be collapsed`);
}
}
doTest(false);
doTest(true);
aWindow.close();
SimpleTest.finish();
}
</script>
</html>

View File

@ -1647,7 +1647,7 @@ class StaticAnalysis(MachCommandBase):
"""Utilities for running C++ static analysis checks and format."""
# List of file extension to consider (should start with dot)
_format_include_extensions = ('.cpp', '.c', '.cc', '.h', '.java')
_format_include_extensions = ('.cpp', '.c', '.cc', '.h')
# File contaning all paths to exclude from formatting
_format_ignore_file = '.clang-format-ignore'

View File

@ -13,6 +13,8 @@ const {
} = ChromeUtils.import("chrome://marionette/content/error.js", {});
const {
MessageManagerDestroyedPromise,
waitForEvent,
waitForObserverTopic,
} = ChromeUtils.import("chrome://marionette/content/sync.js", {});
this.EXPORTED_SYMBOLS = ["browser", "Context", "WindowState"];
@ -70,11 +72,11 @@ this.Context = Context;
*/
browser.getBrowserForTab = function(tab) {
// Fennec
if ("browser" in tab) {
if (tab && "browser" in tab) {
return tab.browser;
// Firefox
} else if ("linkedBrowser" in tab) {
} else if (tab && "linkedBrowser" in tab) {
return tab.linkedBrowser;
}
@ -287,17 +289,58 @@ browser.Context = class {
* A promise which is resolved when the current window has been closed.
*/
closeWindow() {
return new Promise(resolve => {
// Wait for the window message manager to be destroyed
let destroyed = new MessageManagerDestroyedPromise(
this.window.messageManager);
let destroyed = new MessageManagerDestroyedPromise(
this.window.messageManager);
let unloaded = waitForEvent(this.window, "unload");
this.window.addEventListener("unload", async () => {
await destroyed;
resolve();
}, {once: true});
this.window.close();
});
this.window.close();
return Promise.all([destroyed, unloaded]);
}
/**
* Open a new browser window.
*
* @return {Promise}
* A promise resolving to the newly created chrome window.
*/
async openBrowserWindow(focus = false) {
switch (this.driver.appName) {
case "firefox":
// Open new browser window, and wait until it is fully loaded.
// Also wait for the window to be focused and activated to prevent a
// race condition when promptly focusing to the original window again.
let win = this.window.OpenBrowserWindow();
let activated = waitForEvent(win, "activate");
let focused = waitForEvent(win, "focus", {capture: true});
let startup = waitForObserverTopic("browser-delayed-startup-finished",
subject => subject == win);
// Bug 1509380 - Missing focus/activate event when Firefox is not
// the top-most application. As such wait for the next tick, and
// manually focus the newly opened window.
win.setTimeout(() => win.focus(), 0);
await Promise.all([activated, focused, startup]);
if (!focus) {
// The new window shouldn't get focused. As such set the
// focus back to the currently selected window.
activated = waitForEvent(this.window, "activate");
focused = waitForEvent(this.window, "focus", {capture: true});
this.window.focus();
await Promise.all([activated, focused]);
}
return win;
default:
throw new UnsupportedOperationError(
`openWindow() not supported in ${this.driver.appName}`);
}
}
/**
@ -319,40 +362,61 @@ browser.Context = class {
return this.closeWindow();
}
return new Promise((resolve, reject) => {
// Wait for the browser message manager to be destroyed
let browserDetached = async () => {
await new MessageManagerDestroyedPromise(this.messageManager);
resolve();
};
let destroyed = new MessageManagerDestroyedPromise(this.messageManager);
let tabClosed;
if (this.tabBrowser.closeTab) {
switch (this.driver.appName) {
case "fennec":
// Fennec
this.tabBrowser.deck.addEventListener(
"TabClose", browserDetached, {once: true});
tabClosed = waitForEvent(this.tabBrowser.deck, "TabClose");
this.tabBrowser.closeTab(this.tab);
break;
} else if (this.tabBrowser.removeTab) {
// Firefox
this.tab.addEventListener(
"TabClose", browserDetached, {once: true});
case "firefox":
tabClosed = waitForEvent(this.tab, "TabClose");
this.tabBrowser.removeTab(this.tab);
break;
} else {
reject(new UnsupportedOperationError(
`closeTab() not supported in ${this.driver.appName}`));
}
});
default:
throw new UnsupportedOperationError(
`closeTab() not supported in ${this.driver.appName}`);
}
return Promise.all([destroyed, tabClosed]);
}
/**
* Opens a tab with given URI.
*
* @param {string} uri
* URI to open.
* Open a new tab in the currently selected chrome window.
*/
addTab(uri) {
return this.tabBrowser.addTab(uri, true);
async openTab(focus = false) {
let tab = null;
let tabOpened = waitForEvent(this.window, "TabOpen");
switch (this.driver.appName) {
case "fennec":
tab = this.tabBrowser.addTab(null, {selected: focus});
break;
case "firefox":
this.window.BrowserOpenTab();
tab = this.tabBrowser.selectedTab;
// The new tab is always selected by default. If focus is not wanted,
// the previously tab needs to be selected again.
if (!focus) {
this.tabBrowser.selectedTab = this.tab;
}
break;
default:
throw new UnsupportedOperationError(
`openTab() not supported in ${this.driver.appName}`);
}
await tabOpened;
return tab;
}
/**
@ -386,16 +450,18 @@ browser.Context = class {
this.tab = this.tabBrowser.tabs[index];
if (focus) {
if (this.tabBrowser.selectTab) {
// Fennec
this.tabBrowser.selectTab(this.tab);
switch (this.driver.appName) {
case "fennec":
this.tabBrowser.selectTab(this.tab);
break;
} else if ("selectedTab" in this.tabBrowser) {
// Firefox
this.tabBrowser.selectedTab = this.tab;
case "firefox":
this.tabBrowser.selectedTab = this.tab;
break;
} else {
throw new UnsupportedOperationError("switchToTab() not supported");
default:
throw new UnsupportedOperationError(
`switchToTab() not supported in ${this.driver.appName}`);
}
}
}

View File

@ -1428,6 +1428,20 @@ class Marionette(object):
return self._send_message("WebDriver:GetPageSource",
key="value")
def open(self, type=None, focus=False):
"""Open a new window, or tab based on the specified context type.
If no context type is given the application will choose the best
option based on tab and window support.
:param type: Type of window to be opened. Can be one of "tab" or "window"
:param focus: If true, the opened window will be focused
:returns: Dict with new window handle, and type of opened window
"""
body = {"type": type, "focus": focus}
return self._send_message("WebDriver:NewWindow", body)
def close(self):
"""Close the current window, ending the session if it's the last
window currently open.

View File

@ -3,6 +3,8 @@ sync module
Provides an assortment of synchronisation primitives.
.. js:autofunction:: executeSoon
.. js:autoclass:: MessageManagerDestroyedPromise
:members:
@ -14,3 +16,9 @@ Provides an assortment of synchronisation primitives.
.. js:autoclass:: TimedPromise
:members:
.. js:autofunction:: waitForEvent
.. js:autofunction:: waitForMessage
.. js:autofunction:: waitForObserverTopic

View File

@ -62,6 +62,8 @@ const {
IdlePromise,
PollPromise,
TimedPromise,
waitForEvent,
waitForObserverTopic,
} = ChromeUtils.import("chrome://marionette/content/sync.js", {});
XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
@ -106,13 +108,12 @@ const globalMessageManager = Services.mm;
*
* @class GeckoDriver
*
* @param {string} appId
* Unique identifier of the application.
* @param {MarionetteServer} server
* The instance of Marionette server.
*/
this.GeckoDriver = function(appId, server) {
this.appId = appId;
this.GeckoDriver = function(server) {
this.appId = Services.appinfo.ID;
this.appName = Services.appinfo.name.toLowerCase();
this._server = server;
this.sessionID = null;
@ -1292,6 +1293,7 @@ GeckoDriver.prototype.getIdForBrowser = function(browser) {
if (browser === null) {
return null;
}
let permKey = browser.permanentKey;
if (this._browserIds.has(permKey)) {
return this._browserIds.get(permKey);
@ -2707,6 +2709,66 @@ GeckoDriver.prototype.deleteCookie = async function(cmd) {
}
};
/**
* Open a new top-level browsing context.
*
* @param {string=} type
* Optional type of the new top-level browsing context. Can be one of
* `tab` or `window`. Defaults to `tab`.
* @param {boolean=} focus
* Optional flag if the new top-level browsing context should be opened
* in foreground (focused) or background (not focused). Defaults to false.
*
* @return {Object.<string, string>}
* Handle and type of the new browsing context.
*/
GeckoDriver.prototype.newWindow = async function(cmd) {
assert.open(this.getCurrentWindow(Context.Content));
await this._handleUserPrompts();
let focus = false;
if (typeof cmd.parameters.focus != "undefined") {
focus = assert.boolean(cmd.parameters.focus,
pprint`Expected "focus" to be a boolean, got ${cmd.parameters.focus}`);
}
let type;
if (typeof cmd.parameters.type != "undefined") {
type = assert.string(cmd.parameters.type,
pprint`Expected "type" to be a string, got ${cmd.parameters.type}`);
}
// If an invalid or no type has been specified default to a tab.
if (typeof type == "undefined" || !["tab", "window"].includes(type)) {
type = "tab";
}
let contentBrowser;
switch (type) {
case "window":
let win = await this.curBrowser.openBrowserWindow(focus);
contentBrowser = browser.getTabBrowser(win).selectedBrowser;
break;
default:
// To not fail if a new type gets added in the future, make opening
// a new tab the default action.
let tab = await this.curBrowser.openTab(focus);
contentBrowser = browser.getBrowserForTab(tab);
}
// Even with the framescript registered, the browser might not be known to
// the parent process yet. Wait until it is available.
// TODO: Fix by using `Browser:Init` or equivalent on bug 1311041
let windowId = await new PollPromise((resolve, reject) => {
let id = this.getIdForBrowser(contentBrowser);
this.windowHandles.includes(id) ? resolve(id) : reject();
});
return {handle: windowId.toString(), type};
};
/**
* Close the currently selected tab/window.
*
@ -3086,16 +3148,14 @@ GeckoDriver.prototype.dismissDialog = async function() {
let win = assert.open(this.getCurrentWindow());
this._checkIfAlertIsPresent();
await new Promise(resolve => {
win.addEventListener("DOMModalDialogClosed", async () => {
await new IdlePromise(win);
this.dialog = null;
resolve();
}, {once: true});
let dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
let {button0, button1} = this.dialog.ui;
(button1 ? button1 : button0).click();
});
let {button0, button1} = this.dialog.ui;
(button1 ? button1 : button0).click();
await dialogClosed;
this.dialog = null;
};
/**
@ -3106,16 +3166,14 @@ GeckoDriver.prototype.acceptDialog = async function() {
let win = assert.open(this.getCurrentWindow());
this._checkIfAlertIsPresent();
await new Promise(resolve => {
win.addEventListener("DOMModalDialogClosed", async () => {
await new IdlePromise(win);
this.dialog = null;
resolve();
}, {once: true});
let dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
let {button0} = this.dialog.ui;
button0.click();
});
let {button0} = this.dialog.ui;
button0.click();
await dialogClosed;
this.dialog = null;
};
/**
@ -3286,15 +3344,10 @@ GeckoDriver.prototype.quit = async function(cmd) {
this.deleteSession();
// delay response until the application is about to quit
let quitApplication = new Promise(resolve => {
Services.obs.addObserver(
(subject, topic, data) => resolve(data),
"quit-application");
});
let quitApplication = waitForObserverTopic("quit-application");
Services.startup.quit(mode);
return {cause: await quitApplication};
return {cause: (await quitApplication).data};
};
GeckoDriver.prototype.installAddon = function(cmd) {
@ -3556,6 +3609,7 @@ GeckoDriver.prototype.commands = {
"WebDriver:MaximizeWindow": GeckoDriver.prototype.maximizeWindow,
"WebDriver:Navigate": GeckoDriver.prototype.get,
"WebDriver:NewSession": GeckoDriver.prototype.newSession,
"WebDriver:NewWindow": GeckoDriver.prototype.newWindow,
"WebDriver:PerformActions": GeckoDriver.prototype.performActions,
"WebDriver:Refresh": GeckoDriver.prototype.refresh,
"WebDriver:ReleaseActions": GeckoDriver.prototype.releaseActions,
@ -3591,5 +3645,5 @@ function restoreWindow(window) {
} else {
reject();
}
});
}, {timeout: 2000});
}

View File

@ -6,14 +6,12 @@ from __future__ import absolute_import
import sys
from marionette_driver import By, Wait
from marionette_driver import Wait
from six import reraise
class WindowManagerMixin(object):
_menu_item_new_tab = (By.ID, "menu_newNavigatorTab")
def setUp(self):
super(WindowManagerMixin, self).setUp()
@ -60,15 +58,18 @@ class WindowManagerMixin(object):
self.marionette.switch_to_window(self.start_window)
def open_tab(self, trigger="menu"):
def open_tab(self, callback=None, focus=False):
current_tabs = self.marionette.window_handles
try:
if callable(trigger):
trigger()
elif trigger == 'menu':
with self.marionette.using_context("chrome"):
self.marionette.find_element(*self._menu_item_new_tab).click()
if callable(callback):
callback()
else:
result = self.marionette.open(type="tab", focus=focus)
if result["type"] != "tab":
raise Exception(
"Newly opened browsing context is of type {} and not tab.".format(
result["type"]))
except Exception:
exc, val, tb = sys.exc_info()
reraise(exc, 'Failed to trigger opening a new tab: {}'.format(val), tb)
@ -82,8 +83,9 @@ class WindowManagerMixin(object):
return new_tab
def open_window(self, trigger=None):
def open_window(self, callback=None, focus=False):
current_windows = self.marionette.chrome_window_handles
current_tabs = self.marionette.window_handles
def loaded(handle):
with self.marionette.using_context("chrome"):
@ -95,11 +97,14 @@ class WindowManagerMixin(object):
""", script_args=[handle])
try:
if callable(trigger):
trigger()
if callable(callback):
callback()
else:
with self.marionette.using_context("chrome"):
self.marionette.execute_script("OpenBrowserWindow();")
result = self.marionette.open(type="window", focus=focus)
if result["type"] != "window":
raise Exception(
"Newly opened browsing context is of type {} and not window.".format(
result["type"]))
except Exception:
exc, val, tb = sys.exc_info()
reraise(exc, 'Failed to trigger opening a new window: {}'.format(val), tb)
@ -116,9 +121,16 @@ class WindowManagerMixin(object):
lambda _: loaded(new_window),
message="Window with handle '{}'' did not finish loading".format(new_window))
return new_window
# Bug 1507771 - Return the correct handle based on the currently selected context
# as long as "WebDriver:NewWindow" is not handled separtely in chrome context
context = self.marionette._send_message("Marionette:GetContext", key="value")
if context == "chrome":
return new_window
elif context == "content":
[new_tab] = list(set(self.marionette.window_handles) - set(current_tabs))
return new_tab
def open_chrome_window(self, url):
def open_chrome_window(self, url, focus=False):
"""Open a new chrome window with the specified chrome URL.
Can be replaced with "WebDriver:NewWindow" once the command
@ -166,4 +178,5 @@ class WindowManagerMixin(object):
})();
""", script_args=(url,))
return self.open_window(trigger=open_with_js)
with self.marionette.using_context("chrome"):
return self.open_window(callback=open_with_js, focus=focus)

View File

@ -1,22 +1,5 @@
#Copyright 2007-2009 WebDriver committers
#Copyright 2007-2009 Google Inc.
#
#Licensed under the Apache License, Version 2.0 (the "License");
#you may not use this file except in compliance with the License.
#You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.
from __future__ import absolute_import
from marionette_driver import By
from marionette_harness import MarionetteTestCase, WindowManagerMixin
@ -25,18 +8,13 @@ class ChromeTests(WindowManagerMixin, MarionetteTestCase):
def setUp(self):
super(ChromeTests, self).setUp()
self.marionette.set_context('chrome')
def tearDown(self):
self.close_all_windows()
super(ChromeTests, self).tearDown()
def test_hang_until_timeout(self):
def open_with_menu():
menu = self.marionette.find_element(By.ID, 'aboutName')
menu.click()
new_window = self.open_window(trigger=open_with_menu)
with self.marionette.using_context("chrome"):
new_window = self.open_window()
self.marionette.switch_to_window(new_window)
try:
@ -45,7 +23,8 @@ class ChromeTests(WindowManagerMixin, MarionetteTestCase):
# while running this test. Otherwise it would mask eg. IOError as
# thrown for a socket timeout.
raise NotImplementedError('Exception should not cause a hang when '
'closing the chrome window')
'closing the chrome window in content '
'context')
finally:
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)

View File

@ -449,20 +449,16 @@ class TestClickCloseContext(WindowManagerMixin, MarionetteTestCase):
super(TestClickCloseContext, self).tearDown()
def test_click_close_tab(self):
self.marionette.navigate(self.marionette.absolute_url("windowHandles.html"))
tab = self.open_tab(
lambda: self.marionette.find_element(By.ID, "new-tab").click())
self.marionette.switch_to_window(tab)
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
self.marionette.navigate(self.test_page)
self.marionette.find_element(By.ID, "close-window").click()
@skip_if_mobile("Fennec doesn't support other chrome windows")
def test_click_close_window(self):
self.marionette.navigate(self.marionette.absolute_url("windowHandles.html"))
win = self.open_window(
lambda: self.marionette.find_element(By.ID, "new-window").click())
self.marionette.switch_to_window(win)
new_tab = self.open_window()
self.marionette.switch_to_window(new_tab)
self.marionette.navigate(self.test_page)
self.marionette.find_element(By.ID, "close-window").click()

View File

@ -77,22 +77,3 @@ class TestKeyActions(WindowManagerMixin, MarionetteTestCase):
.key_down("x")
.perform())
self.assertEqual(self.key_reporter_value, "")
@skip_if_mobile("Interacting with chrome windows not available for Fennec")
def test_open_in_new_window_shortcut(self):
def open_window_with_action():
el = self.marionette.find_element(By.TAG_NAME, "a")
(self.key_action.key_down(Keys.SHIFT)
.press(el)
.release()
.key_up(Keys.SHIFT)
.perform())
self.marionette.navigate(inline("<a href='#'>Click</a>"))
new_window = self.open_window(trigger=open_window_with_action)
self.marionette.switch_to_window(new_window)
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)

View File

@ -54,13 +54,8 @@ class BaseNavigationTestCase(WindowManagerMixin, MarionetteTestCase):
else:
self.mod_key = Keys.CONTROL
def open_with_link():
link = self.marionette.find_element(By.ID, "new-blank-tab")
link.click()
# Always use a blank new tab for an empty history
self.marionette.navigate(self.marionette.absolute_url("windowHandles.html"))
self.new_tab = self.open_tab(open_with_link)
self.new_tab = self.open_tab()
self.marionette.switch_to_window(self.new_tab)
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda _: self.history_length == 1,
@ -297,7 +292,6 @@ class TestNavigate(BaseNavigationTestCase):
focus_el = self.marionette.find_element(By.CSS_SELECTOR, ":focus")
self.assertEqual(self.marionette.get_active_element(), focus_el)
@skip_if_mobile("Needs application independent method to open a new tab")
def test_no_hang_when_navigating_after_closing_original_tab(self):
# Close the start tab
self.marionette.switch_to_window(self.start_tab)
@ -338,22 +332,6 @@ class TestNavigate(BaseNavigationTestCase):
message="'{}' hasn't been loaded".format(self.test_page_remote))
self.assertTrue(self.is_remote_tab)
@skip_if_mobile("On Android no shortcuts are available")
def test_navigate_shortcut_key(self):
def open_with_shortcut():
self.marionette.navigate(self.test_page_remote)
with self.marionette.using_context("chrome"):
main_win = self.marionette.find_element(By.ID, "main-window")
main_win.send_keys(self.mod_key, Keys.SHIFT, "a")
new_tab = self.open_tab(trigger=open_with_shortcut)
self.marionette.switch_to_window(new_tab)
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda mn: mn.get_url() == "about:addons",
message="'about:addons' hasn't been loaded")
class TestBackForwardNavigation(BaseNavigationTestCase):

View File

@ -253,7 +253,8 @@ class TestScreenCaptureChrome(WindowManagerMixin, ScreenCaptureTestCase):
chrome_document_element = self.document_element
with self.marionette.using_context('content'):
self.assertRaisesRegexp(NoSuchElementException, "Web element reference not seen before",
self.assertRaisesRegexp(NoSuchElementException,
"Web element reference not seen before",
self.marionette.screenshot,
highlights=[chrome_document_element])
@ -274,10 +275,9 @@ class TestScreenCaptureContent(WindowManagerMixin, ScreenCaptureTestCase):
return [document.body.scrollWidth, document.body.scrollHeight]
"""))
@skip_if_mobile("Needs application independent method to open a new tab")
def test_capture_tab_already_closed(self):
tab = self.open_tab()
self.marionette.switch_to_window(tab)
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
self.marionette.close()
self.assertRaises(NoSuchWindowException, self.marionette.screenshot)

View File

@ -6,9 +6,8 @@ from __future__ import absolute_import
import os
import sys
from unittest import skipIf
from marionette_driver import By
from unittest import skipIf
# add this directory to the path
sys.path.append(os.path.dirname(__file__))
@ -28,119 +27,70 @@ class TestSwitchWindowChrome(TestSwitchToWindowContent):
super(TestSwitchWindowChrome, self).tearDown()
def open_window_in_background(self):
with self.marionette.using_context("chrome"):
self.marionette.execute_async_script("""
let callback = arguments[0];
(async function() {
function promiseEvent(target, type, args) {
return new Promise(r => {
let params = Object.assign({once: true}, args);
target.addEventListener(type, r, params);
});
}
function promiseWindowFocus(w) {
return Promise.all([
promiseEvent(w, "focus", {capture: true}),
promiseEvent(w, "activate"),
]);
}
// Open a window, wait for it to receive focus
let win = OpenBrowserWindow();
await promiseWindowFocus(win);
// Now refocus our original window and wait for that to happen.
let windowFocusPromise = promiseWindowFocus(window);
window.focus();
return windowFocusPromise;
})().then(() => {
// can't just pass `callback`, as we can't JSON-ify the events it'd get passed.
callback()
});
""")
def open_window_in_foreground(self):
with self.marionette.using_context("content"):
self.marionette.navigate(self.test_page)
link = self.marionette.find_element(By.ID, "new-window")
link.click()
@skipIf(sys.platform.startswith("linux"),
"Bug 1511970 - New window isn't moved to the background on Linux")
def test_switch_tabs_for_new_background_window_without_focus_change(self):
# Open an addition tab in the original window so we can better check
# Open an additional tab in the original window so we can better check
# the selected index in thew new window to be opened.
second_tab = self.open_tab(trigger=self.open_tab_in_foreground)
second_tab = self.open_tab(focus=True)
self.marionette.switch_to_window(second_tab, focus=True)
second_tab_index = self.get_selected_tab_index()
self.assertNotEqual(second_tab_index, self.selected_tab_index)
# Opens a new background window, but we are interested in the tab
tab_in_new_window = self.open_tab(trigger=self.open_window_in_background)
# Open a new background window, but we are interested in the tab
with self.marionette.using_context("content"):
tab_in_new_window = self.open_window()
self.assertEqual(self.marionette.current_window_handle, second_tab)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.assertEqual(self.get_selected_tab_index(), second_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.empty_page)
# Switch to the tab in the new window but don't focus it
self.marionette.switch_to_window(tab_in_new_window, focus=False)
self.assertEqual(self.marionette.current_window_handle, tab_in_new_window)
self.assertNotEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.assertEqual(self.get_selected_tab_index(), second_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), "about:blank")
def test_switch_tabs_for_new_foreground_window_with_focus_change(self):
# Open an addition tab in the original window so we can better check
# the selected index in thew new window to be opened.
second_tab = self.open_tab(trigger=self.open_tab_in_foreground)
second_tab = self.open_tab()
self.marionette.switch_to_window(second_tab, focus=True)
second_tab_index = self.get_selected_tab_index()
self.assertNotEqual(second_tab_index, self.selected_tab_index)
# Opens a new window, but we are interested in the tab
tab_in_new_window = self.open_tab(trigger=self.open_window_in_foreground)
with self.marionette.using_context("content"):
tab_in_new_window = self.open_window(focus=True)
self.assertEqual(self.marionette.current_window_handle, second_tab)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
self.marionette.switch_to_window(tab_in_new_window)
self.assertEqual(self.marionette.current_window_handle, tab_in_new_window)
self.assertNotEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.empty_page)
self.marionette.switch_to_window(second_tab, focus=True)
self.assertEqual(self.marionette.current_window_handle, second_tab)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
# Bug 1335085 - The focus doesn't change even as requested so.
# self.assertEqual(self.get_selected_tab_index(), second_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
def test_switch_tabs_for_new_foreground_window_without_focus_change(self):
# Open an addition tab in the original window so we can better check
# the selected index in thew new window to be opened.
second_tab = self.open_tab(trigger=self.open_tab_in_foreground)
second_tab = self.open_tab()
self.marionette.switch_to_window(second_tab, focus=True)
second_tab_index = self.get_selected_tab_index()
self.assertNotEqual(second_tab_index, self.selected_tab_index)
# Opens a new window, but we are interested in the tab which automatically
# gets the focus.
self.open_tab(trigger=self.open_window_in_foreground)
self.open_window(focus=True)
self.assertEqual(self.marionette.current_window_handle, second_tab)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
# Switch to the second tab in the first window, but don't focus it.
self.marionette.switch_to_window(second_tab, focus=False)
self.assertEqual(self.marionette.current_window_handle, second_tab)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.assertNotEqual(self.get_selected_tab_index(), second_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)

View File

@ -4,10 +4,10 @@
from __future__ import absolute_import
from marionette_driver import Actions, By, Wait
from marionette_driver import By
from marionette_driver.keys import Keys
from marionette_harness import MarionetteTestCase, skip_if_mobile, WindowManagerMixin
from marionette_harness import MarionetteTestCase, WindowManagerMixin
class TestSwitchToWindowContent(WindowManagerMixin, MarionetteTestCase):
@ -20,14 +20,8 @@ class TestSwitchToWindowContent(WindowManagerMixin, MarionetteTestCase):
else:
self.mod_key = Keys.CONTROL
self.empty_page = self.marionette.absolute_url("empty.html")
self.test_page = self.marionette.absolute_url("windowHandles.html")
self.selected_tab_index = self.get_selected_tab_index()
with self.marionette.using_context("content"):
self.marionette.navigate(self.test_page)
def tearDown(self):
self.close_all_tabs()
@ -69,78 +63,51 @@ class TestSwitchToWindowContent(WindowManagerMixin, MarionetteTestCase):
}
""")
def open_tab_in_background(self):
with self.marionette.using_context("content"):
link = self.marionette.find_element(By.ID, "new-tab")
action = Actions(self.marionette)
action.key_down(self.mod_key).click(link).perform()
def open_tab_in_foreground(self):
with self.marionette.using_context("content"):
link = self.marionette.find_element(By.ID, "new-tab")
link.click()
def test_switch_tabs_with_focus_change(self):
new_tab = self.open_tab(self.open_tab_in_foreground)
new_tab = self.open_tab(focus=True)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
# Switch to new tab first because it is already selected
self.marionette.switch_to_window(new_tab)
self.assertEqual(self.marionette.current_window_handle, new_tab)
self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
with self.marionette.using_context("content"):
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda _: self.marionette.get_url() == self.empty_page,
message="{} has been loaded in the newly opened tab.".format(self.empty_page))
# Switch to original tab by explicitely setting the focus
self.marionette.switch_to_window(self.start_tab, focus=True)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
self.marionette.switch_to_window(new_tab)
self.marionette.close()
self.marionette.switch_to_window(self.start_tab)
self.marionette.switch_to_window(self.start_tab)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
def test_switch_tabs_without_focus_change(self):
new_tab = self.open_tab(self.open_tab_in_foreground)
new_tab = self.open_tab(focus=True)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
# Switch to new tab first because it is already selected
self.marionette.switch_to_window(new_tab)
self.assertEqual(self.marionette.current_window_handle, new_tab)
# Switch to original tab by explicitely not setting the focus
self.marionette.switch_to_window(self.start_tab, focus=False)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
self.marionette.switch_to_window(new_tab)
self.marionette.close()
self.marionette.switch_to_window(self.start_tab)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
def test_switch_from_content_to_chrome_window_should_not_change_selected_tab(self):
new_tab = self.open_tab(self.open_tab_in_foreground)
new_tab = self.open_tab(focus=True)
self.marionette.switch_to_window(new_tab)
self.assertEqual(self.marionette.current_window_handle, new_tab)
@ -150,24 +117,31 @@ class TestSwitchToWindowContent(WindowManagerMixin, MarionetteTestCase):
self.assertEqual(self.marionette.current_window_handle, new_tab)
self.assertEqual(self.get_selected_tab_index(), new_tab_index)
@skip_if_mobile("New windows not supported in Fennec")
def test_switch_to_new_private_browsing_window_has_to_register_browsers(self):
def test_switch_to_new_private_browsing_tab(self):
# Test that tabs (browsers) are correctly registered for a newly opened
# private browsing window. This has to also happen without explicitely
# private browsing window/tab. This has to also happen without explicitely
# switching to the tab itself before using any commands in content scope.
#
# Note: Not sure why this only affects private browsing windows only.
new_tab = self.open_tab(focus=True)
self.marionette.switch_to_window(new_tab)
def open_private_browsing_window():
def open_private_browsing_window_firefox():
with self.marionette.using_context("content"):
self.marionette.navigate("about:privatebrowsing")
button = self.marionette.find_element(By.ID, "startPrivateBrowsing")
button.click()
self.marionette.find_element(By.ID, "startPrivateBrowsing").click()
new_window = self.open_window(open_private_browsing_window)
self.marionette.switch_to_window(new_window)
self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
self.assertNotEqual(self.marionette.current_window_handle, self.start_tab)
def open_private_browsing_tab_fennec():
with self.marionette.using_context("content"):
self.marionette.find_element(By.ID, "newPrivateTabLink").click()
with self.marionette.using_context("content"):
self.marionette.execute_script(" return true; ")
self.marionette.navigate("about:privatebrowsing")
if self.marionette.session_capabilities["browserName"] == "fennec":
new_pb_tab = self.open_tab(open_private_browsing_tab_fennec)
else:
new_pb_tab = self.open_tab(open_private_browsing_window_firefox)
self.marionette.switch_to_window(new_pb_tab)
self.assertEqual(self.marionette.current_window_handle, new_pb_tab)
self.marionette.execute_script(" return true; ")

View File

@ -21,14 +21,14 @@ class TestCloseWindow(WindowManagerMixin, MarionetteTestCase):
super(TestCloseWindow, self).tearDown()
def test_close_chrome_window_for_browser_window(self):
win = self.open_window()
self.marionette.switch_to_window(win)
new_window = self.open_window()
self.marionette.switch_to_window(new_window)
self.assertNotIn(win, self.marionette.window_handles)
self.assertNotIn(new_window, self.marionette.window_handles)
chrome_window_handles = self.marionette.close_chrome_window()
self.assertNotIn(win, chrome_window_handles)
self.assertNotIn(new_window, chrome_window_handles)
self.assertListEqual(self.start_windows, chrome_window_handles)
self.assertNotIn(win, self.marionette.window_handles)
self.assertNotIn(new_window, self.marionette.window_handles)
def test_close_chrome_window_for_non_browser_window(self):
win = self.open_chrome_window("chrome://marionette/content/test.xul")
@ -50,20 +50,20 @@ class TestCloseWindow(WindowManagerMixin, MarionetteTestCase):
self.assertIsNotNone(self.marionette.session)
def test_close_window_for_browser_tab(self):
tab = self.open_tab()
self.marionette.switch_to_window(tab)
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
window_handles = self.marionette.close()
self.assertNotIn(tab, window_handles)
self.assertNotIn(new_tab, window_handles)
self.assertListEqual(self.start_tabs, window_handles)
def test_close_window_for_browser_window_with_single_tab(self):
win = self.open_window()
self.marionette.switch_to_window(win)
new_window = self.open_window()
self.marionette.switch_to_window(new_window)
self.assertEqual(len(self.start_tabs) + 1, len(self.marionette.window_handles))
window_handles = self.marionette.close()
self.assertNotIn(win, window_handles)
self.assertNotIn(new_window, window_handles)
self.assertListEqual(self.start_tabs, window_handles)
self.assertListEqual(self.start_windows, self.marionette.chrome_window_handles)

View File

@ -24,26 +24,27 @@ class TestCloseWindow(WindowManagerMixin, MarionetteTestCase):
@skip_if_mobile("Interacting with chrome windows not available for Fennec")
def test_close_chrome_window_for_browser_window(self):
win = self.open_window()
self.marionette.switch_to_window(win)
with self.marionette.using_context("chrome"):
new_window = self.open_window()
self.marionette.switch_to_window(new_window)
self.assertNotIn(win, self.marionette.window_handles)
self.assertIn(new_window, self.marionette.chrome_window_handles)
chrome_window_handles = self.marionette.close_chrome_window()
self.assertNotIn(win, chrome_window_handles)
self.assertNotIn(new_window, chrome_window_handles)
self.assertListEqual(self.start_windows, chrome_window_handles)
self.assertNotIn(win, self.marionette.window_handles)
self.assertNotIn(new_window, self.marionette.window_handles)
@skip_if_mobile("Interacting with chrome windows not available for Fennec")
def test_close_chrome_window_for_non_browser_window(self):
win = self.open_chrome_window("chrome://marionette/content/test.xul")
self.marionette.switch_to_window(win)
new_window = self.open_chrome_window("chrome://marionette/content/test.xul")
self.marionette.switch_to_window(new_window)
self.assertIn(win, self.marionette.chrome_window_handles)
self.assertNotIn(win, self.marionette.window_handles)
self.assertIn(new_window, self.marionette.chrome_window_handles)
self.assertNotIn(new_window, self.marionette.window_handles)
chrome_window_handles = self.marionette.close_chrome_window()
self.assertNotIn(win, chrome_window_handles)
self.assertNotIn(new_window, chrome_window_handles)
self.assertListEqual(self.start_windows, chrome_window_handles)
self.assertNotIn(win, self.marionette.window_handles)
self.assertNotIn(new_window, self.marionette.window_handles)
@skip_if_mobile("Interacting with chrome windows not available for Fennec")
def test_close_chrome_window_for_last_open_window(self):
@ -54,19 +55,17 @@ class TestCloseWindow(WindowManagerMixin, MarionetteTestCase):
self.assertListEqual([self.start_window], self.marionette.chrome_window_handles)
self.assertIsNotNone(self.marionette.session)
@skip_if_mobile("Needs application independent method to open a new tab")
def test_close_window_for_browser_tab(self):
tab = self.open_tab()
self.marionette.switch_to_window(tab)
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
window_handles = self.marionette.close()
self.assertNotIn(tab, window_handles)
self.assertNotIn(new_tab, window_handles)
self.assertListEqual(self.start_tabs, window_handles)
@skip_if_mobile("Needs application independent method to open a new tab")
def test_close_window_with_dismissed_beforeunload_prompt(self):
tab = self.open_tab()
self.marionette.switch_to_window(tab)
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
self.marionette.navigate(inline("""
<input type="text">
@ -82,12 +81,12 @@ class TestCloseWindow(WindowManagerMixin, MarionetteTestCase):
@skip_if_mobile("Interacting with chrome windows not available for Fennec")
def test_close_window_for_browser_window_with_single_tab(self):
win = self.open_window()
self.marionette.switch_to_window(win)
new_tab = self.open_window()
self.marionette.switch_to_window(new_tab)
self.assertEqual(len(self.start_tabs) + 1, len(self.marionette.window_handles))
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
window_handles = self.marionette.close()
self.assertNotIn(win, window_handles)
self.assertNotIn(new_tab, window_handles)
self.assertListEqual(self.start_tabs, window_handles)
self.assertListEqual(self.start_windows, self.marionette.chrome_window_handles)
@ -104,8 +103,8 @@ class TestCloseWindow(WindowManagerMixin, MarionetteTestCase):
self.close_all_tabs()
test_page = self.marionette.absolute_url("windowHandles.html")
tab = self.open_tab()
self.marionette.switch_to_window(tab)
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
self.marionette.navigate(test_page)
self.marionette.switch_to_window(self.start_tab)

View File

@ -6,7 +6,7 @@ from __future__ import absolute_import
import types
from marionette_driver import By, errors, Wait
from marionette_driver import errors
from marionette_harness import MarionetteTestCase, WindowManagerMixin
@ -16,9 +16,7 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
def setUp(self):
super(TestWindowHandles, self).setUp()
self.empty_page = self.marionette.absolute_url("empty.html")
self.test_page = self.marionette.absolute_url("windowHandles.html")
self.marionette.navigate(self.test_page)
self.xul_dialog = "chrome://marionette/content/test_dialog.xul"
self.marionette.set_context("chrome")
@ -42,17 +40,16 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
self.assertIsInstance(handle, types.StringTypes)
def test_chrome_window_handles_with_scopes(self):
# Open a browser and a non-browser (about window) chrome window
self.open_window(
trigger=lambda: self.marionette.execute_script("OpenBrowserWindow();"))
new_browser = self.open_window()
self.assert_window_handles()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
self.assertIn(new_browser, self.marionette.chrome_window_handles)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.open_window(
trigger=lambda: self.marionette.find_element(By.ID, "aboutName").click())
new_dialog = self.open_chrome_window(self.xul_dialog)
self.assert_window_handles()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 2)
self.assertIn(new_dialog, self.marionette.chrome_window_handles)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
chrome_window_handles_in_chrome_scope = self.marionette.chrome_window_handles
@ -64,117 +61,112 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
self.assertEqual(self.marionette.window_handles,
window_handles_in_chrome_scope)
def test_chrome_window_handles_after_opening_new_dialog(self):
xul_dialog = "chrome://marionette/content/test_dialog.xul"
new_win = self.open_chrome_window(xul_dialog)
def test_chrome_window_handles_after_opening_new_chrome_window(self):
new_window = self.open_chrome_window(self.xul_dialog)
self.assert_window_handles()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
self.assertIn(new_window, self.marionette.chrome_window_handles)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
# Check that the new tab has the correct page loaded
self.marionette.switch_to_window(new_win)
# Check that the new chrome window has the correct URL loaded
self.marionette.switch_to_window(new_window)
self.assert_window_handles()
self.assertEqual(self.marionette.current_chrome_window_handle, new_win)
self.assertEqual(self.marionette.get_url(), xul_dialog)
self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
self.assertEqual(self.marionette.get_url(), self.xul_dialog)
# Close the opened dialog and carry on in our original tab.
# Close the chrome window, and carry on in our original window.
self.marionette.close_chrome_window()
self.assert_window_handles()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
self.assertNotIn(new_window, self.marionette.chrome_window_handles)
self.marionette.switch_to_window(self.start_window)
self.assert_window_handles()
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
def test_chrome_window_handles_after_opening_new_window(self):
def open_with_link():
with self.marionette.using_context("content"):
link = self.marionette.find_element(By.ID, "new-window")
link.click()
# We open a new window but are actually interested in the new tab
new_win = self.open_window(trigger=open_with_link)
new_window = self.open_window()
self.assert_window_handles()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
self.assertIn(new_window, self.marionette.chrome_window_handles)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
# Check that the new tab has the correct page loaded
self.marionette.switch_to_window(new_win)
self.marionette.switch_to_window(new_window)
self.assert_window_handles()
self.assertEqual(self.marionette.current_chrome_window_handle, new_win)
with self.marionette.using_context("content"):
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda mn: mn.get_url() == self.empty_page,
message="{} did not load after opening a new tab".format(self.empty_page))
self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
# Ensure navigate works in our current window
other_page = self.marionette.absolute_url("test.html")
with self.marionette.using_context("content"):
self.marionette.navigate(other_page)
self.assertEqual(self.marionette.get_url(), other_page)
# Close the opened window and carry on in our original tab.
# Close the opened window and carry on in our original window.
self.marionette.close()
self.assert_window_handles()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
self.assertNotIn(new_window, self.marionette.chrome_window_handles)
self.marionette.switch_to_window(self.start_window)
self.assert_window_handles()
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
def test_window_handles_after_opening_new_tab(self):
def open_with_link():
with self.marionette.using_context("content"):
link = self.marionette.find_element(By.ID, "new-tab")
link.click()
new_tab = self.open_tab(trigger=open_with_link)
with self.marionette.using_context("content"):
new_tab = self.open_tab()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertIn(new_tab, self.marionette.window_handles)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
with self.marionette.using_context("content"):
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda mn: mn.get_url() == self.empty_page,
message="{} did not load after opening a new tab".format(self.empty_page))
# Ensure navigate works in our current tab
other_page = self.marionette.absolute_url("test.html")
with self.marionette.using_context("content"):
self.marionette.navigate(other_page)
self.assertEqual(self.marionette.get_url(), other_page)
self.marionette.switch_to_window(self.start_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
self.marionette.switch_to_window(new_tab)
self.marionette.close()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.assertNotIn(new_tab, self.marionette.window_handles)
self.marionette.switch_to_window(self.start_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
def test_window_handles_after_opening_new_dialog(self):
xul_dialog = "chrome://marionette/content/test_dialog.xul"
new_win = self.open_chrome_window(xul_dialog)
def test_window_handles_after_opening_new_foreground_tab(self):
with self.marionette.using_context("content"):
new_tab = self.open_tab(focus=True)
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertIn(new_tab, self.marionette.window_handles)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
# We still have the default tab set as our window handle. This
# get_url command should be sent immediately, and not be forever-queued.
with self.marionette.using_context("content"):
self.marionette.get_url()
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
self.marionette.close()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.assertNotIn(new_tab, self.marionette.window_handles)
self.marionette.switch_to_window(self.start_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.marionette.switch_to_window(new_win)
def test_window_handles_after_opening_new_chrome_window(self):
new_window = self.open_chrome_window(self.xul_dialog)
self.assert_window_handles()
self.assertEqual(self.marionette.get_url(), xul_dialog)
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.assertNotIn(new_window, self.marionette.window_handles)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.marionette.switch_to_window(new_window)
self.assert_window_handles()
self.assertEqual(self.marionette.get_url(), self.xul_dialog)
# Check that the opened dialog is not accessible via window handles
with self.assertRaises(errors.NoSuchWindowException):
@ -190,111 +182,23 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
def test_window_handles_after_opening_new_window(self):
def open_with_link():
with self.marionette.using_context("content"):
link = self.marionette.find_element(By.ID, "new-window")
link.click()
# We open a new window but are actually interested in the new tab
new_tab = self.open_tab(trigger=open_with_link)
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
# Check that the new tab has the correct page loaded
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
with self.marionette.using_context("content"):
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda mn: mn.get_url() == self.empty_page,
message="{} did not load after opening a new tab".format(self.empty_page))
# Ensure navigate works in our current window
other_page = self.marionette.absolute_url("test.html")
with self.marionette.using_context("content"):
self.marionette.navigate(other_page)
self.assertEqual(self.marionette.get_url(), other_page)
# Close the opened window and carry on in our original tab.
self.marionette.close()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.marionette.switch_to_window(self.start_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
def test_window_handles_after_closing_original_tab(self):
def open_with_link():
with self.marionette.using_context("content"):
link = self.marionette.find_element(By.ID, "new-tab")
link.click()
new_tab = self.open_tab(trigger=open_with_link)
with self.marionette.using_context("content"):
new_tab = self.open_tab()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertIn(new_tab, self.marionette.window_handles)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.marionette.close()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.assertIn(new_tab, self.marionette.window_handles)
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
with self.marionette.using_context("content"):
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda mn: mn.get_url() == self.empty_page,
message="{} did not load after opening a new tab".format(self.empty_page))
def test_window_handles_no_switch(self):
"""Regression test for bug 1294456.
This test is testing the case where Marionette attempts to send a
command to a window handle when the browser has opened and selected
a new tab. Before bug 1294456 landed, the Marionette driver was getting
confused about which window handle the client cared about, and assumed
it was the window handle for the newly opened and selected tab.
This caused Marionette to think that the browser needed to do a remoteness
flip in the e10s case, since the tab opened by menu_newNavigatorTab is
about:newtab (which is currently non-remote). This meant that commands
sent to what should have been the original window handle would be
queued and never sent, since the remoteness flip in the new tab was
never going to happen.
"""
def open_with_menu():
menu_new_tab = self.marionette.find_element(By.ID, 'menu_newNavigatorTab')
menu_new_tab.click()
new_tab = self.open_tab(trigger=open_with_menu)
self.assert_window_handles()
# We still have the default tab set as our window handle. This
# get_url command should be sent immediately, and not be forever-queued.
with self.marionette.using_context("content"):
self.assertEqual(self.marionette.get_url(), self.test_page)
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
self.marionette.close()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.marionette.switch_to_window(self.start_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
def test_window_handles_after_closing_last_window(self):
self.close_all_windows()

View File

@ -7,7 +7,7 @@ from __future__ import absolute_import
import types
import urllib
from marionette_driver import By, errors, Wait
from marionette_driver import errors
from marionette_harness import MarionetteTestCase, skip_if_mobile, WindowManagerMixin
@ -21,9 +21,7 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
def setUp(self):
super(TestWindowHandles, self).setUp()
self.empty_page = self.marionette.absolute_url("empty.html")
self.test_page = self.marionette.absolute_url("windowHandles.html")
self.marionette.navigate(self.test_page)
self.xul_dialog = "chrome://marionette/content/test_dialog.xul"
def tearDown(self):
self.close_all_tabs()
@ -39,12 +37,8 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
for handle in self.marionette.window_handles:
self.assertIsInstance(handle, types.StringTypes)
def test_window_handles_after_opening_new_tab(self):
def open_with_link():
link = self.marionette.find_element(By.ID, "new-tab")
link.click()
new_tab = self.open_tab(trigger=open_with_link)
def tst_window_handles_after_opening_new_tab(self):
new_tab = self.open_tab()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
@ -52,13 +46,9 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda mn: mn.get_url() == self.empty_page,
message="{} did not load after opening a new tab".format(self.empty_page))
self.marionette.switch_to_window(self.start_tab)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertEqual(self.marionette.get_url(), self.test_page)
self.marionette.switch_to_window(new_tab)
self.marionette.close()
@ -69,29 +59,15 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
def test_window_handles_after_opening_new_browser_window(self):
def open_with_link():
link = self.marionette.find_element(By.ID, "new-window")
link.click()
# We open a new window but are actually interested in the new tab
new_tab = self.open_tab(trigger=open_with_link)
def tst_window_handles_after_opening_new_browser_window(self):
new_tab = self.open_window()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
# Check that the new tab has the correct page loaded
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
Wait(self.marionette, self.marionette.timeout.page_load).until(
lambda _: self.marionette.get_url() == self.empty_page,
message="The expected page '{}' has not been loaded".format(self.empty_page))
# Ensure navigate works in our current window
other_page = self.marionette.absolute_url("test.html")
self.marionette.navigate(other_page)
self.assertEqual(self.marionette.get_url(), other_page)
# Close the opened window and carry on in our original tab.
self.marionette.close()
@ -101,31 +77,16 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertEqual(self.marionette.get_url(), self.test_page)
@skip_if_mobile("Fennec doesn't support other chrome windows")
def test_window_handles_after_opening_new_non_browser_window(self):
def open_with_link():
self.marionette.navigate(inline("""
<a id="blob-download" download="foo.html">Download</a>
<script>
const string = "test";
const blob = new Blob([string], { type: "text/html" });
const link = document.getElementById("blob-download");
link.href = URL.createObjectURL(blob);
</script>
"""))
link = self.marionette.find_element(By.ID, "blob-download")
link.click()
new_win = self.open_window(trigger=open_with_link)
def tst_window_handles_after_opening_new_non_browser_window(self):
new_window = self.open_chrome_window(self.xul_dialog)
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertNotIn(new_window, self.marionette.window_handles)
self.marionette.switch_to_window(new_win)
self.marionette.switch_to_window(new_window)
self.assert_window_handles()
# Check that the opened window is not accessible via window handles
@ -144,26 +105,21 @@ class TestWindowHandles(WindowManagerMixin, MarionetteTestCase):
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
def test_window_handles_after_closing_original_tab(self):
def open_with_link():
link = self.marionette.find_element(By.ID, "new-tab")
link.click()
new_tab = self.open_tab(trigger=open_with_link)
new_tab = self.open_tab()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1)
self.assertEqual(self.marionette.current_window_handle, self.start_tab)
self.assertIn(new_tab, self.marionette.window_handles)
self.marionette.close()
self.assert_window_handles()
self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs))
self.assertNotIn(self.start_tab, self.marionette.window_handles)
self.marionette.switch_to_window(new_tab)
self.assert_window_handles()
self.assertEqual(self.marionette.current_window_handle, new_tab)
Wait(self.marionette, self.marionette.timeout.page_load).until(
lambda _: self.marionette.get_url() == self.empty_page,
message="The expected page '{}' has not been loaded".format(self.empty_page))
def test_window_handles_after_closing_last_tab(self):
def tst_window_handles_after_closing_last_tab(self):
self.close_all_tabs()
self.assertEqual(self.marionette.close(), [])

View File

@ -21,15 +21,9 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
@skip_if_mobile("Fennec doesn't support other chrome windows")
def test_closed_chrome_window(self):
def open_with_link():
with self.marionette.using_context("content"):
test_page = self.marionette.absolute_url("windowHandles.html")
self.marionette.navigate(test_page)
self.marionette.find_element(By.ID, "new-window").click()
win = self.open_window(open_with_link)
self.marionette.switch_to_window(win)
with self.marionette.using_context("chrome"):
new_window = self.open_window()
self.marionette.switch_to_window(new_window)
self.marionette.close_chrome_window()
# When closing a browser window both handles are not available
@ -43,12 +37,12 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_window)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(win)
self.marionette.switch_to_window(new_window)
@skip_if_mobile("Fennec doesn't support other chrome windows")
def test_closed_chrome_window_while_in_frame(self):
win = self.open_chrome_window("chrome://marionette/content/test.xul")
self.marionette.switch_to_window(win)
new_window = self.open_chrome_window("chrome://marionette/content/test.xul")
self.marionette.switch_to_window(new_window)
with self.marionette.using_context("chrome"):
self.marionette.switch_to_frame("iframe")
self.marionette.close_chrome_window()
@ -61,13 +55,12 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_window)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(win)
self.marionette.switch_to_window(new_window)
def test_closed_tab(self):
with self.marionette.using_context("content"):
tab = self.open_tab()
self.marionette.switch_to_window(tab)
self.marionette.close()
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
self.marionette.close()
# Check that only the content window is not available in both contexts
for context in ("chrome", "content"):
@ -79,25 +72,26 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_tab)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(tab)
self.marionette.switch_to_window(new_tab)
def test_closed_tab_while_in_frame(self):
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
with self.marionette.using_context("content"):
tab = self.open_tab()
self.marionette.switch_to_window(tab)
self.marionette.navigate(self.marionette.absolute_url("test_iframe.html"))
frame = self.marionette.find_element(By.ID, "test_iframe")
self.marionette.switch_to_frame(frame)
self.marionette.close()
self.marionette.close()
with self.assertRaises(NoSuchWindowException):
self.marionette.current_window_handle
self.marionette.current_chrome_window_handle
with self.assertRaises(NoSuchWindowException):
self.marionette.current_window_handle
self.marionette.current_chrome_window_handle
self.marionette.switch_to_window(self.start_tab)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(tab)
self.marionette.switch_to_window(new_tab)
class TestNoSuchWindowChrome(TestNoSuchWindowContent):
@ -121,42 +115,22 @@ class TestSwitchWindow(WindowManagerMixin, MarionetteTestCase):
self.close_all_windows()
super(TestSwitchWindow, self).tearDown()
def test_windows(self):
def open_browser_with_js():
self.marionette.execute_script(" window.open(); ")
new_window = self.open_window(trigger=open_browser_with_js)
def test_switch_window_after_open_and_close(self):
with self.marionette.using_context("chrome"):
new_window = self.open_window()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows) + 1)
self.assertIn(new_window, self.marionette.chrome_window_handles)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
# switch to the other window
# switch to the new chrome window and close it
self.marionette.switch_to_window(new_window)
self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
self.assertNotEqual(self.marionette.current_chrome_window_handle, self.start_window)
# switch back and close original window
self.marionette.close_chrome_window()
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
self.assertNotIn(new_window, self.marionette.chrome_window_handles)
# switch back to the original chrome window
self.marionette.switch_to_window(self.start_window)
self.assertEqual(self.marionette.current_chrome_window_handle, self.start_window)
self.marionette.close_chrome_window()
self.assertNotIn(self.start_window, self.marionette.chrome_window_handles)
self.assertEqual(len(self.marionette.chrome_window_handles), len(self.start_windows))
def test_should_load_and_close_a_window(self):
def open_window_with_link():
test_html = self.marionette.absolute_url("test_windows.html")
with self.marionette.using_context("content"):
self.marionette.navigate(test_html)
self.marionette.find_element(By.LINK_TEXT, "Open new window").click()
new_window = self.open_window(trigger=open_window_with_link)
self.marionette.switch_to_window(new_window)
self.assertEqual(self.marionette.current_chrome_window_handle, new_window)
self.assertEqual(len(self.marionette.chrome_window_handles), 2)
with self.marionette.using_context('content'):
self.assertEqual(self.marionette.title, "We Arrive Here")
# Let's close and check
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
self.assertEqual(len(self.marionette.chrome_window_handles), 1)

View File

@ -15,30 +15,15 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
def setUp(self):
super(TestNoSuchWindowContent, self).setUp()
self.test_page = self.marionette.absolute_url("windowHandles.html")
with self.marionette.using_context("content"):
self.marionette.navigate(self.test_page)
def tearDown(self):
self.close_all_windows()
super(TestNoSuchWindowContent, self).tearDown()
def open_tab_in_foreground(self):
with self.marionette.using_context("content"):
link = self.marionette.find_element(By.ID, "new-tab")
link.click()
@skip_if_mobile("Fennec doesn't support other chrome windows")
def test_closed_chrome_window(self):
def open_with_link():
with self.marionette.using_context("content"):
test_page = self.marionette.absolute_url("windowHandles.html")
self.marionette.navigate(test_page)
self.marionette.find_element(By.ID, "new-window").click()
win = self.open_window(open_with_link)
self.marionette.switch_to_window(win)
with self.marionette.using_context("chrome"):
new_window = self.open_window()
self.marionette.switch_to_window(new_window)
self.marionette.close_chrome_window()
# When closing a browser window both handles are not available
@ -52,12 +37,13 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_window)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(win)
self.marionette.switch_to_window(new_window)
@skip_if_mobile("Fennec doesn't support other chrome windows")
def test_closed_chrome_window_while_in_frame(self):
win = self.open_chrome_window("chrome://marionette/content/test.xul")
self.marionette.switch_to_window(win)
new_window = self.open_chrome_window("chrome://marionette/content/test.xul")
self.marionette.switch_to_window(new_window)
with self.marionette.using_context("chrome"):
self.marionette.switch_to_frame("iframe")
self.marionette.close_chrome_window()
@ -70,13 +56,12 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_window)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(win)
self.marionette.switch_to_window(new_window)
def test_closed_tab(self):
with self.marionette.using_context("content"):
tab = self.open_tab(self.open_tab_in_foreground)
self.marionette.switch_to_window(tab)
self.marionette.close()
new_tab = self.open_tab(focus=True)
self.marionette.switch_to_window(new_tab)
self.marionette.close()
# Check that only the content window is not available in both contexts
for context in ("chrome", "content"):
@ -88,22 +73,24 @@ class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.start_tab)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(tab)
self.marionette.switch_to_window(new_tab)
def test_closed_tab_while_in_frame(self):
new_tab = self.open_tab()
self.marionette.switch_to_window(new_tab)
with self.marionette.using_context("content"):
tab = self.open_tab(self.open_tab_in_foreground)
self.marionette.switch_to_window(tab)
self.marionette.navigate(self.marionette.absolute_url("test_iframe.html"))
frame = self.marionette.find_element(By.ID, "test_iframe")
self.marionette.switch_to_frame(frame)
self.marionette.close()
with self.assertRaises(NoSuchWindowException):
self.marionette.current_window_handle
self.marionette.current_chrome_window_handle
self.marionette.close()
with self.assertRaises(NoSuchWindowException):
self.marionette.current_window_handle
self.marionette.current_chrome_window_handle
self.marionette.switch_to_window(self.start_tab)
with self.assertRaises(NoSuchWindowException):
self.marionette.switch_to_window(tab)
self.marionette.switch_to_window(new_tab)

View File

@ -11,7 +11,6 @@ const ServerSocket = CC(
"nsIServerSocket",
"initSpecialConnection");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("chrome://marionette/content/assert.js");
@ -74,7 +73,7 @@ class TCPListener {
*/
driverFactory() {
MarionettePrefs.contentListener = false;
return new GeckoDriver(Services.appinfo.ID, this);
return new GeckoDriver(this);
}
set acceptConnections(value) {

View File

@ -13,23 +13,43 @@ const {
stack,
TimeoutError,
} = ChromeUtils.import("chrome://marionette/content/error.js", {});
const {truncate} = ChromeUtils.import("chrome://marionette/content/format.js", {});
const {Log} = ChromeUtils.import("chrome://marionette/content/log.js", {});
XPCOMUtils.defineLazyGetter(this, "log", Log.get);
this.EXPORTED_SYMBOLS = [
"executeSoon",
"DebounceCallback",
"IdlePromise",
"MessageManagerDestroyedPromise",
"PollPromise",
"Sleep",
"TimedPromise",
"waitForEvent",
"waitForMessage",
"waitForObserverTopic",
];
const {TYPE_ONE_SHOT, TYPE_REPEATING_SLACK} = Ci.nsITimer;
const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
/**
* Dispatch a function to be executed on the main thread.
*
* @param {function} func
* Function to be executed.
*/
function executeSoon(func) {
if (typeof func != "function") {
throw new TypeError();
}
Services.tm.dispatchToMainThread(func);
}
/**
* @callback Condition
*
@ -72,14 +92,14 @@ const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
* } else {
* reject([]);
* }
* });
* }, {timeout: 1000});
*
* @param {Condition} func
* Function to run off the main thread.
* @param {number=} [timeout=2000] timeout
* Desired timeout. If 0 or less than the runtime evaluation
* @param {number=} [timeout] timeout
* Desired timeout if wanted. If 0 or less than the runtime evaluation
* time of ``func``, ``func`` is guaranteed to run at least once.
* The default is 2000 milliseconds.
* Defaults to using no timeout.
* @param {number=} [interval=10] interval
* Duration between each poll of ``func`` in milliseconds.
* Defaults to 10 milliseconds.
@ -95,23 +115,30 @@ const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
* @throws {RangeError}
* If `timeout` or `interval` are not unsigned integers.
*/
function PollPromise(func, {timeout = 2000, interval = 10} = {}) {
function PollPromise(func, {timeout = null, interval = 10} = {}) {
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
if (typeof func != "function") {
throw new TypeError();
}
if (!(typeof timeout == "number" && typeof interval == "number")) {
if (timeout != null && typeof timeout != "number") {
throw new TypeError();
}
if ((!Number.isInteger(timeout) || timeout < 0) ||
if (typeof interval != "number") {
throw new TypeError();
}
if ((timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
(!Number.isInteger(interval) || interval < 0)) {
throw new RangeError();
}
return new Promise((resolve, reject) => {
const start = new Date().getTime();
const end = start + timeout;
let start, end;
if (Number.isInteger(timeout)) {
start = new Date().getTime();
end = start + timeout;
}
let evalFn = () => {
new Promise(func).then(resolve, rejected => {
@ -119,9 +146,10 @@ function PollPromise(func, {timeout = 2000, interval = 10} = {}) {
throw rejected;
}
// return if timeout is 0, allowing |func| to be evaluated at
// least once
if (start == end || new Date().getTime() >= end) {
// return if there is a timeout and set to 0,
// allowing |func| to be evaluated at least once
if (typeof end != "undefined" &&
(start == end || new Date().getTime() >= end)) {
resolve(rejected);
}
}).catch(reject);
@ -351,3 +379,192 @@ class DebounceCallback {
}
}
this.DebounceCallback = DebounceCallback;
/**
* Wait for an event to be fired on a specified element.
*
* This method has been duplicated from BrowserTestUtils.jsm.
*
* Because this function is intended for testing, any error in checkFn
* will cause the returned promise to be rejected instead of waiting for
* the next event, since this is probably a bug in the test.
*
* Usage::
*
* let promiseEvent = waitForEvent(element, "eventName");
* // Do some processing here that will cause the event to be fired
* // ...
* // Now wait until the Promise is fulfilled
* let receivedEvent = await promiseEvent;
*
* The promise resolution/rejection handler for the returned promise is
* guaranteed not to be called until the next event tick after the event
* listener gets called, so that all other event listeners for the element
* are executed before the handler is executed::
*
* let promiseEvent = waitForEvent(element, "eventName");
* // Same event tick here.
* await promiseEvent;
* // Next event tick here.
*
* If some code, such like adding yet another event listener, needs to be
* executed in the same event tick, use raw addEventListener instead and
* place the code inside the event listener::
*
* element.addEventListener("load", () => {
* // Add yet another event listener in the same event tick as the load
* // event listener.
* p = waitForEvent(element, "ready");
* }, { once: true });
*
* @param {Element} subject
* The element that should receive the event.
* @param {string} eventName
* Name of the event to listen to.
* @param {Object=} options
* Extra options.
* @param {boolean=} options.capture
* True to use a capturing listener.
* @param {function(Event)=} options.checkFn
* Called with the ``Event`` object as argument, should return ``true``
* if the event is the expected one, or ``false`` if it should be
* ignored and listening should continue. If not specified, the first
* event with the specified name resolves the returned promise.
* @param {boolean=} options.wantsUntrusted
* True to receive synthetic events dispatched by web content.
*
* @return {Promise.<Event>}
* Promise which resolves to the received ``Event`` object, or rejects
* in case of a failure.
*/
function waitForEvent(subject, eventName,
{capture = false, checkFn = null, wantsUntrusted = false} = {}) {
if (subject == null || !("addEventListener" in subject)) {
throw new TypeError();
}
if (typeof eventName != "string") {
throw new TypeError();
}
if (capture != null && typeof capture != "boolean") {
throw new TypeError();
}
if (checkFn != null && typeof checkFn != "function") {
throw new TypeError();
}
if (wantsUntrusted != null && typeof wantsUntrusted != "boolean") {
throw new TypeError();
}
return new Promise((resolve, reject) => {
subject.addEventListener(eventName, function listener(event) {
log.trace(`Received DOM event ${event.type} for ${event.target}`);
try {
if (checkFn && !checkFn(event)) {
return;
}
subject.removeEventListener(eventName, listener, capture);
executeSoon(() => resolve(event));
} catch (ex) {
try {
subject.removeEventListener(eventName, listener, capture);
} catch (ex2) {
// Maybe the provided object does not support removeEventListener.
}
executeSoon(() => reject(ex));
}
}, capture, wantsUntrusted);
});
}
/**
* Wait for a message to be fired from a particular message manager.
*
* This method has been duplicated from BrowserTestUtils.jsm.
*
* @param {nsIMessageManager} messageManager
* The message manager that should be used.
* @param {string} messageName
* The message to wait for.
* @param {Object=} options
* Extra options.
* @param {function(Message)=} options.checkFn
* Called with the ``Message`` object as argument, should return ``true``
* if the message is the expected one, or ``false`` if it should be
* ignored and listening should continue. If not specified, the first
* message with the specified name resolves the returned promise.
*
* @return {Promise.<Object>}
* Promise which resolves to the data property of the received
* ``Message``.
*/
function waitForMessage(messageManager, messageName,
{checkFn = undefined} = {}) {
if (messageManager == null || !("addMessageListener" in messageManager)) {
throw new TypeError();
}
if (typeof messageName != "string") {
throw new TypeError();
}
if (checkFn && typeof checkFn != "function") {
throw new TypeError();
}
return new Promise(resolve => {
messageManager.addMessageListener(messageName, function onMessage(msg) {
log.trace(`Received ${messageName} for ${msg.target}`);
if (checkFn && !checkFn(msg)) {
return;
}
messageManager.removeMessageListener(messageName, onMessage);
resolve(msg.data);
});
});
}
/**
* Wait for the specified observer topic to be observed.
*
* This method has been duplicated from TestUtils.jsm.
*
* Because this function is intended for testing, any error in checkFn
* will cause the returned promise to be rejected instead of waiting for
* the next notification, since this is probably a bug in the test.
*
* @param {string} topic
* The topic to observe.
* @param {Object=} options
* Extra options.
* @param {function(String,Object)=} options.checkFn
* Called with ``subject``, and ``data`` as arguments, should return true
* if the notification is the expected one, or false if it should be
* ignored and listening should continue. If not specified, the first
* notification for the specified topic resolves the returned promise.
*
* @return {Promise.<Array<String, Object>>}
* Promise which resolves to an array of ``subject``, and ``data`` from
* the observed notification.
*/
function waitForObserverTopic(topic, {checkFn = null} = {}) {
if (typeof topic != "string") {
throw new TypeError();
}
if (checkFn != null && typeof checkFn != "function") {
throw new TypeError();
}
return new Promise((resolve, reject) => {
Services.obs.addObserver(function observer(subject, topic, data) {
log.trace(`Received observer notification ${topic}`);
try {
if (checkFn && !checkFn(subject, data)) {
return;
}
Services.obs.removeObserver(observer, topic);
resolve({subject, data});
} catch (ex) {
Services.obs.removeObserver(observer, topic);
reject(ex);
}
}, topic);
});
}

View File

@ -2,15 +2,90 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
ChromeUtils.import("resource://gre/modules/Services.jsm");
const {
DebounceCallback,
IdlePromise,
PollPromise,
Sleep,
TimedPromise,
waitForEvent,
waitForMessage,
waitForObserverTopic,
} = ChromeUtils.import("chrome://marionette/content/sync.js", {});
const DEFAULT_TIMEOUT = 2000;
/**
* Mimic a DOM node for listening for events.
*/
class MockElement {
constructor() {
this.capture = false;
this.func = null;
this.eventName = null;
this.untrusted = false;
}
addEventListener(name, func, capture, untrusted) {
this.eventName = name;
this.func = func;
if (capture != null) {
this.capture = capture;
}
if (untrusted != null) {
this.untrusted = untrusted;
}
}
click() {
if (this.func) {
let details = {
capture: this.capture,
target: this,
type: this.eventName,
untrusted: this.untrusted,
};
this.func(details);
}
}
removeEventListener(name, func) {
this.capture = false;
this.func = null;
this.eventName = null;
this.untrusted = false;
}
}
/**
* Mimic a message manager for sending messages.
*/
class MessageManager {
constructor() {
this.func = null;
this.message = null;
}
addMessageListener(message, func) {
this.func = func;
this.message = message;
}
removeMessageListener(message) {
this.func = null;
this.message = null;
}
send(message, data) {
if (this.func) {
this.func({
data,
message,
target: this,
});
}
}
}
/**
* Mimics nsITimer, but instead of using a system clock you can
@ -35,6 +110,23 @@ class MockTimer {
}
}
add_test(function test_executeSoon_callback() {
// executeSoon() is already defined for xpcshell in head.js. As such import
// our implementation into a custom namespace.
let sync = {};
ChromeUtils.import("chrome://marionette/content/sync.js", sync);
for (let func of ["foo", null, true, [], {}]) {
Assert.throws(() => sync.executeSoon(func), /TypeError/);
}
let a;
sync.executeSoon(() => { a = 1; });
executeSoon(() => equal(1, a));
run_next_test();
});
add_test(function test_PollPromise_funcTypes() {
for (let type of ["foo", 42, null, undefined, true, [], {}]) {
Assert.throws(() => new PollPromise(type), /TypeError/);
@ -46,13 +138,15 @@ add_test(function test_PollPromise_funcTypes() {
});
add_test(function test_PollPromise_timeoutTypes() {
for (let timeout of ["foo", null, true, [], {}]) {
for (let timeout of ["foo", true, [], {}]) {
Assert.throws(() => new PollPromise(() => {}, {timeout}), /TypeError/);
}
for (let timeout of [1.2, -1]) {
Assert.throws(() => new PollPromise(() => {}, {timeout}), /RangeError/);
}
new PollPromise(() => {}, {timeout: 42});
for (let timeout of [null, undefined, 42]) {
new PollPromise(resolve => resolve(1), {timeout});
}
run_next_test();
});
@ -75,18 +169,6 @@ add_task(async function test_PollPromise_retvalTypes() {
}
});
add_task(async function test_PollPromise_timeoutElapse() {
let nevals = 0;
let start = new Date().getTime();
await new PollPromise((resolve, reject) => {
++nevals;
reject();
});
let end = new Date().getTime();
greaterOrEqual((end - start), DEFAULT_TIMEOUT);
greaterOrEqual(nevals, 15);
});
add_task(async function test_PollPromise_rethrowError() {
let nevals = 0;
let err;
@ -103,6 +185,15 @@ add_task(async function test_PollPromise_rethrowError() {
});
add_task(async function test_PollPromise_noTimeout() {
let nevals = 0;
await new PollPromise((resolve, reject) => {
++nevals;
nevals < 100 ? reject() : resolve();
});
equal(100, nevals);
});
add_task(async function test_PollPromise_zeroTimeout() {
// run at least once when timeout is 0
let nevals = 0;
let start = new Date().getTime();
@ -112,10 +203,10 @@ add_task(async function test_PollPromise_noTimeout() {
}, {timeout: 0});
let end = new Date().getTime();
equal(1, nevals);
less((end - start), DEFAULT_TIMEOUT);
less((end - start), 500);
});
add_task(async function test_PollPromise_timeout() {
add_task(async function test_PollPromise_timeoutElapse() {
let nevals = 0;
let start = new Date().getTime();
await new PollPromise((resolve, reject) => {
@ -123,7 +214,7 @@ add_task(async function test_PollPromise_timeout() {
reject();
}, {timeout: 100});
let end = new Date().getTime();
greater(nevals, 1);
lessOrEqual(nevals, 11);
greaterOrEqual((end - start), 100);
});
@ -213,3 +304,155 @@ add_task(async function test_DebounceCallback_repeatedCallback() {
equal(ncalls, 1);
ok(debouncer.timer.cancelled);
});
add_task(async function test_waitForEvent_subjectAndEventNameTypes() {
let element = new MockElement();
for (let subject of ["foo", 42, null, undefined, true, [], {}]) {
Assert.throws(() => waitForEvent(subject, "click"), /TypeError/);
}
for (let eventName of [42, null, undefined, true, [], {}]) {
Assert.throws(() => waitForEvent(element, eventName), /TypeError/);
}
let clicked = waitForEvent(element, "click");
element.click();
let event = await clicked;
equal(element, event.target);
});
add_task(async function test_waitForEvent_captureTypes() {
let element = new MockElement();
for (let capture of ["foo", 42, [], {}]) {
Assert.throws(() => waitForEvent(
element, "click", {capture}), /TypeError/);
}
for (let capture of [null, undefined, false, true]) {
let expected_capture = (capture == null) ? false : capture;
element = new MockElement();
let clicked = waitForEvent(element, "click", {capture});
element.click();
let event = await clicked;
equal(element, event.target);
equal(expected_capture, event.capture);
}
});
add_task(async function test_waitForEvent_checkFnTypes() {
let element = new MockElement();
for (let checkFn of ["foo", 42, true, [], {}]) {
Assert.throws(() => waitForEvent(
element, "click", {checkFn}), /TypeError/);
}
let count;
for (let checkFn of [null, undefined, event => count++ > 0]) {
let expected_count = (checkFn == null) ? 0 : 2;
count = 0;
element = new MockElement();
let clicked = waitForEvent(element, "click", {checkFn});
element.click();
element.click();
let event = await clicked;
equal(element, event.target);
equal(expected_count, count);
}
});
add_task(async function test_waitForEvent_wantsUntrustedTypes() {
let element = new MockElement();
for (let wantsUntrusted of ["foo", 42, [], {}]) {
Assert.throws(() => waitForEvent(
element, "click", {wantsUntrusted}), /TypeError/);
}
for (let wantsUntrusted of [null, undefined, false, true]) {
let expected_untrusted = (wantsUntrusted == null) ? false : wantsUntrusted;
element = new MockElement();
let clicked = waitForEvent(element, "click", {wantsUntrusted});
element.click();
let event = await clicked;
equal(element, event.target);
equal(expected_untrusted, event.untrusted);
}
});
add_task(async function test_waitForMessage_messageManagerAndMessageTypes() {
let messageManager = new MessageManager();
for (let manager of ["foo", 42, null, undefined, true, [], {}]) {
Assert.throws(() => waitForMessage(manager, "message"), /TypeError/);
}
for (let message of [42, null, undefined, true, [], {}]) {
Assert.throws(() => waitForEvent(messageManager, message), /TypeError/);
}
let data = {"foo": "bar"};
let sent = waitForMessage(messageManager, "message");
messageManager.send("message", data);
equal(data, await sent);
});
add_task(async function test_waitForMessage_checkFnTypes() {
let messageManager = new MessageManager();
for (let checkFn of ["foo", 42, true, [], {}]) {
Assert.throws(() => waitForMessage(
messageManager, "message", {checkFn}), /TypeError/);
}
let data1 = {"fo": "bar"};
let data2 = {"foo": "bar"};
for (let checkFn of [null, undefined, msg => "foo" in msg.data]) {
let expected_data = (checkFn == null) ? data1 : data2;
messageManager = new MessageManager();
let sent = waitForMessage(messageManager, "message", {checkFn});
messageManager.send("message", data1);
messageManager.send("message", data2);
equal(expected_data, await sent);
}
});
add_task(async function test_waitForObserverTopic_topicTypes() {
for (let topic of [42, null, undefined, true, [], {}]) {
Assert.throws(() => waitForObserverTopic(topic), /TypeError/);
}
let data = {"foo": "bar"};
let sent = waitForObserverTopic("message");
Services.obs.notifyObservers(this, "message", data);
let result = await sent;
equal(this, result.subject);
equal(data, result.data);
});
add_task(async function test_waitForObserverTopic_checkFnTypes() {
for (let checkFn of ["foo", 42, true, [], {}]) {
Assert.throws(() => waitForObserverTopic(
"message", {checkFn}), /TypeError/);
}
let data1 = {"fo": "bar"};
let data2 = {"foo": "bar"};
for (let checkFn of [null, undefined, (subject, data) => data == data2]) {
let expected_data = (checkFn == null) ? data1 : data2;
let sent = waitForObserverTopic("message");
Services.obs.notifyObservers(this, "message", data1);
Services.obs.notifyObservers(this, "message", data2);
let result = await sent;
equal(expected_data, result.data);
}
});

View File

@ -10,14 +10,17 @@ const CC = Components.Constructor;
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/EventEmitter.jsm");
const {StreamUtils} =
ChromeUtils.import("chrome://marionette/content/stream-utils.js", {});
const {Packet, JSONPacket, BulkPacket} =
ChromeUtils.import("chrome://marionette/content/packets.js", {});
const executeSoon = function(func) {
Services.tm.dispatchToMainThread(func);
};
const {
StreamUtils,
} = ChromeUtils.import("chrome://marionette/content/stream-utils.js", {});
const {
BulkPacket,
JSONPacket,
Packet,
} = ChromeUtils.import("chrome://marionette/content/packets.js", {});
const {
executeSoon,
} = ChromeUtils.import("chrome://marionette/content/sync.js", {});
const flags = {wantVerbose: false, wantLogging: false};

View File

@ -80,6 +80,9 @@ class WebPlatformTestsRunnerSetup(MozbuildObject):
if kwargs["log_mach_screenshot"] is None:
kwargs["log_mach_screenshot"] = True
if kwargs["lsan_dir"] is None:
kwargs["lsan_dir"] = os.path.join(self.topsrcdir, "build", "sanitizers")
kwargs["capture_stdio"] = True
return kwargs

View File

@ -1,5 +1,4 @@
[promise.py]
expected: TIMEOUT
[test_promise_timeout]
expected: FAIL

View File

@ -74,6 +74,7 @@ def browser_kwargs(test_type, run_info_data, config, **kwargs):
"certutil_binary": kwargs["certutil_binary"],
"ca_certificate_path": config.ssl_config["ca_cert_path"],
"e10s": kwargs["gecko_e10s"],
"lsan_dir": kwargs["lsan_dir"],
"stackfix_dir": kwargs["stackfix_dir"],
"binary_args": kwargs["binary_args"],
"timeout_multiplier": get_timeout_multiplier(test_type,
@ -167,7 +168,7 @@ class FirefoxBrowser(Browser):
def __init__(self, logger, binary, prefs_root, test_type, extra_prefs=None, debug_info=None,
symbols_path=None, stackwalk_binary=None, certutil_binary=None,
ca_certificate_path=None, e10s=False, stackfix_dir=None,
ca_certificate_path=None, e10s=False, lsan_dir=None, stackfix_dir=None,
binary_args=None, timeout_multiplier=None, leak_check=False, asan=False,
stylo_threads=1, chaos_mode_flags=None, config=None, headless=None, **kwargs):
Browser.__init__(self, logger)
@ -196,6 +197,7 @@ class FirefoxBrowser(Browser):
self.init_timeout = self.init_timeout * timeout_multiplier
self.asan = asan
self.lsan_dir = lsan_dir
self.lsan_allowed = None
self.lsan_max_stack_depth = None
self.mozleak_allowed = None
@ -237,7 +239,7 @@ class FirefoxBrowser(Browser):
env = test_environment(xrePath=os.path.dirname(self.binary),
debugger=self.debug_info is not None,
log=self.logger,
lsanPath=self.prefs_root)
lsanPath=self.lsan_dir)
env["STYLO_THREADS"] = str(self.stylo_threads)
if self.chaos_mode_flags is not None:

View File

@ -251,6 +251,8 @@ scheme host and port.""")
help="Run tests without electrolysis preferences")
gecko_group.add_argument("--stackfix-dir", dest="stackfix_dir", action="store",
help="Path to directory containing assertion stack fixing scripts")
gecko_group.add_argument("--lsan-dir", dest="lsan_dir", action="store",
help="Path to directory containing LSAN suppressions file")
gecko_group.add_argument("--setpref", dest="extra_prefs", action='append',
default=[], metavar="PREF=VALUE",
help="Defines an extra user preference (overrides those in prefs_root)")
@ -525,6 +527,9 @@ def check_args(kwargs):
if kwargs["reftest_internal"] is None:
kwargs["reftest_internal"] = True
if kwargs["lsan_dir"] is None:
kwargs["lsan_dir"] = kwargs["prefs_root"]
return kwargs

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@
[package]
name = "encoding_rs"
version = "0.8.13"
version = "0.8.14"
authors = ["Henri Sivonen <hsivonen@hsivonen.fi>"]
description = "A Gecko-oriented implementation of the Encoding Standard"
homepage = "https://docs.rs/encoding_rs/"

View File

@ -81,16 +81,24 @@ For decoding character encodings that occur in email, use the
[`charset`](https://crates.io/crates/charset) crate instead of using this
one directly. (It wraps this crate and adds UTF-7 decoding.)
## Windows Code Page Identifier Mappings
For mappings to and from Windows code page identifiers, use the
[`codepage`](https://crates.io/crates/codepage) crate.
## Licensing
Please see the file named
[COPYRIGHT](https://github.com/hsivonen/encoding_rs/blob/master/COPYRIGHT).
## API Documentation
## Documentation
Generated [API documentation](https://docs.rs/encoding_rs/) is available
online.
There is a [long-form write-up](https://hsivonen.fi/encoding_rs/) about the
design and internals of the crate.
## C and C++ bindings
An FFI layer for encoding_rs is available as a
@ -103,6 +111,9 @@ For the Gecko context, there's a
These bindings do not cover the `mem` module.
There's a [write-up](https://hsivonen.fi/modern-cpp-in-rust/) about the C++
wrappers.
## Sample programs
* [Rust](https://github.com/hsivonen/recode_rs)
@ -326,6 +337,8 @@ To regenerate the generated code:
* Have Python 2 installed.
* Clone [`https://github.com/hsivonen/encoding_c`](https://github.com/hsivonen/encoding_c)
next to the `encoding_rs` directory.
* Clone [`https://github.com/hsivonen/codepage`](https://github.com/hsivonen/codepage)
next to the `encoding_rs` directory.
* Clone [`https://github.com/whatwg/encoding`](https://github.com/whatwg/encoding)
next to the `encoding_rs` directory.
* Checkout revision `f381389` of the `encoding` repo.
@ -365,15 +378,20 @@ To regenerate the generated code:
- [ ] ~Investigate Björn Höhrmann's lookup table acceleration for UTF-8 as
adapted to Rust in rust-encoding.~
- [x] Add actually fast CJK encode options.
- [ ] Investigate [Bob Steagall's lookup table acceleration for UTF-8](https://github.com/BobSteagall/CppNow2018/blob/master/FastConversionFromUTF-8/Fast%20Conversion%20From%20UTF-8%20with%20C%2B%2B%2C%20DFAs%2C%20and%20SSE%20Intrinsics%20-%20Bob%20Steagall%20-%20C%2B%2BNow%202018.pdf).
- [ ] ~Investigate [Bob Steagall's lookup table acceleration for UTF-8](https://github.com/BobSteagall/CppNow2018/blob/master/FastConversionFromUTF-8/Fast%20Conversion%20From%20UTF-8%20with%20C%2B%2B%2C%20DFAs%2C%20and%20SSE%20Intrinsics%20-%20Bob%20Steagall%20-%20C%2B%2BNow%202018.pdf).~
## Release Notes
### 0.8.14
* Made UTF-16 to UTF-8 encode conversion fill the output buffer as
closely as possible.
### 0.8.13
* Made the UTF-8 to UTF-16 compare the number of code units written with the
length of the right slice (the output slice) to fix a panic introduced in
0.8.11.
* Made the UTF-8 to UTF-16 decoder compare the number of code units written
with the length of the right slice (the output slice) to fix a panic
introduced in 0.8.11.
### 0.8.12

View File

@ -22,6 +22,10 @@ if not os.path.isfile("../encoding_c/src/lib.rs"):
sys.stderr.write("This script also writes the generated parts of the encoding_c crate and needs a clone of https://github.com/hsivonen/encoding_c next to the encoding_rs directory.\n");
sys.exit(-1)
if not os.path.isfile("../codepage/src/lib.rs"):
sys.stderr.write("This script also writes the generated parts of the codepage crate and needs a clone of https://github.com/hsivonen/codepage next to the encoding_rs directory.\n");
sys.exit(-1)
def cmp_from_end(one, other):
c = cmp(len(one), len(other))
if c != 0:
@ -119,8 +123,6 @@ single_byte = []
multi_byte = []
code_pages = []
def to_camel_name(name):
if name == u"iso-8859-8-i":
return u"Iso8I"
@ -137,6 +139,52 @@ def to_snake_name(name):
def to_dom_name(name):
return name
# Guestimate based on
# https://w3techs.com/technologies/overview/character_encoding/all
# whose methodology is known to be bogus, but the results are credible for
# this purpose. UTF-16LE lifted up due to prevalence on Windows and
# "ANSI codepages" prioritized.
encodings_by_code_page_frequency = [
"UTF-8",
"UTF-16LE",
"windows-1252",
"windows-1251",
"GBK",
"Shift_JIS",
"EUC-KR",
"windows-1250",
"windows-1256",
"windows-1254",
"Big5",
"windows-874",
"windows-1255",
"windows-1253",
"windows-1257",
"windows-1258",
"EUC-JP",
"ISO-8859-2",
"ISO-8859-15",
"ISO-8859-7",
"KOI8-R",
"gb18030",
"ISO-8859-5",
"ISO-8859-8-I",
"ISO-8859-4",
"ISO-8859-6",
"ISO-2022-JP",
"KOI8-U",
"ISO-8859-13",
"ISO-8859-3",
"UTF-16BE",
"IBM866",
"ISO-8859-10",
"ISO-8859-8",
"macintosh",
"x-mac-cyrillic",
"ISO-8859-14",
"ISO-8859-16",
]
encodings_by_code_page = {
932: "Shift_JIS",
936: "GBK",
@ -185,18 +233,36 @@ for code_page, encoding in encodings_by_code_page.iteritems():
encoding_by_alias_code_page = {
951: "Big5",
10007: "x-mac-cyrillic",
20936: "GBK",
20949: "EUC-KR",
21010: "UTF-16LE", # Undocumented; needed by calamine for Excel compat
28591: "windows-1252",
28599: "windows-1254",
28601: "windows-847",
28601: "windows-874",
50220: "ISO-2022-JP",
50222: "ISO-2022-JP",
50225: "replacement", # ISO-2022-KR
50227: "replacement", # ISO-2022-CN
51949: "EUC-JP",
51936: "GBK",
51949: "EUC-KR",
52936: "replacement", # HZ
}
code_pages = []
for name in encodings_by_code_page_frequency:
code_pages.append(code_pages_by_encoding[name])
encodings_by_code_page.update(encoding_by_alias_code_page)
temp_keys = encodings_by_code_page.keys()
temp_keys.sort()
for code_page in temp_keys:
if not code_page in code_pages:
code_pages.append(code_page)
# The position in the index (0 is the first index entry,
# i.e. byte value 0x80) that starts the longest run of
# consecutive code points. Must not be in the first
@ -1806,4 +1872,71 @@ for pointer in range(0, len(index)):
jis0212_in_ref_file.write(u"\uFFFD\n".encode("utf-8"))
jis0212_in_ref_file.close()
(codepage_begin, codepage_end) = read_non_generated("../codepage/src/lib.rs")
codepage_file = open("../codepage/src/lib.rs", "w")
codepage_file.write(codepage_begin)
codepage_file.write("""
// Instead, please regenerate using generate-encoding-data.py
/// Supported code page numbers in estimated order of usage frequency
static CODE_PAGES: [u16; %d] = [
""" % len(code_pages))
for code_page in code_pages:
codepage_file.write(" %d,\n" % code_page)
codepage_file.write("""];
/// Encodings corresponding to the code page numbers in the same order
static ENCODINGS: [&'static Encoding; %d] = [
""" % len(code_pages))
for code_page in code_pages:
name = encodings_by_code_page[code_page]
codepage_file.write(" &%s_INIT,\n" % to_constant_name(name))
codepage_file.write("""];
""")
codepage_file.write(codepage_end)
codepage_file.close()
(codepage_test_begin, codepage_test_end) = read_non_generated("../codepage/src/tests.rs")
codepage_test_file = open("../codepage/src/tests.rs", "w")
codepage_test_file.write(codepage_test_begin)
codepage_test_file.write("""
// Instead, please regenerate using generate-encoding-data.py
#[test]
fn test_to_encoding() {
assert_eq!(to_encoding(0), None);
""")
for code_page in code_pages:
codepage_test_file.write(" assert_eq!(to_encoding(%d), Some(%s));\n" % (code_page, to_constant_name(encodings_by_code_page[code_page])))
codepage_test_file.write("""}
#[test]
fn test_from_encoding() {
""")
for name in preferred:
if code_pages_by_encoding.has_key(name):
codepage_test_file.write(" assert_eq!(from_encoding(%s), Some(%d));\n" % (to_constant_name(name), code_pages_by_encoding[name]))
else:
codepage_test_file.write(" assert_eq!(from_encoding(%s), None);\n" % to_constant_name(name))
codepage_test_file.write("""}
""")
codepage_test_file.write(codepage_test_end)
codepage_test_file.close()
subprocess.call(["cargo", "fmt"])

View File

@ -31,7 +31,7 @@
))]
use simd_funcs::*;
cfg_if!{
cfg_if! {
if #[cfg(feature = "simd-accel")] {
#[allow(unused_imports)]
use ::std::intrinsics::unlikely;
@ -90,10 +90,7 @@ macro_rules! ascii_alu {
$src_unit:ty,
$dst_unit:ty,
$stride_fn:ident) => {
#[cfg_attr(
feature = "cargo-clippy",
allow(never_loop, cast_ptr_alignment)
)]
#[cfg_attr(feature = "cargo-clippy", allow(never_loop, cast_ptr_alignment))]
#[inline(always)]
pub unsafe fn $name(
src: *const $src_unit,
@ -186,11 +183,7 @@ macro_rules! basic_latin_alu {
$stride_fn:ident) => {
#[cfg_attr(
feature = "cargo-clippy",
allow(
never_loop,
cast_ptr_alignment,
cast_lossless
)
allow(never_loop, cast_ptr_alignment, cast_lossless)
)]
#[inline(always)]
pub unsafe fn $name(
@ -285,11 +278,7 @@ macro_rules! latin1_alu {
($name:ident, $src_unit:ty, $dst_unit:ty, $stride_fn:ident) => {
#[cfg_attr(
feature = "cargo-clippy",
allow(
never_loop,
cast_ptr_alignment,
cast_lossless
)
allow(never_loop, cast_ptr_alignment, cast_lossless)
)]
#[inline(always)]
pub unsafe fn $name(src: *const $src_unit, dst: *mut $dst_unit, len: usize) {

View File

@ -29498,10 +29498,7 @@ pub static JIS0208_RANGE_TRIPLES: [u16; 54] = [
0x29DC, 0x000A, 0x2170, 0x29E6, 0x000A, 0x2160,
];
#[cfg(all(
feature = "less-slow-kanji-encode",
not(feature = "fast-kanji-encode")
))]
#[cfg(all(feature = "less-slow-kanji-encode", not(feature = "fast-kanji-encode")))]
static JIS0208_LEVEL1_KANJI_CODE_POINTS: [u16; 2965] = [
0x4E00, 0x4E01, 0x4E03, 0x4E07, 0x4E08, 0x4E09, 0x4E0A, 0x4E0B, 0x4E0D, 0x4E0E, 0x4E11, 0x4E14,
0x4E16, 0x4E18, 0x4E19, 0x4E1E, 0x4E21, 0x4E26, 0x4E2D, 0x4E32, 0x4E38, 0x4E39, 0x4E3B, 0x4E43,
@ -29753,10 +29750,7 @@ static JIS0208_LEVEL1_KANJI_CODE_POINTS: [u16; 2965] = [
0x9F8D,
];
#[cfg(all(
feature = "less-slow-kanji-encode",
not(feature = "fast-kanji-encode")
))]
#[cfg(all(feature = "less-slow-kanji-encode", not(feature = "fast-kanji-encode")))]
static JIS0208_LEVEL1_KANJI_SHIFT_JIS_BYTES: [[u8; 2]; 2965] = [
[0x88, 0xEA],
[0x92, 0x9A],
@ -113967,10 +113961,7 @@ pub fn jis0208_kanji_shift_jis_encode(bmp: u16) -> Option<(u8, u8)> {
Some((lead | 0x80, trail))
}
#[cfg(any(
feature = "less-slow-kanji-encode",
feature = "fast-kanji-encode"
))]
#[cfg(any(feature = "less-slow-kanji-encode", feature = "fast-kanji-encode"))]
#[inline(always)]
fn shift_jis_to_euc_jp(tuple: (u8, u8)) -> (u8, u8) {
let (shift_jis_lead, shift_jis_trail) = tuple;
@ -114012,10 +114003,7 @@ pub fn jis0208_kanji_euc_jp_encode(bmp: u16) -> Option<(u8, u8)> {
Some(shift_jis_to_euc_jp((lead, trail)))
}
#[cfg(any(
feature = "less-slow-kanji-encode",
feature = "fast-kanji-encode"
))]
#[cfg(any(feature = "less-slow-kanji-encode", feature = "fast-kanji-encode"))]
#[inline(always)]
fn shift_jis_to_iso_2022_jp(tuple: (u8, u8)) -> (u8, u8) {
let (shift_jis_lead, shift_jis_trail) = tuple;
@ -114057,10 +114045,7 @@ pub fn jis0208_kanji_iso_2022_jp_encode(bmp: u16) -> Option<(u8, u8)> {
Some(shift_jis_to_iso_2022_jp((lead, trail)))
}
#[cfg(not(any(
feature = "less-slow-kanji-encode",
feature = "fast-kanji-encode"
)))]
#[cfg(not(any(feature = "less-slow-kanji-encode", feature = "fast-kanji-encode")))]
#[inline(always)]
pub fn jis0208_level1_kanji_shift_jis_encode(bmp: u16) -> Option<(u8, u8)> {
position(&JIS0208_LEVEL1_KANJI[..], bmp).map(|kanji_pointer| {
@ -114073,10 +114058,7 @@ pub fn jis0208_level1_kanji_shift_jis_encode(bmp: u16) -> Option<(u8, u8)> {
})
}
#[cfg(all(
feature = "less-slow-kanji-encode",
not(feature = "fast-kanji-encode")
))]
#[cfg(all(feature = "less-slow-kanji-encode", not(feature = "fast-kanji-encode")))]
#[inline(always)]
pub fn jis0208_level1_kanji_shift_jis_encode(bmp: u16) -> Option<(u8, u8)> {
match JIS0208_LEVEL1_KANJI_CODE_POINTS.binary_search(&bmp) {
@ -114088,10 +114070,7 @@ pub fn jis0208_level1_kanji_shift_jis_encode(bmp: u16) -> Option<(u8, u8)> {
}
}
#[cfg(not(any(
feature = "less-slow-kanji-encode",
feature = "fast-kanji-encode"
)))]
#[cfg(not(any(feature = "less-slow-kanji-encode", feature = "fast-kanji-encode")))]
#[inline(always)]
pub fn jis0208_level1_kanji_euc_jp_encode(bmp: u16) -> Option<(u8, u8)> {
position(&JIS0208_LEVEL1_KANJI[..], bmp).map(|kanji_pointer| {
@ -114101,19 +114080,13 @@ pub fn jis0208_level1_kanji_euc_jp_encode(bmp: u16) -> Option<(u8, u8)> {
})
}
#[cfg(all(
feature = "less-slow-kanji-encode",
not(feature = "fast-kanji-encode")
))]
#[cfg(all(feature = "less-slow-kanji-encode", not(feature = "fast-kanji-encode")))]
#[inline(always)]
pub fn jis0208_level1_kanji_euc_jp_encode(bmp: u16) -> Option<(u8, u8)> {
jis0208_level1_kanji_shift_jis_encode(bmp).map(shift_jis_to_euc_jp)
}
#[cfg(not(any(
feature = "less-slow-kanji-encode",
feature = "fast-kanji-encode"
)))]
#[cfg(not(any(feature = "less-slow-kanji-encode", feature = "fast-kanji-encode")))]
#[inline(always)]
pub fn jis0208_level1_kanji_iso_2022_jp_encode(bmp: u16) -> Option<(u8, u8)> {
position(&JIS0208_LEVEL1_KANJI[..], bmp).map(|kanji_pointer| {
@ -114123,10 +114096,7 @@ pub fn jis0208_level1_kanji_iso_2022_jp_encode(bmp: u16) -> Option<(u8, u8)> {
})
}
#[cfg(all(
feature = "less-slow-kanji-encode",
not(feature = "fast-kanji-encode")
))]
#[cfg(all(feature = "less-slow-kanji-encode", not(feature = "fast-kanji-encode")))]
#[inline(always)]
pub fn jis0208_level1_kanji_iso_2022_jp_encode(bmp: u16) -> Option<(u8, u8)> {
jis0208_level1_kanji_shift_jis_encode(bmp).map(shift_jis_to_iso_2022_jp)

View File

@ -282,7 +282,7 @@ fn convert_unaligned_utf16_to_utf8<E: Endian>(
unit
}
CopyAsciiResult::Stop(read_written) => {
return (src_pos + read_written, dst_pos + read_written, false)
return (src_pos + read_written, dst_pos + read_written, false);
}
};
if dst_pos >= dst_len_minus_three {

View File

@ -367,10 +367,7 @@ fn is_kanji_mapped(bmp: u16) -> bool {
#[cfg(not(feature = "fast-kanji-encode"))]
#[cfg_attr(
feature = "cargo-clippy",
allow(
if_let_redundant_pattern_matching,
if_same_then_else
)
allow(if_let_redundant_pattern_matching, if_same_then_else)
)]
#[inline(always)]
fn is_kanji_mapped(bmp: u16) -> bool {
@ -391,10 +388,7 @@ fn is_kanji_mapped(bmp: u16) -> bool {
#[cfg_attr(
feature = "cargo-clippy",
allow(
if_let_redundant_pattern_matching,
if_same_then_else
)
allow(if_let_redundant_pattern_matching, if_same_then_else)
)]
fn is_mapped_for_two_byte_encode(bmp: u16) -> bool {
// The code below uses else after return to

View File

@ -9,13 +9,9 @@
#![cfg_attr(
feature = "cargo-clippy",
allow(
doc_markdown,
inline_always,
new_ret_no_self
)
allow(doc_markdown, inline_always, new_ret_no_self)
)]
#![doc(html_root_url = "https://docs.rs/encoding_rs/0.8.13")]
#![doc(html_root_url = "https://docs.rs/encoding_rs/0.8.14")]
//! encoding_rs is a Gecko-oriented Free Software / Open Source implementation
//! of the [Encoding Standard](https://encoding.spec.whatwg.org/) in Rust.
@ -32,6 +28,9 @@
//! [_UTF-16LE, UTF-16BE and Unicode Encoding Schemes_](#utf-16le-utf-16be-and-unicode-encoding-schemes),
//! [_ISO-8859-1_](#iso-8859-1) and [_Web / Browser Focus_](#web--browser-focus) below.
//!
//! There is a [long-form write-up](https://hsivonen.fi/encoding_rs/) about the
//! design and internals of the crate.
//!
//! # Availability
//!
//! The code is available under the
@ -240,7 +239,9 @@
//! way will find encoding_rs useful. While encoding_rs does not try to match
//! Windows behavior, many of the encodings are close enough to legacy
//! encodings implemented by Windows that applications that need to consume
//! data in legacy Windows encodins may find encoding_rs useful.
//! data in legacy Windows encodins may find encoding_rs useful. The
//! [codepage](https://crates.io/crates/codepage) crate maps from Windows
//! code page identifiers onto encoding_rs `Encoding`s and vice versa.
//!
//! For decoding email, UTF-7 support is needed (unfortunately) in additition
//! to the encodings defined in the Encoding Standard. The
@ -664,10 +665,7 @@
//! See the section [_UTF-16LE, UTF-16BE and Unicode Encoding Schemes_](#utf-16le-utf-16be-and-unicode-encoding-schemes)
//! for discussion about the UTF-16 family.
#![cfg_attr(
feature = "simd-accel",
feature(platform_intrinsics, core_intrinsics)
)]
#![cfg_attr(feature = "simd-accel", feature(platform_intrinsics, core_intrinsics))]
#[macro_use]
extern crate cfg_if;
@ -3653,7 +3651,7 @@ impl Decoder {
| DecoderLifeCycle::AtUtf8Start
| DecoderLifeCycle::AtUtf16LeStart
| DecoderLifeCycle::AtUtf16BeStart => {
return self.variant.max_utf8_buffer_length(byte_length)
return self.variant.max_utf8_buffer_length(byte_length);
}
DecoderLifeCycle::AtStart => {
if let Some(utf8_bom) = checked_add(3, byte_length.checked_mul(3)) {
@ -3745,7 +3743,7 @@ impl Decoder {
| DecoderLifeCycle::AtUtf16BeStart => {
return self
.variant
.max_utf8_buffer_length_without_replacement(byte_length)
.max_utf8_buffer_length_without_replacement(byte_length);
}
DecoderLifeCycle::AtStart => {
if let Some(utf8_bom) = byte_length.checked_add(3) {
@ -4061,7 +4059,7 @@ impl Decoder {
| DecoderLifeCycle::AtUtf8Start
| DecoderLifeCycle::AtUtf16LeStart
| DecoderLifeCycle::AtUtf16BeStart => {
return self.variant.max_utf16_buffer_length(byte_length)
return self.variant.max_utf16_buffer_length(byte_length);
}
DecoderLifeCycle::AtStart => {
if let Some(utf8_bom) = byte_length.checked_add(1) {
@ -5300,13 +5298,11 @@ mod tests {
#[test]
fn test_decode_bomful_invalid_utf8_to_cow_without_bom_handling_and_without_replacement() {
assert!(
UTF_8
.decode_without_bom_handling_and_without_replacement(
b"\xEF\xBB\xBF\xE2\x82\xAC\x80\xC3\xA4"
)
.is_none()
);
assert!(UTF_8
.decode_without_bom_handling_and_without_replacement(
b"\xEF\xBB\xBF\xE2\x82\xAC\x80\xC3\xA4"
)
.is_none());
}
#[test]
@ -5324,11 +5320,9 @@ mod tests {
#[test]
fn test_decode_invalid_windows_1257_to_cow_without_bom_handling_and_without_replacement() {
assert!(
WINDOWS_1257
.decode_without_bom_handling_and_without_replacement(b"abc\x80\xA1\xE4")
.is_none()
);
assert!(WINDOWS_1257
.decode_without_bom_handling_and_without_replacement(b"abc\x80\xA1\xE4")
.is_none());
}
#[test]

View File

@ -29,7 +29,6 @@ use super::in_inclusive_range8;
use super::in_range16;
use super::in_range32;
use super::DecoderResult;
use super::EncoderResult;
use ascii::*;
use utf_8::*;
@ -37,10 +36,16 @@ macro_rules! non_fuzz_debug_assert {
($($arg:tt)*) => (if !cfg!(fuzzing) { debug_assert!($($arg)*); })
}
cfg_if!{
cfg_if! {
if #[cfg(feature = "simd-accel")] {
use ::std::intrinsics::likely;
use ::std::intrinsics::unlikely;
} else {
#[inline(always)]
// Unsafe to match the intrinsic, which is needlessly unsafe.
unsafe fn likely(b: bool) -> bool {
b
}
#[inline(always)]
// Unsafe to match the intrinsic, which is needlessly unsafe.
unsafe fn unlikely(b: bool) -> bool {
@ -220,7 +225,7 @@ macro_rules! by_unit_check_simd {
};
}
cfg_if!{
cfg_if! {
if #[cfg(all(feature = "simd-accel", any(target_feature = "sse2", all(target_endian = "little", target_arch = "aarch64"), all(target_endian = "little", target_feature = "neon"))))] {
use simd_funcs::*;
use simd::u8x16;
@ -352,7 +357,7 @@ fn utf16_valid_up_to_alu(buffer: &[u16]) -> (usize, bool) {
}
}
cfg_if!{
cfg_if! {
if #[cfg(all(feature = "simd-accel", any(target_feature = "sse2", all(target_endian = "little", target_arch = "aarch64"), all(target_endian = "little", target_feature = "neon"))))] {
#[inline(always)]
fn is_str_latin1_impl(buffer: &str) -> Option<usize> {
@ -441,7 +446,7 @@ fn is_utf8_latin1_impl(buffer: &[u8]) -> Option<usize> {
}
}
cfg_if!{
cfg_if! {
if #[cfg(all(feature = "simd-accel", any(target_feature = "sse2", all(target_endian = "little", target_arch = "aarch64"), all(target_endian = "little", target_feature = "neon"))))] {
#[inline(always)]
fn is_utf16_bidi_impl(buffer: &[u16]) -> bool {
@ -491,7 +496,7 @@ cfg_if!{
}
}
cfg_if!{
cfg_if! {
if #[cfg(all(feature = "simd-accel", any(target_feature = "sse2", all(target_endian = "little", target_arch = "aarch64"), all(target_endian = "little", target_feature = "neon"))))] {
#[inline(always)]
fn check_utf16_for_latin1_and_bidi_impl(buffer: &[u16]) -> Latin1Bidi {
@ -687,10 +692,7 @@ pub fn is_utf16_latin1(buffer: &[u16]) -> bool {
/// Returns `true` if the input is invalid UTF-8 or the input contains an
/// RTL character. Returns `false` if the input is valid UTF-8 and contains
/// no RTL characters.
#[cfg_attr(
feature = "cargo-clippy",
allow(collapsible_if, cyclomatic_complexity)
)]
#[cfg_attr(feature = "cargo-clippy", allow(collapsible_if, cyclomatic_complexity))]
#[inline]
pub fn is_utf8_bidi(buffer: &[u8]) -> bool {
// As of rustc 1.25.0-nightly (73ac5d6a8 2018-01-11), this is faster
@ -771,9 +773,11 @@ pub fn is_utf8_bidi(buffer: &[u8]) -> bool {
// Three-byte normal
let second = unsafe { *(src.get_unchecked(read + 1)) };
let third = unsafe { *(src.get_unchecked(read + 2)) };
if ((UTF8_DATA.table[usize::from(second)] & unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
}) | (third >> 6))
if ((UTF8_DATA.table[usize::from(second)]
& unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
})
| (third >> 6))
!= 2
{
return true;
@ -784,9 +788,11 @@ pub fn is_utf8_bidi(buffer: &[u8]) -> bool {
// Three-byte normal, potentially bidi
let second = unsafe { *(src.get_unchecked(read + 1)) };
let third = unsafe { *(src.get_unchecked(read + 2)) };
if ((UTF8_DATA.table[usize::from(second)] & unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
}) | (third >> 6))
if ((UTF8_DATA.table[usize::from(second)]
& unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
})
| (third >> 6))
!= 2
{
return true;
@ -806,9 +812,11 @@ pub fn is_utf8_bidi(buffer: &[u8]) -> bool {
// Three-byte normal, potentially bidi
let second = unsafe { *(src.get_unchecked(read + 1)) };
let third = unsafe { *(src.get_unchecked(read + 2)) };
if ((UTF8_DATA.table[usize::from(second)] & unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
}) | (third >> 6))
if ((UTF8_DATA.table[usize::from(second)]
& unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
})
| (third >> 6))
!= 2
{
return true;
@ -840,9 +848,11 @@ pub fn is_utf8_bidi(buffer: &[u8]) -> bool {
// Three-byte special lower bound, potentially bidi
let second = unsafe { *(src.get_unchecked(read + 1)) };
let third = unsafe { *(src.get_unchecked(read + 2)) };
if ((UTF8_DATA.table[usize::from(second)] & unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
}) | (third >> 6))
if ((UTF8_DATA.table[usize::from(second)]
& unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
})
| (third >> 6))
!= 2
{
return true;
@ -857,9 +867,11 @@ pub fn is_utf8_bidi(buffer: &[u8]) -> bool {
// Three-byte special upper bound
let second = unsafe { *(src.get_unchecked(read + 1)) };
let third = unsafe { *(src.get_unchecked(read + 2)) };
if ((UTF8_DATA.table[usize::from(second)] & unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
}) | (third >> 6))
if ((UTF8_DATA.table[usize::from(second)]
& unsafe {
*(UTF8_DATA.table.get_unchecked(byte as usize + 0x80))
})
| (third >> 6))
!= 2
{
return true;
@ -1596,6 +1608,10 @@ pub fn convert_str_to_utf16(src: &str, dst: &mut [u16]) -> usize {
///
/// Returns the number of code units read and the number of bytes written.
///
/// Guarantees that the bytes in the destination beyond the number of
/// bytes claimed as written by the second item of the return tuple
/// are left unmodified.
///
/// Not all code units are read if there isn't enough output space.
///
/// Note that this method isn't designed for general streamability but for
@ -1603,25 +1619,37 @@ pub fn convert_str_to_utf16(src: &str, dst: &mut [u16]) -> usize {
/// if the input starts with or ends with an unpaired surrogate, those are
/// replaced with the REPLACEMENT CHARACTER.
///
/// Matches the semantics of `TextEncoder.encodeInto()` from the
/// Encoding Standard.
///
/// # Safety
///
/// Note that this function may write garbage beyond the number of bytes
/// indicated by the return value, so using a `&mut str` interpreted as
/// `&mut [u8]` as the destination is not safe. If you want to convert into
/// a `&mut str`, use `convert_utf16_to_str()` instead of this function.
#[inline]
/// If you want to convert into a `&mut str`, use
/// `convert_utf16_to_str_partial()` instead of using this function
/// together with the `unsafe` method `as_bytes_mut()` on `&mut str`.
#[inline(always)]
pub fn convert_utf16_to_utf8_partial(src: &[u16], dst: &mut [u8]) -> (usize, usize) {
let mut encoder = Utf8Encoder;
let (result, read, written) = encoder.encode_from_utf16_raw(src, dst, true);
debug_assert!(result == EncoderResult::OutputFull || read == src.len());
(read, written)
// The two functions called below are marked `inline(never)` to make
// transitions from the hot part (first function) into the cold part
// (second function) go through a return and another call to discouge
// the CPU from speculating from the hot code into the cold code.
// Letting the transitions be mere intra-function jumps, even to
// basic blocks out-of-lined to the end of the function would wipe
// away a quarter of Arabic encode performance on Haswell!
let (read, written) = convert_utf16_to_utf16_partial_inner(src, dst);
if unsafe { likely(read == src.len()) } {
return (read, written);
}
let (tail_read, tail_written) =
convert_utf16_to_utf16_partial_tail(&src[read..], &mut dst[written..]);
(read + tail_read, written + tail_written)
}
/// Converts potentially-invalid UTF-16 to valid UTF-8 with errors replaced
/// with the REPLACEMENT CHARACTER.
///
/// The length of the destination buffer must be at least the length of the
/// source buffer times three _plus one_.
/// source buffer times three.
///
/// Returns the number of bytes written.
///
@ -1631,13 +1659,12 @@ pub fn convert_utf16_to_utf8_partial(src: &[u16], dst: &mut [u8]) -> (usize, usi
///
/// # Safety
///
/// Note that this function may write garbage beyond the number of bytes
/// indicated by the return value, so using a `&mut str` interpreted as
/// `&mut [u8]` as the destination is not safe. If you want to convert into
/// a `&mut str`, use `convert_utf16_to_str()` instead of this function.
#[inline]
/// If you want to convert into a `&mut str`, use `convert_utf16_to_str()`
/// instead of using this function together with the `unsafe` method
/// `as_bytes_mut()` on `&mut str`.
#[inline(always)]
pub fn convert_utf16_to_utf8(src: &[u16], dst: &mut [u8]) -> usize {
assert!(dst.len() > src.len() * 3);
assert!(dst.len() >= src.len() * 3);
let (read, written) = convert_utf16_to_utf8_partial(src, dst);
debug_assert_eq!(read, src.len());
written
@ -1662,11 +1689,6 @@ pub fn convert_utf16_to_str_partial(src: &[u16], dst: &mut str) -> (usize, usize
let (read, written) = convert_utf16_to_utf8_partial(src, bytes);
let len = bytes.len();
let mut trail = written;
let max = ::std::cmp::min(len, trail + MAX_STRIDE_SIZE);
while trail < max {
bytes[trail] = 0;
trail += 1;
}
while trail < len && ((bytes[trail] & 0xC0) == 0x80) {
bytes[trail] = 0;
trail += 1;
@ -1679,16 +1701,16 @@ pub fn convert_utf16_to_str_partial(src: &[u16], dst: &mut str) -> (usize, usize
/// signaled using the Rust type system.
///
/// The length of the destination buffer must be at least the length of the
/// source buffer times three _plus one_.
/// source buffer times three.
///
/// Returns the number of bytes written.
///
/// # Panics
///
/// Panics if the destination buffer is shorter than stated above.
#[inline]
#[inline(always)]
pub fn convert_utf16_to_str(src: &[u16], dst: &mut str) -> usize {
assert!(dst.len() > src.len() * 3);
assert!(dst.len() >= src.len() * 3);
let (read, written) = convert_utf16_to_str_partial(src, dst);
debug_assert_eq!(read, src.len());
written

View File

@ -12,9 +12,10 @@ use ascii::ascii_to_basic_latin;
use ascii::basic_latin_to_ascii;
use ascii::validate_ascii;
use handles::*;
use mem::convert_utf16_to_utf8_partial;
use variant::*;
cfg_if!{
cfg_if! {
if #[cfg(feature = "simd-accel")] {
use ::std::intrinsics::unlikely;
use ::std::intrinsics::likely;
@ -233,10 +234,7 @@ pub fn utf8_valid_up_to(src: &[u8]) -> usize {
read
}
#[cfg_attr(
feature = "cargo-clippy",
allow(never_loop, cyclomatic_complexity)
)]
#[cfg_attr(feature = "cargo-clippy", allow(never_loop, cyclomatic_complexity))]
pub fn convert_utf8_to_utf16_up_to_invalid(src: &[u8], dst: &mut [u16]) -> (usize, usize) {
// This algorithm differs from the UTF-8 validation algorithm, but making
// this one consistent with that one makes this slower for reasons I don't
@ -612,6 +610,223 @@ impl Utf8Decoder {
);
}
#[cfg_attr(feature = "cargo-clippy", allow(never_loop))]
#[inline(never)]
pub fn convert_utf16_to_utf16_partial_inner(src: &[u16], dst: &mut [u8]) -> (usize, usize) {
let mut read = 0;
let mut written = 0;
'outer: loop {
let mut unit = {
let src_remaining = &src[read..];
let dst_remaining = &mut dst[written..];
let length = if dst_remaining.len() < src_remaining.len() {
dst_remaining.len()
} else {
src_remaining.len()
};
match unsafe {
basic_latin_to_ascii(src_remaining.as_ptr(), dst_remaining.as_mut_ptr(), length)
} {
None => {
read += length;
written += length;
return (read, written);
}
Some((non_ascii, consumed)) => {
read += consumed;
written += consumed;
non_ascii
}
}
};
'inner: loop {
// The following loop is only broken out of as a goto forward.
loop {
// Unfortunately, this check isn't enough for the compiler to elide
// the bound checks on writes to dst, which is why they are manually
// elided, which makes a measurable difference.
if written.checked_add(4).unwrap() > dst.len() {
return (read, written);
}
read += 1;
if unit < 0x800 {
unsafe {
*(dst.get_unchecked_mut(written)) = (unit >> 6) as u8 | 0xC0u8;
written += 1;
*(dst.get_unchecked_mut(written)) = (unit & 0x3F) as u8 | 0x80u8;
written += 1;
}
break;
}
let unit_minus_surrogate_start = unit.wrapping_sub(0xD800);
if unsafe { likely(unit_minus_surrogate_start > (0xDFFF - 0xD800)) } {
unsafe {
*(dst.get_unchecked_mut(written)) = (unit >> 12) as u8 | 0xE0u8;
written += 1;
*(dst.get_unchecked_mut(written)) = ((unit & 0xFC0) >> 6) as u8 | 0x80u8;
written += 1;
*(dst.get_unchecked_mut(written)) = (unit & 0x3F) as u8 | 0x80u8;
written += 1;
}
break;
}
if unsafe { likely(unit_minus_surrogate_start <= (0xDBFF - 0xD800)) } {
// high surrogate
// read > src.len() is impossible, but using
// >= instead of == allows the compiler to elide a bound check.
if read >= src.len() {
debug_assert_eq!(read, src.len());
// Unpaired surrogate at the end of the buffer.
unsafe {
*(dst.get_unchecked_mut(written)) = 0xEFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBDu8;
written += 1;
}
return (read, written);
}
let second = src[read];
let second_minus_low_surrogate_start = second.wrapping_sub(0xDC00);
if unsafe { likely(second_minus_low_surrogate_start <= (0xDFFF - 0xDC00)) } {
// The next code unit is a low surrogate. Advance position.
read += 1;
let astral = (u32::from(unit) << 10) + u32::from(second)
- (((0xD800u32 << 10) - 0x10000u32) + 0xDC00u32);
unsafe {
*(dst.get_unchecked_mut(written)) = (astral >> 18) as u8 | 0xF0u8;
written += 1;
*(dst.get_unchecked_mut(written)) =
((astral & 0x3F000u32) >> 12) as u8 | 0x80u8;
written += 1;
*(dst.get_unchecked_mut(written)) =
((astral & 0xFC0u32) >> 6) as u8 | 0x80u8;
written += 1;
*(dst.get_unchecked_mut(written)) = (astral & 0x3F) as u8 | 0x80u8;
written += 1;
}
break;
}
// The next code unit is not a low surrogate. Don't advance
// position and treat the high surrogate as unpaired.
// Fall through
}
// Unpaired low surrogate
unsafe {
*(dst.get_unchecked_mut(written)) = 0xEFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBDu8;
written += 1;
}
break;
}
// Now see if the next unit is Basic Latin
// read > src.len() is impossible, but using
// >= instead of == allows the compiler to elide a bound check.
if read >= src.len() {
debug_assert_eq!(read, src.len());
return (read, written);
}
unit = src[read];
if unsafe { unlikely(unit < 0x80) } {
// written > dst.len() is impossible, but using
// >= instead of == allows the compiler to elide a bound check.
if written >= dst.len() {
debug_assert_eq!(written, dst.len());
return (read, written);
}
dst[written] = unit as u8;
read += 1;
written += 1;
// Mysteriously, adding a punctuation check here makes
// the expected benificiary cases *slower*!
continue 'outer;
}
continue 'inner;
}
}
}
#[inline(never)]
pub fn convert_utf16_to_utf16_partial_tail(src: &[u16], dst: &mut [u8]) -> (usize, usize) {
// Everything below is cold code!
let mut read = 0;
let mut written = 0;
let mut unit = src[read];
// We now have up to 3 output slots, so an astral character
// will not fit.
if unit < 0x800 {
loop {
if unit < 0x80 {
if written >= dst.len() {
return (read, written);
}
read += 1;
dst[written] = unit as u8;
written += 1;
} else if unit < 0x800 {
if written + 2 > dst.len() {
return (read, written);
}
read += 1;
dst[written] = (unit >> 6) as u8 | 0xC0u8;
written += 1;
dst[written] = (unit & 0x3F) as u8 | 0x80u8;
written += 1;
} else {
return (read, written);
}
// read > src.len() is impossible, but using
// >= instead of == allows the compiler to elide a bound check.
if read >= src.len() {
debug_assert_eq!(read, src.len());
return (read, written);
}
unit = src[read];
}
}
// Could be an unpaired surrogate, but we'll need 3 output
// slots in any case.
if written + 3 > dst.len() {
return (read, written);
}
read += 1;
let unit_minus_surrogate_start = unit.wrapping_sub(0xD800);
if unit_minus_surrogate_start <= (0xDFFF - 0xD800) {
// Got surrogate
if unit_minus_surrogate_start <= (0xDBFF - 0xD800) {
// Got high surrogate
if read >= src.len() {
// Unpaired high surrogate
unit = 0xFFFD;
} else {
let second = src[read];
if in_inclusive_range16(second, 0xDC00, 0xDFFF) {
// Valid surrogate pair, but we know it won't fit.
read -= 1;
return (read, written);
}
// Unpaired high
unit = 0xFFFD;
}
} else {
// Unpaired low
unit = 0xFFFD;
}
}
dst[written] = (unit >> 12) as u8 | 0xE0u8;
written += 1;
dst[written] = ((unit & 0xFC0) >> 6) as u8 | 0x80u8;
written += 1;
dst[written] = (unit & 0x3F) as u8 | 0x80u8;
written += 1;
debug_assert_eq!(written, dst.len());
(read, written)
}
pub struct Utf8Encoder;
impl Utf8Encoder {
@ -623,7 +838,7 @@ impl Utf8Encoder {
&self,
u16_length: usize,
) -> Option<usize> {
checked_add(1, u16_length.checked_mul(3))
u16_length.checked_mul(3)
}
pub fn max_buffer_length_from_utf8_without_replacement(
@ -633,150 +848,22 @@ impl Utf8Encoder {
Some(byte_length)
}
#[cfg_attr(feature = "cargo-clippy", allow(never_loop))]
pub fn encode_from_utf16_raw(
&mut self,
src: &[u16],
dst: &mut [u8],
_last: bool,
) -> (EncoderResult, usize, usize) {
let mut read = 0;
let mut written = 0;
'outer: loop {
let mut unit = {
let src_remaining = &src[read..];
let dst_remaining = &mut dst[written..];
let (pending, length) = if dst_remaining.len() < src_remaining.len() {
(EncoderResult::OutputFull, dst_remaining.len())
} else {
(EncoderResult::InputEmpty, src_remaining.len())
};
match unsafe {
basic_latin_to_ascii(src_remaining.as_ptr(), dst_remaining.as_mut_ptr(), length)
} {
None => {
read += length;
written += length;
return (pending, read, written);
}
Some((non_ascii, consumed)) => {
read += consumed;
written += consumed;
non_ascii
}
}
};
'inner: loop {
// The following loop is only broken out of as a goto forward.
loop {
// Unfortunately, this check isn't enough for the compiler to elide
// the bound checks on writes to dst, which is why they are manually
// elided, which makes a measurable difference.
if written.checked_add(4).unwrap() > dst.len() {
return (EncoderResult::OutputFull, read, written);
}
read += 1;
if unit < 0x800 {
unsafe {
*(dst.get_unchecked_mut(written)) = (unit >> 6) as u8 | 0xC0u8;
written += 1;
*(dst.get_unchecked_mut(written)) = (unit & 0x3F) as u8 | 0x80u8;
written += 1;
}
break;
}
let unit_minus_surrogate_start = unit.wrapping_sub(0xD800);
if unsafe { likely(unit_minus_surrogate_start > (0xDFFF - 0xD800)) } {
unsafe {
*(dst.get_unchecked_mut(written)) = (unit >> 12) as u8 | 0xE0u8;
written += 1;
*(dst.get_unchecked_mut(written)) =
((unit & 0xFC0) >> 6) as u8 | 0x80u8;
written += 1;
*(dst.get_unchecked_mut(written)) = (unit & 0x3F) as u8 | 0x80u8;
written += 1;
}
break;
}
if unsafe { likely(unit_minus_surrogate_start <= (0xDBFF - 0xD800)) } {
// high surrogate
// read > src.len() is impossible, but using
// >= instead of == allows the compiler to elide a bound check.
if read >= src.len() {
debug_assert_eq!(read, src.len());
// Unpaired surrogate at the end of the buffer.
unsafe {
*(dst.get_unchecked_mut(written)) = 0xEFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBDu8;
written += 1;
}
return (EncoderResult::InputEmpty, read, written);
}
let second = src[read];
let second_minus_low_surrogate_start = second.wrapping_sub(0xDC00);
if unsafe { likely(second_minus_low_surrogate_start <= (0xDFFF - 0xDC00)) }
{
// The next code unit is a low surrogate. Advance position.
read += 1;
let astral = (u32::from(unit) << 10) + u32::from(second)
- (((0xD800u32 << 10) - 0x10000u32) + 0xDC00u32);
unsafe {
*(dst.get_unchecked_mut(written)) = (astral >> 18) as u8 | 0xF0u8;
written += 1;
*(dst.get_unchecked_mut(written)) =
((astral & 0x3F000u32) >> 12) as u8 | 0x80u8;
written += 1;
*(dst.get_unchecked_mut(written)) =
((astral & 0xFC0u32) >> 6) as u8 | 0x80u8;
written += 1;
*(dst.get_unchecked_mut(written)) = (astral & 0x3F) as u8 | 0x80u8;
written += 1;
}
break;
}
// The next code unit is not a low surrogate. Don't advance
// position and treat the high surrogate as unpaired.
// Fall through
}
// Unpaired low surrogate
unsafe {
*(dst.get_unchecked_mut(written)) = 0xEFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBFu8;
written += 1;
*(dst.get_unchecked_mut(written)) = 0xBDu8;
written += 1;
}
break;
}
// Now see if the next unit is Basic Latin
// read > src.len() is impossible, but using
// >= instead of == allows the compiler to elide a bound check.
if read >= src.len() {
debug_assert_eq!(read, src.len());
return (EncoderResult::InputEmpty, read, written);
}
unit = src[read];
if unsafe { unlikely(unit < 0x80) } {
// written > dst.len() is impossible, but using
// >= instead of == allows the compiler to elide a bound check.
if written >= dst.len() {
debug_assert_eq!(written, dst.len());
return (EncoderResult::OutputFull, read, written);
}
dst[written] = unit as u8;
read += 1;
written += 1;
// Mysteriously, adding a punctuation check here makes
// the expected benificiary cases *slower*!
continue 'outer;
}
continue 'inner;
}
}
let (read, written) = convert_utf16_to_utf8_partial(src, dst);
(
if read == src.len() {
EncoderResult::InputEmpty
} else {
EncoderResult::OutputFull
},
read,
written,
)
}
pub fn encode_from_utf8_raw(
@ -829,6 +916,39 @@ mod tests {
encode_from_utf8(UTF_8, string, expect);
}
fn encode_utf8_from_utf16_with_output_limit(
string: &[u16],
expect: &str,
limit: usize,
expect_result: EncoderResult,
) {
let mut dst = Vec::new();
{
dst.resize(limit, 0u8);
let mut encoder = UTF_8.new_encoder();
let (result, read, written) =
encoder.encode_from_utf16_without_replacement(string, &mut dst, false);
assert_eq!(result, expect_result);
if expect_result == EncoderResult::InputEmpty {
assert_eq!(read, string.len());
}
assert_eq!(&dst[..written], expect.as_bytes());
}
{
dst.resize(64, 0u8);
for (i, elem) in dst.iter_mut().enumerate() {
*elem = i as u8;
}
let mut encoder = UTF_8.new_encoder();
let (_, _, mut j) =
encoder.encode_from_utf16_without_replacement(string, &mut dst, false);
while j < dst.len() {
assert_eq!(usize::from(dst[j]), j);
j += 1;
}
}
}
#[test]
fn test_utf8_decode() {
// Empty
@ -1013,6 +1133,404 @@ mod tests {
encode_utf8_from_utf16(&[0xDC00, 0xDEDE], "\u{FFFD}\u{FFFD}".as_bytes());
}
#[test]
fn test_encode_utf8_from_utf16_with_output_limit() {
encode_utf8_from_utf16_with_output_limit(&[0x0062], "\u{62}", 1, EncoderResult::InputEmpty);
encode_utf8_from_utf16_with_output_limit(&[0x00A7], "\u{A7}", 2, EncoderResult::InputEmpty);
encode_utf8_from_utf16_with_output_limit(
&[0x2603],
"\u{2603}",
3,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDCA9],
"\u{1F4A9}",
4,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(&[0x00A7], "", 1, EncoderResult::OutputFull);
encode_utf8_from_utf16_with_output_limit(&[0x2603], "", 2, EncoderResult::OutputFull);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDCA9],
"",
3,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x0062],
"\u{63}\u{62}",
2,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00A7],
"\u{63}\u{A7}",
3,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x2603],
"\u{63}\u{2603}",
4,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0xD83D, 0xDCA9],
"\u{63}\u{1F4A9}",
5,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00A7],
"\u{63}",
2,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x2603],
"\u{63}",
3,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0xD83D, 0xDCA9],
"\u{63}",
4,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x00B6, 0x0062],
"\u{B6}\u{62}",
3,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x00B6, 0x00A7],
"\u{B6}\u{A7}",
4,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x00B6, 0x2603],
"\u{B6}\u{2603}",
5,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x00B6, 0xD83D, 0xDCA9],
"\u{B6}\u{1F4A9}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x00B6, 0x00A7],
"\u{B6}",
3,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x00B6, 0x2603],
"\u{B6}",
4,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x00B6, 0xD83D, 0xDCA9],
"\u{B6}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x0062],
"\u{263A}\u{62}",
4,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x00A7],
"\u{263A}\u{A7}",
5,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x2603],
"\u{263A}\u{2603}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0xD83D, 0xDCA9],
"\u{263A}\u{1F4A9}",
7,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x00A7],
"\u{263A}",
4,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x2603],
"\u{263A}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0xD83D, 0xDCA9],
"\u{263A}",
6,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDE0E, 0x0062],
"\u{1F60E}\u{62}",
5,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDE0E, 0x00A7],
"\u{1F60E}\u{A7}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDE0E, 0x2603],
"\u{1F60E}\u{2603}",
7,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDE0E, 0xD83D, 0xDCA9],
"\u{1F60E}\u{1F4A9}",
8,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDE0E, 0x00A7],
"\u{1F60E}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDE0E, 0x2603],
"\u{1F60E}",
6,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0xD83D, 0xDE0E, 0xD83D, 0xDCA9],
"\u{1F60E}",
7,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x0062, 0x0062],
"\u{63}\u{B6}\u{62}\u{62}",
5,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x0062, 0x0062],
"\u{63}\u{B6}\u{62}",
4,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x0062, 0x0062, 0x0062],
"\u{63}\u{B6}\u{62}\u{62}\u{62}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x0062, 0x0062, 0x0062],
"\u{63}\u{B6}\u{62}\u{62}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x0062, 0x0062],
"\u{263A}\u{62}\u{62}",
5,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x0062, 0x0062],
"\u{263A}\u{62}",
4,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x0062, 0x0062, 0x0062],
"\u{263A}\u{62}\u{62}\u{62}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x0062, 0x0062, 0x0062],
"\u{263A}\u{62}\u{62}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x00A7],
"\u{63}\u{B6}\u{A7}",
5,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x00A7],
"\u{63}\u{B6}",
4,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x00A7, 0x0062],
"\u{63}\u{B6}\u{A7}\u{62}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x00A7, 0x0062],
"\u{63}\u{B6}\u{A7}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x00A7, 0x0062],
"\u{263A}\u{A7}\u{62}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x00A7, 0x0062],
"\u{263A}\u{A7}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x0062, 0x00A7],
"\u{63}\u{B6}\u{62}\u{A7}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x0062, 0x00A7],
"\u{63}\u{B6}\u{62}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x0062, 0x00A7],
"\u{263A}\u{62}\u{A7}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x0062, 0x00A7],
"\u{263A}\u{62}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x2603],
"\u{63}\u{B6}\u{2603}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0x2603],
"\u{63}\u{B6}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x2603],
"\u{263A}\u{2603}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0x2603],
"\u{263A}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0xD83D],
"\u{63}\u{B6}\u{FFFD}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0xD83D],
"\u{63}\u{B6}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0xD83D],
"\u{263A}\u{FFFD}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0xD83D],
"\u{263A}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0xDCA9],
"\u{63}\u{B6}\u{FFFD}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x0063, 0x00B6, 0xDCA9],
"\u{63}\u{B6}",
5,
EncoderResult::OutputFull,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0xDCA9],
"\u{263A}\u{FFFD}",
6,
EncoderResult::InputEmpty,
);
encode_utf8_from_utf16_with_output_limit(
&[0x263A, 0xDCA9],
"\u{263A}",
5,
EncoderResult::OutputFull,
);
}
#[test]
fn test_utf8_max_length_from_utf16() {
let mut encoder = UTF_8.new_encoder();
@ -1114,7 +1632,6 @@ mod tests {
assert!(!had_errors);
assert_eq!(output[0], 0x00E4);
}
}
}

View File

@ -11,7 +11,7 @@ use super::*;
use handles::*;
use variant::*;
cfg_if!{
cfg_if! {
if #[cfg(feature = "simd-accel")] {
use simd_funcs::*;
use simd::u16x8;

View File

@ -197,11 +197,14 @@ const char nsXPLookAndFeel::sColorPrefs[][41] = {
"ui.-moz-mac-source-list-selection",
"ui.-moz-mac-active-source-list-selection",
"ui.-moz-mac-tooltip",
"ui.-moz-win-accentcolor",
"ui.-moz-win-accentcolortext",
"ui.-moz-win-mediatext",
"ui.-moz-win-communicationstext",
"ui.-moz-nativehyperlinktext",
"ui.-moz-comboboxtext",
"ui.-moz-combobox"};
"ui.-moz-combobox",
"ui.-moz-gtk-info-bar-text"};
int32_t nsXPLookAndFeel::sCachedColors[LookAndFeel::eColorID_LAST_COLOR] = {0};
int32_t nsXPLookAndFeel::sCachedColorBits[COLOR_CACHE_SIZE] = {0};

View File

@ -19,8 +19,8 @@ use conversions::encoding_rs::Encoding;
/// Required math stated in the docs of
/// `convert_utf16_to_utf8()`.
#[inline(always)]
fn times_three_plus_one(a: usize) -> Option<usize> {
a.checked_mul(3)?.checked_add(1)
fn times_three(a: usize) -> Option<usize> {
a.checked_mul(3)
}
#[inline(always)]
@ -321,7 +321,7 @@ impl nsACString {
// point. BUT if the worst case fits inside the inline capacity of an autostring, we skip
// the ASCII stuff.
let worst_case_needed = if let Some(inline_capacity) = self.inline_capacity() {
let worst_case = times_three_plus_one(other.len()).ok_or(())?;
let worst_case = times_three(other.len()).ok_or(())?;
if worst_case <= inline_capacity {
Some(worst_case)
} else {
@ -340,7 +340,7 @@ impl nsACString {
return Ok(handle.finish(old_len + written, true));
}
let filled = old_len + written;
let needed = times_three_plus_one(left).ok_or(())?;
let needed = times_three(left).ok_or(())?;
let new_len = filled.checked_add(needed).ok_or(())?;
unsafe {
handle.restart_bulk_write(new_len, filled, false)?;
@ -351,7 +351,7 @@ impl nsACString {
let needed = if let Some(n) = worst_case_needed {
n
} else {
times_three_plus_one(other.len()).ok_or(())?
times_three(other.len()).ok_or(())?
};
let new_len = old_len.checked_add(needed).ok_or(())?;
let mut handle = unsafe { self.bulk_write(new_len, old_len, false)? };

View File

@ -51,13 +51,13 @@ char* ToNewUTF8String(const nsAString& aSource, uint32_t* aUTF8Count) {
// The uses of this function seem temporary enough that it's not
// worthwhile to be fancy about the allocation size. Let's just use
// the worst case.
// Times 3 plus 2, because ConvertUTF16toUTF8 requires times 3 plus 1 and
// Times 3 plus 1, because ConvertUTF16toUTF8 requires times 3 and
// then we have the terminator.
// Using CheckedInt<uint32_t>, because aUTF8Count is uint32_t* for
// historical reasons.
mozilla::CheckedInt<uint32_t> destLen(len);
destLen *= 3;
destLen += 2;
destLen += 1;
if (!destLen.isValid()) {
return nullptr;
}

View File

@ -129,8 +129,7 @@ inline void ConvertLatin1toUTF16(mozilla::Span<const char> aSource,
/**
* Lone surrogates are replaced with the REPLACEMENT CHARACTER.
*
* The length of aDest must be at least the length of aSource times three
* _plus one_.
* The length of aDest must be at least the length of aSource times three.
*
* Returns the number of code units written.
*/