author | Margaret Leibovic <margaret.leibovic@gmail.com> |
Sun, 28 Jul 2013 12:08:01 -0700 | |
changeset 143466 | c183395271416d3b96ebcded68fac6c31f9dc3a8 |
parent 143465 | 008632d5460705de6ae26aa47db503e732758735 (current diff) |
parent 140267 | 73b69c146ca6926b4a72bb484550e5afe04b93cc (diff) |
child 143467 | ba63590416075b1c196c15fc27bcdcad358ef1c5 |
push id | 25130 |
push user | lrocha@mozilla.com |
push date | Wed, 21 Aug 2013 09:41:27 +0000 |
treeherder | mozilla-central@b2486721572e [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 25.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/accessible/src/base/ARIAMap.cpp +++ b/accessible/src/base/ARIAMap.cpp @@ -171,19 +171,20 @@ static nsRoleMapEntry sWAIRoleMaps[] = { // grid &nsGkAtoms::grid, roles::TABLE, kUseMapRole, eNoValue, eNoAction, eNoLiveAttr, eSelect | eTable, - states::FOCUSABLE, + kNoReqStates, eARIAMultiSelectable, - eARIAReadonlyOrEditable + eARIAReadonlyOrEditable, + eFocusableUntilDisabled }, { // gridcell &nsGkAtoms::gridcell, roles::GRID_CELL, kUseMapRole, eNoValue, eNoAction, eNoLiveAttr, @@ -258,17 +259,18 @@ static nsRoleMapEntry sWAIRoleMaps[] = roles::LISTBOX, kUseMapRole, eNoValue, eNoAction, eNoLiveAttr, eListControl | eSelect, kNoReqStates, eARIAMultiSelectable, - eARIAReadonly + eARIAReadonly, + eFocusableUntilDisabled }, { // listitem &nsGkAtoms::listitem, roles::LISTITEM, kUseMapRole, eNoValue, eNoAction, // XXX: should depend on state, parent accessible eNoLiveAttr, @@ -534,17 +536,17 @@ static nsRoleMapEntry sWAIRoleMaps[] = eARIASelectable }, { // tablist &nsGkAtoms::tablist, roles::PAGETABLIST, kUseMapRole, eNoValue, eNoAction, - ePoliteLiveAttr, + eNoLiveAttr, eSelect, kNoReqStates }, { // tabpanel &nsGkAtoms::tabpanel, roles::PROPERTYPAGE, kUseMapRole, eNoValue, @@ -600,29 +602,31 @@ static nsRoleMapEntry sWAIRoleMaps[] = roles::OUTLINE, kUseMapRole, eNoValue, eNoAction, eNoLiveAttr, eSelect, kNoReqStates, eARIAReadonly, - eARIAMultiSelectable + eARIAMultiSelectable, + eFocusableUntilDisabled }, { // treegrid &nsGkAtoms::treegrid, roles::TREE_TABLE, kUseMapRole, eNoValue, eNoAction, eNoLiveAttr, eSelect | eTable, kNoReqStates, eARIAReadonly, - eARIAMultiSelectable + eARIAMultiSelectable, + eFocusableUntilDisabled }, { // treeitem &nsGkAtoms::treeitem, roles::OUTLINEITEM, kUseMapRole, eNoValue, eActivateAction, // XXX: should expose second 'expand/collapse' action based // on states
--- a/accessible/src/base/ARIAStateMap.cpp +++ b/accessible/src/base/ARIAStateMap.cpp @@ -1,15 +1,16 @@ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * 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/. */ #include "ARIAMap.h" +#include "nsAccUtils.h" #include "States.h" #include "mozilla/dom/Element.h" using namespace mozilla; using namespace mozilla::a11y; using namespace mozilla::a11y::aria; @@ -322,16 +323,26 @@ aria::MapToState(EStateRule aRule, dom:: { if (!aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow) && !aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext)) *aState |= states::MIXED; return true; } + case eFocusableUntilDisabled: + { + if (!nsAccUtils::HasDefinedARIAToken(aElement, nsGkAtoms::aria_disabled) || + aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_disabled, + nsGkAtoms::_false, eCaseMatters)) + *aState |= states::FOCUSABLE; + + return true; + } + default: return false; } } static void MapEnumType(dom::Element* aElement, uint64_t* aState, const EnumTypeData& aData) {
--- a/accessible/src/base/ARIAStateMap.h +++ b/accessible/src/base/ARIAStateMap.h @@ -39,17 +39,18 @@ enum EStateRule eARIAPressed, eARIAReadonly, eARIAReadonlyOrEditable, eARIAReadonlyOrEditableIfDefined, eARIARequired, eARIASelectable, eARIASelectableIfDefined, eReadonlyUntilEditable, - eIndeterminateIfNoValue + eIndeterminateIfNoValue, + eFocusableUntilDisabled }; /** * Expose the accessible states for the given element accordingly to state * mapping rule. * * @param aRule [in] state mapping rule ID * @param aElement [in] node of the accessible
--- a/accessible/src/base/DocManager.cpp +++ b/accessible/src/base/DocManager.cpp @@ -112,20 +112,20 @@ DocManager::Shutdown() progress->RemoveProgressListener(static_cast<nsIWebProgressListener*>(this)); ClearDocCache(); } //////////////////////////////////////////////////////////////////////////////// // nsISupports -NS_IMPL_THREADSAFE_ISUPPORTS3(DocManager, - nsIWebProgressListener, - nsIDOMEventListener, - nsISupportsWeakReference) +NS_IMPL_ISUPPORTS3(DocManager, + nsIWebProgressListener, + nsIDOMEventListener, + nsISupportsWeakReference) //////////////////////////////////////////////////////////////////////////////// // nsIWebProgressListener NS_IMETHODIMP DocManager::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aStateFlags, nsresult aStatus)
--- a/accessible/src/base/DocManager.h +++ b/accessible/src/base/DocManager.h @@ -24,17 +24,17 @@ class DocAccessible; */ class DocManager : public nsIWebProgressListener, public nsIDOMEventListener, public nsSupportsWeakReference { public: virtual ~DocManager() { } - NS_DECL_ISUPPORTS + NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIWEBPROGRESSLISTENER NS_DECL_NSIDOMEVENTLISTENER /** * Return document accessible for the given DOM node. */ DocAccessible* GetDocAccessible(nsIDocument* aDocument);
--- a/accessible/src/base/nsAccessiblePivot.cpp +++ b/accessible/src/base/nsAccessiblePivot.cpp @@ -293,36 +293,119 @@ nsAccessiblePivot::MoveLast(nsIAccessibl NS_ENSURE_SUCCESS(rv, rv); if (accessible) *aResult = MovePivotInternal(accessible, nsAccessiblePivot::REASON_LAST); return NS_OK; } -// TODO: Implement NS_IMETHODIMP nsAccessiblePivot::MoveNextByText(TextBoundaryType aBoundary, bool* aResult) { NS_ENSURE_ARG(aResult); *aResult = false; - return NS_ERROR_NOT_IMPLEMENTED; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + HyperTextAccessible* text = mPosition->AsHyperText(); + Accessible* oldPosition = mPosition; + while (!text) { + oldPosition = mPosition; + mPosition = mPosition->Parent(); + text = mPosition->AsHyperText(); + } + + if (mEndOffset == -1) + mEndOffset = text != oldPosition ? text->GetChildOffset(oldPosition) : 0; + + if (mEndOffset == text->CharacterCount()) + return NS_OK; + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundary) { + case CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + nsAutoString unusedText; + int32_t newStart = 0, newEnd = 0; + text->GetTextAtOffset(mEndOffset, endBoundary, &newStart, &mEndOffset, unusedText); + text->GetTextBeforeOffset(mEndOffset, startBoundary, &newStart, &newEnd, + unusedText); + mStartOffset = newEnd == mEndOffset ? newStart : newEnd; + + *aResult = true; + + NotifyOfPivotChange(mPosition, oldStart, oldEnd, + nsIAccessiblePivot::REASON_TEXT); + return NS_OK; } -// TODO: Implement NS_IMETHODIMP nsAccessiblePivot::MovePreviousByText(TextBoundaryType aBoundary, bool* aResult) { NS_ENSURE_ARG(aResult); *aResult = false; - return NS_ERROR_NOT_IMPLEMENTED; + int32_t oldStart = mStartOffset, oldEnd = mEndOffset; + HyperTextAccessible* text = mPosition->AsHyperText(); + Accessible* oldPosition = mPosition; + while (!text) { + oldPosition = mPosition; + mPosition = mPosition->Parent(); + text = mPosition->AsHyperText(); + } + + if (mStartOffset == -1) + mStartOffset = text != oldPosition ? text->GetChildOffset(oldPosition) : 0; + + if (mStartOffset == 0) + return NS_OK; + + AccessibleTextBoundary startBoundary, endBoundary; + switch (aBoundary) { + case CHAR_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_CHAR; + endBoundary = nsIAccessibleText::BOUNDARY_CHAR; + break; + case WORD_BOUNDARY: + startBoundary = nsIAccessibleText::BOUNDARY_WORD_START; + endBoundary = nsIAccessibleText::BOUNDARY_WORD_END; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + nsAutoString unusedText; + int32_t newStart = 0, newEnd = 0; + text->GetTextBeforeOffset(mStartOffset, startBoundary, &newStart, &newEnd, + unusedText); + if (newStart < mStartOffset) + mStartOffset = newEnd == mStartOffset ? newStart : newEnd; + else // XXX: In certain odd cases newStart is equal to mStartOffset + text->GetTextBeforeOffset(mStartOffset - 1, startBoundary, &newStart, + &mStartOffset, unusedText); + text->GetTextAtOffset(mStartOffset, endBoundary, &newStart, &mEndOffset, + unusedText); + + *aResult = true; + + NotifyOfPivotChange(mPosition, oldStart, oldEnd, + nsIAccessiblePivot::REASON_TEXT); + return NS_OK; } NS_IMETHODIMP nsAccessiblePivot::MoveToPoint(nsIAccessibleTraversalRule* aRule, int32_t aX, int32_t aY, bool aIgnoreNoMatch, bool* aResult) { NS_ENSURE_ARG_POINTER(aResult);
--- a/accessible/src/base/nsCoreUtils.cpp +++ b/accessible/src/base/nsCoreUtils.cpp @@ -27,16 +27,17 @@ #include "nsEventStateManager.h" #include "nsISelectionPrivate.h" #include "nsISelectionController.h" #include "nsPIDOMWindow.h" #include "nsGUIEvent.h" #include "nsView.h" #include "nsLayoutUtils.h" #include "nsGkAtoms.h" +#include "nsDOMTouchEvent.h" #include "nsComponentManagerUtils.h" #include "nsIInterfaceRequestorUtils.h" #include "mozilla/dom/Element.h" #include "nsITreeBoxObject.h" #include "nsITreeColumns.h" @@ -108,50 +109,24 @@ nsCoreUtils::DispatchClickEvent(nsITreeB nsPresContext* presContext = presShell->GetPresContext(); int32_t cnvdX = presContext->CSSPixelsToDevPixels(tcX + x + 1) + presContext->AppUnitsToDevPixels(offset.x); int32_t cnvdY = presContext->CSSPixelsToDevPixels(tcY + y + 1) + presContext->AppUnitsToDevPixels(offset.y); + // XUL is just desktop, so there is no real reason for senfing touch events. DispatchMouseEvent(NS_MOUSE_BUTTON_DOWN, cnvdX, cnvdY, tcContent, tcFrame, presShell, rootWidget); DispatchMouseEvent(NS_MOUSE_BUTTON_UP, cnvdX, cnvdY, tcContent, tcFrame, presShell, rootWidget); } -bool -nsCoreUtils::DispatchMouseEvent(uint32_t aEventType, - nsIPresShell *aPresShell, - nsIContent *aContent) -{ - nsIFrame *frame = aContent->GetPrimaryFrame(); - if (!frame) - return false; - - // Compute x and y coordinates. - nsPoint point; - nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point); - if (!widget) - return false; - - nsSize size = frame->GetSize(); - - nsPresContext* presContext = aPresShell->GetPresContext(); - - int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2); - int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2); - - // Fire mouse event. - DispatchMouseEvent(aEventType, x, y, aContent, frame, aPresShell, widget); - return true; -} - void nsCoreUtils::DispatchMouseEvent(uint32_t aEventType, int32_t aX, int32_t aY, nsIContent *aContent, nsIFrame *aFrame, nsIPresShell *aPresShell, nsIWidget *aRootWidget) { nsMouseEvent event(true, aEventType, aRootWidget, nsMouseEvent::eReal, nsMouseEvent::eNormal); @@ -161,16 +136,38 @@ nsCoreUtils::DispatchMouseEvent(uint32_t event.button = nsMouseEvent::eLeftButton; event.time = PR_IntervalNow(); event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN; nsEventStatus status = nsEventStatus_eIgnore; aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status); } +void +nsCoreUtils::DispatchTouchEvent(uint32_t aEventType, int32_t aX, int32_t aY, + nsIContent* aContent, nsIFrame* aFrame, + nsIPresShell* aPresShell, nsIWidget* aRootWidget) +{ + if (!nsDOMTouchEvent::PrefEnabled()) + return; + + nsTouchEvent event(true, aEventType, aRootWidget); + + event.time = PR_IntervalNow(); + + // XXX: Touch has an identifier of -1 to hint that it is synthesized. + nsRefPtr<mozilla::dom::Touch> t = + new mozilla::dom::Touch(-1, nsIntPoint(aX, aY), + nsIntPoint(1, 1), 0.0f, 1.0f); + t->SetTarget(aContent); + event.touches.AppendElement(t); + nsEventStatus status = nsEventStatus_eIgnore; + aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status); +} + uint32_t nsCoreUtils::GetAccessKeyFor(nsIContent* aContent) { // Accesskeys are registered by @accesskey attribute only. At first check // whether it is presented on the given element to avoid the slow // nsEventStateManager::GetRegisteredAccessKey() method. if (!aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::accesskey)) return 0;
--- a/accessible/src/base/nsCoreUtils.h +++ b/accessible/src/base/nsCoreUtils.h @@ -44,39 +44,42 @@ public: */ static void DispatchClickEvent(nsITreeBoxObject *aTreeBoxObj, int32_t aRowIndex, nsITreeColumn *aColumn, const nsCString& aPseudoElt = EmptyCString()); /** * Send mouse event to the given element. * - * @param aEventType [in] an event type (see nsGUIEvent.h for constants) - * @param aPresShell [in] the presshell for the given element - * @param aContent [in] the element - */ - static bool DispatchMouseEvent(uint32_t aEventType, - nsIPresShell *aPresShell, - nsIContent *aContent); - - /** - * Send mouse event to the given element. - * * @param aEventType [in] an event type (see nsGUIEvent.h for constants) * @param aX [in] x coordinate in dev pixels * @param aY [in] y coordinate in dev pixels * @param aContent [in] the element * @param aFrame [in] frame of the element * @param aPresShell [in] the presshell for the element * @param aRootWidget [in] the root widget of the element */ static void DispatchMouseEvent(uint32_t aEventType, int32_t aX, int32_t aY, nsIContent *aContent, nsIFrame *aFrame, - nsIPresShell *aPresShell, - nsIWidget *aRootWidget); + nsIPresShell *aPresShell, nsIWidget *aRootWidget); + + /** + * Send a touch event with a single touch point to the given element. + * + * @param aEventType [in] an event type (see nsGUIEvent.h for constants) + * @param aX [in] x coordinate in dev pixels + * @param aY [in] y coordinate in dev pixels + * @param aContent [in] the element + * @param aFrame [in] frame of the element + * @param aPresShell [in] the presshell for the element + * @param aRootWidget [in] the root widget of the element + */ + static void DispatchTouchEvent(uint32_t aEventType, int32_t aX, int32_t aY, + nsIContent* aContent, nsIFrame* aFrame, + nsIPresShell* aPresShell, nsIWidget* aRootWidget); /** * Return an accesskey registered on the given element by * nsEventStateManager or 0 if there is no registered accesskey. * * @param aContent - the given element. */ static uint32_t GetAccessKeyFor(nsIContent *aContent);
--- a/accessible/src/base/nsTextEquivUtils.cpp +++ b/accessible/src/base/nsTextEquivUtils.cpp @@ -50,29 +50,16 @@ nsTextEquivUtils::GetNameFromSubtree(Acc } } sInitiatorAcc = nullptr; return NS_OK; } -void -nsTextEquivUtils::GetTextEquivFromSubtree(Accessible* aAccessible, - nsString& aTextEquiv) -{ - aTextEquiv.Truncate(); - - uint32_t nameRule = GetRoleRule(aAccessible->Role()); - if (nameRule & eNameFromSubtreeIfReqRule) { - AppendFromAccessibleChildren(aAccessible, &aTextEquiv); - aTextEquiv.CompressWhitespace(); - } -} - nsresult nsTextEquivUtils::GetTextEquivFromIDRefs(Accessible* aAccessible, nsIAtom *aIDRefsAttr, nsAString& aTextEquiv) { aTextEquiv.Truncate(); nsIContent* content = aAccessible->GetContent();
--- a/accessible/src/base/nsTextEquivUtils.h +++ b/accessible/src/base/nsTextEquivUtils.h @@ -50,21 +50,26 @@ public: * @param aAccessible [in] the given accessible * @param aName [out] accessible name */ static nsresult GetNameFromSubtree(Accessible* aAccessible, nsAString& aName); /** * Calculates text equivalent from the subtree. Similar to GetNameFromSubtree. - * The difference it returns not empty result for things like HTML p, i.e. - * if the role has eNameFromSubtreeIfReq rule. + * However it returns not empty result for things like HTML p. */ static void GetTextEquivFromSubtree(Accessible* aAccessible, - nsString& aTextEquiv); + nsString& aTextEquiv) + { + aTextEquiv.Truncate(); + + AppendFromAccessibleChildren(aAccessible, &aTextEquiv); + aTextEquiv.CompressWhitespace(); + } /** * Calculates text equivalent for the given accessible from its IDRefs * attribute (like aria-labelledby or aria-describedby). * * @param aAccessible [in] the accessible text equivalent is computed for * @param aIDRefsAttr [in] IDRefs attribute on DOM node of the accessible * @param aTextEquiv [out] result text equivalent
--- a/accessible/src/generic/Accessible.cpp +++ b/accessible/src/generic/Accessible.cpp @@ -1676,16 +1676,22 @@ Accessible::Value(nsString& aValue) if (!mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext, aValue)) { mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::aria_valuenow, aValue); } return; } + // Value of textbox is a textified subtree. + if (mRoleMapEntry->Is(nsGkAtoms::textbox)) { + nsTextEquivUtils::GetTextEquivFromSubtree(this, aValue); + return; + } + // Value of combobox is a text of current or selected item. if (mRoleMapEntry->Is(nsGkAtoms::combobox)) { Accessible* option = CurrentItem(); if (!option) { Accessible* listbox = nullptr; IDRefsIterator iter(mDoc, mContent, nsGkAtoms::aria_owns); while ((listbox = iter.Next()) && !listbox->IsListControl()); @@ -2282,23 +2288,38 @@ Accessible::DispatchClickEvent(nsIConten nsIPresShell* presShell = mDoc->PresShell(); // Scroll into view. presShell->ScrollContentIntoView(aContent, nsIPresShell::ScrollAxis(), nsIPresShell::ScrollAxis(), nsIPresShell::SCROLL_OVERFLOW_HIDDEN); - // Fire mouse down and mouse up events. - bool res = nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_DOWN, presShell, - aContent); - if (!res) + nsIFrame* frame = aContent->GetPrimaryFrame(); + if (!frame) + return; + + // Compute x and y coordinates. + nsPoint point; + nsCOMPtr<nsIWidget> widget = frame->GetNearestWidget(point); + if (!widget) return; - nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_UP, presShell, aContent); + nsSize size = frame->GetSize(); + + nsPresContext* presContext = presShell->GetPresContext(); + + int32_t x = presContext->AppUnitsToDevPixels(point.x + size.width / 2); + int32_t y = presContext->AppUnitsToDevPixels(point.y + size.height / 2); + + // Simulate a touch interaction by dispatching touch events with mouse events. + nsCoreUtils::DispatchTouchEvent(NS_TOUCH_START, x, y, aContent, frame, presShell, widget); + nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_DOWN, x, y, aContent, frame, presShell, widget); + nsCoreUtils::DispatchTouchEvent(NS_TOUCH_END, x, y, aContent, frame, presShell, widget); + nsCoreUtils::DispatchMouseEvent(NS_MOUSE_BUTTON_UP, x, y, aContent, frame, presShell, widget); } NS_IMETHODIMP Accessible::ScrollTo(uint32_t aHow) { if (IsDefunct()) return NS_ERROR_FAILURE;
--- a/accessible/src/generic/DocAccessible.cpp +++ b/accessible/src/generic/DocAccessible.cpp @@ -1260,29 +1260,53 @@ DocAccessible::GetAccessibleByUniqueIDIn if (child) return child; } return nullptr; } Accessible* -DocAccessible::GetAccessibleOrContainer(nsINode* aNode) +DocAccessible::GetAccessibleOrContainer(nsINode* aNode) const { if (!aNode || !aNode->IsInDoc()) return nullptr; nsINode* currNode = aNode; Accessible* accessible = nullptr; while (!(accessible = GetAccessible(currNode)) && (currNode = currNode->GetParentNode())); return accessible; } +Accessible* +DocAccessible::GetAccessibleOrDescendant(nsINode* aNode) const +{ + Accessible* acc = GetAccessible(aNode); + if (acc) + return acc; + + acc = GetContainerAccessible(aNode); + if (acc) { + uint32_t childCnt = acc->ChildCount(); + for (uint32_t idx = 0; idx < childCnt; idx++) { + Accessible* child = acc->GetChildAt(idx); + for (nsIContent* elm = child->GetContent(); + elm && elm != acc->GetContent(); + elm = elm->GetFlattenedTreeParent()) { + if (elm == aNode) + return child; + } + } + } + + return nullptr; +} + bool DocAccessible::BindToDocument(Accessible* aAccessible, nsRoleMapEntry* aRoleMapEntry) { if (!aAccessible) return false; // Put into DOM node cache.
--- a/accessible/src/generic/DocAccessible.h +++ b/accessible/src/generic/DocAccessible.h @@ -237,27 +237,32 @@ public: * this and nested documents. */ Accessible* GetAccessibleByUniqueIDInSubtree(void* aUniqueID); /** * Return an accessible for the given DOM node or container accessible if * the node is not accessible. */ - Accessible* GetAccessibleOrContainer(nsINode* aNode); + Accessible* GetAccessibleOrContainer(nsINode* aNode) const; /** * Return a container accessible for the given DOM node. */ - Accessible* GetContainerAccessible(nsINode* aNode) + Accessible* GetContainerAccessible(nsINode* aNode) const { return aNode ? GetAccessibleOrContainer(aNode->GetParentNode()) : nullptr; } /** + * Return an accessible for the given node or its first accessible descendant. + */ + Accessible* GetAccessibleOrDescendant(nsINode* aNode) const; + + /** * Return true if the given ID is referred by relation attribute. * * @note Different elements may share the same ID if they are hosted inside * XBL bindings. Be careful the result of this method may be senseless * while it's called for XUL elements (where XBL is used widely). */ bool IsDependentID(const nsAString& aID) const { return mDependentIDsHash.Get(aID, nullptr); }
--- a/accessible/src/generic/HyperTextAccessible.cpp +++ b/accessible/src/generic/HyperTextAccessible.cpp @@ -713,43 +713,29 @@ HyperTextAccessible::GetRelativeOffset(n rv = RenderedToContentOffset(frame, aFromOffset, &contentOffset); NS_ENSURE_SUCCESS(rv, -1); } nsPeekOffsetStruct pos(aAmount, aDirection, contentOffset, 0, kIsJumpLinesOk, kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, aWordMovementType); rv = aFromFrame->PeekOffset(&pos); - if (NS_FAILED(rv)) { - pos.mResultContent = aFromFrame->GetContent(); - if (aDirection == eDirPrevious) { - // Use passed-in frame as starting point in failure case for now, - // this is a hack to deal with starting on a list bullet frame, - // which fails in PeekOffset() because the line iterator doesn't see it. - // XXX Need to look at our overall handling of list bullets, which are an odd case - int32_t endOffsetUnused; - aFromFrame->GetOffsets(pos.mContentOffset, endOffsetUnused); - } - else { - // XXX: PeekOffset fails on a last frame in the document for - // eSelectLine/eDirNext. DOM selection (up/down arrowing processing) has - // similar code to handle this case. One day it should be incorporated - // into PeekOffset. - int32_t startOffsetUnused; - aFromFrame->GetOffsets(startOffsetUnused, pos.mContentOffset); - } + + // PeekOffset fails on last/first lines of the text in certain cases. + if (NS_FAILED(rv) && aAmount == eSelectLine) { + pos.mAmount = (aDirection == eDirNext) ? eSelectEndLine : eSelectBeginLine; + aFromFrame->PeekOffset(&pos); } - - // Turn the resulting node and offset into a hyperTextOffset - int32_t hyperTextOffset; if (!pos.mResultContent) return -1; + // Turn the resulting node and offset into a hyperTextOffset // If finalAccessible is nullptr, then DOMPointToHypertextOffset() searched // through the hypertext children without finding the node/offset position. + int32_t hyperTextOffset; Accessible* finalAccessible = DOMPointToHypertextOffset(pos.mResultContent, pos.mContentOffset, &hyperTextOffset, aDirection == eDirNext); if (!finalAccessible && aDirection == eDirPrevious) { // If we reached the end during search, this means we didn't find the DOM point // and we're actually at the start of the paragraph hyperTextOffset = 0; @@ -927,23 +913,29 @@ HyperTextAccessible::GetTextBeforeOffset case BOUNDARY_LINE_START: if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); *aStartOffset = FindLineBoundary(offset, ePrevLineBegin); *aEndOffset = FindLineBoundary(offset, eThisLineBegin); return GetText(*aStartOffset, *aEndOffset, aText); - case BOUNDARY_LINE_END: + case BOUNDARY_LINE_END: { if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET) offset = AdjustCaretOffset(offset); *aEndOffset = FindLineBoundary(offset, ePrevLineEnd); - *aStartOffset = FindLineBoundary(*aEndOffset, ePrevLineEnd); + int32_t tmpOffset = *aEndOffset; + // Adjust offset if line is wrapped. + if (*aEndOffset != 0 && !IsLineEndCharAt(*aEndOffset)) + tmpOffset--; + + *aStartOffset = FindLineBoundary(tmpOffset, ePrevLineEnd); return GetText(*aStartOffset, *aEndOffset, aText); + } default: return NS_ERROR_INVALID_ARG; } } NS_IMETHODIMP HyperTextAccessible::GetTextAtOffset(int32_t aOffset, @@ -2146,23 +2138,19 @@ HyperTextAccessible::RenderedToContentOf bool HyperTextAccessible::GetCharAt(int32_t aOffset, EGetTextType aShift, nsAString& aChar, int32_t* aStartOffset, int32_t* aEndOffset) { aChar.Truncate(); int32_t offset = ConvertMagicOffset(aOffset) + static_cast<int32_t>(aShift); - int32_t childIdx = GetChildIndexAtOffset(offset); - if (childIdx == -1) + if (!CharAt(offset, aChar)) return false; - Accessible* child = GetChildAt(childIdx); - child->AppendTextTo(aChar, offset - GetChildOffset(childIdx), 1); - if (aStartOffset) *aStartOffset = offset; if (aEndOffset) *aEndOffset = aChar.IsEmpty() ? offset : offset + 1; return true; }
--- a/accessible/src/generic/HyperTextAccessible.h +++ b/accessible/src/generic/HyperTextAccessible.h @@ -160,16 +160,46 @@ public: * Return character count within the hypertext accessible. */ uint32_t CharacterCount() { return GetChildOffset(ChildCount()); } /** + * Get a character at the given offset (don't support magic offsets). + */ + bool CharAt(int32_t aOffset, nsAString& aChar) + { + int32_t childIdx = GetChildIndexAtOffset(aOffset); + if (childIdx == -1) + return false; + + Accessible* child = GetChildAt(childIdx); + child->AppendTextTo(aChar, aOffset - GetChildOffset(childIdx), 1); + return true; + } + + /** + * Return true if char at the given offset equals to given char. + */ + bool IsCharAt(int32_t aOffset, char aChar) + { + nsAutoString charAtOffset; + CharAt(aOffset, charAtOffset); + return charAtOffset.CharAt(0) == aChar; + } + + /** + * Return true if terminal char is at the given offset. + */ + bool IsLineEndCharAt(int32_t aOffset) + { return IsCharAt(aOffset, '\n'); } + + /** * Get a character before/at/after the given offset. * * @param aOffset [in] the given offset * @param aShift [in] specifies whether to get a char before/at/after * offset * @param aChar [out] the character * @param aStartOffset [out, optional] the start offset of the character * @param aEndOffset [out, optional] the end offset of the character @@ -283,22 +313,18 @@ protected: return aOffset; } /** * Return true if the given offset points to terminal empty line if any. */ bool IsEmptyLastLineOffset(int32_t aOffset) { - if (aOffset != static_cast<int32_t>(CharacterCount())) - return false; - - nsAutoString lastChar; - GetText(aOffset -1, -1, lastChar); - return lastChar.EqualsLiteral("\n"); + return aOffset == static_cast<int32_t>(CharacterCount()) && + IsLineEndCharAt(aOffset - 1); } /** * Return an offset of the found word boundary. */ int32_t FindWordBoundary(int32_t aOffset, nsDirection aDirection, EWordMovementType aWordMovementType) {
--- a/accessible/src/jsat/AccessFu.jsm +++ b/accessible/src/jsat/AccessFu.jsm @@ -111,17 +111,17 @@ this.AccessFu = { TouchAdapter.start(); Services.obs.addObserver(this, 'remote-browser-frame-shown', false); Services.obs.addObserver(this, 'in-process-browser-or-app-frame-shown', false); Services.obs.addObserver(this, 'Accessibility:NextObject', false); Services.obs.addObserver(this, 'Accessibility:PreviousObject', false); Services.obs.addObserver(this, 'Accessibility:Focus', false); Services.obs.addObserver(this, 'Accessibility:ActivateObject', false); - Services.obs.addObserver(this, 'Accessibility:MoveCaret', false); + Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false); Utils.win.addEventListener('TabOpen', this); Utils.win.addEventListener('TabClose', this); Utils.win.addEventListener('TabSelect', this); if (this.readyCallback) { this.readyCallback(); delete this.readyCallback; } @@ -154,17 +154,17 @@ this.AccessFu = { Utils.win.removeEventListener('TabSelect', this); Services.obs.removeObserver(this, 'remote-browser-frame-shown'); Services.obs.removeObserver(this, 'in-process-browser-or-app-frame-shown'); Services.obs.removeObserver(this, 'Accessibility:NextObject'); Services.obs.removeObserver(this, 'Accessibility:PreviousObject'); Services.obs.removeObserver(this, 'Accessibility:Focus'); Services.obs.removeObserver(this, 'Accessibility:ActivateObject'); - Services.obs.removeObserver(this, 'Accessibility:MoveCaret'); + Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity'); if (this.doneCallback) { this.doneCallback(); delete this.doneCallback; } }, _enableOrDisable: function _enableOrDisable() { @@ -196,16 +196,19 @@ this.AccessFu = { this._output(aMessage.json, aMessage.target); break; case 'AccessFu:Input': this.Input.setEditState(aMessage.json); break; case 'AccessFu:ActivateContextMenu': this.Input.activateContextMenu(aMessage.json); break; + case 'AccessFu:DoScroll': + this.Input.doScroll(aMessage.json); + break; } }, _output: function _output(aPresentationData, aBrowser) { for each (let presenter in aPresentationData) { if (!presenter) continue; @@ -235,23 +238,25 @@ this.AccessFu = { } }, _addMessageListeners: function _addMessageListeners(aMessageManager) { aMessageManager.addMessageListener('AccessFu:Present', this); aMessageManager.addMessageListener('AccessFu:Input', this); aMessageManager.addMessageListener('AccessFu:Ready', this); aMessageManager.addMessageListener('AccessFu:ActivateContextMenu', this); + aMessageManager.addMessageListener('AccessFu:DoScroll', this); }, _removeMessageListeners: function _removeMessageListeners(aMessageManager) { aMessageManager.removeMessageListener('AccessFu:Present', this); aMessageManager.removeMessageListener('AccessFu:Input', this); aMessageManager.removeMessageListener('AccessFu:Ready', this); aMessageManager.removeMessageListener('AccessFu:ActivateContextMenu', this); + aMessageManager.removeMessageListener('AccessFu:DoScroll', this); }, _handleMessageManager: function _handleMessageManager(aMessageManager) { if (this._enabled) { this._addMessageListeners(aMessageManager); } this._loadFrameScript(aMessageManager); }, @@ -272,18 +277,18 @@ this.AccessFu = { this.Input.activateCurrent(JSON.parse(aData)); break; case 'Accessibility:Focus': this._focused = JSON.parse(aData); if (this._focused) { this.showCurrent(true); } break; - case 'Accessibility:MoveCaret': - this.Input.moveCaret(JSON.parse(aData)); + case 'Accessibility:MoveByGranularity': + this.Input.moveByGranularity(JSON.parse(aData)); break; case 'remote-browser-frame-shown': case 'in-process-browser-or-app-frame-shown': { let mm = aSubject.QueryInterface(Ci.nsIFrameLoader).messageManager; this._handleMessageManager(mm); break; } @@ -648,41 +653,41 @@ var Input = { _handleGesture: function _handleGesture(aGesture) { let gestureName = aGesture.type + aGesture.touches.length; Logger.info('Gesture', aGesture.type, '(fingers: ' + aGesture.touches.length + ')'); switch (gestureName) { case 'dwell1': case 'explore1': - this.moveToPoint('SimpleTouch', aGesture.x, aGesture.y); + this.moveToPoint('Simple', aGesture.x, aGesture.y); break; case 'doubletap1': this.activateCurrent(); break; case 'doubletaphold1': this.sendContextMenuMessage(); break; case 'swiperight1': this.moveCursor('moveNext', 'Simple', 'gestures'); break; case 'swipeleft1': this.moveCursor('movePrevious', 'Simple', 'gesture'); break; case 'swiperight2': - this.scroll(-1, true); + this.sendScrollMessage(-1, true); break; case 'swipedown2': - this.scroll(-1); + this.sendScrollMessage(-1); break; case 'swipeleft2': - this.scroll(1, true); + this.sendScrollMessage(1, true); break; case 'swipeup2': - this.scroll(1); + this.sendScrollMessage(1); break; case 'explore2': Utils.CurrentBrowser.contentWindow.scrollBy( -aGesture.deltaX, -aGesture.deltaY); break; case 'swiperight3': this.moveCursor('moveNext', this.quickNavMode.current, 'gesture'); break; @@ -783,59 +788,84 @@ var Input = { moveCursor: function moveCursor(aAction, aRule, aInputType) { let mm = Utils.getMessageManager(Utils.CurrentBrowser); mm.sendAsyncMessage('AccessFu:MoveCursor', {action: aAction, rule: aRule, origin: 'top', inputType: aInputType}); }, - moveCaret: function moveCaret(aDetails) { + moveByGranularity: function moveByGranularity(aDetails) { + const MOVEMENT_GRANULARITY_PARAGRAPH = 8; + if (!this.editState.editing) { - return; + if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) { + this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture'); + return; + } + } else { + aDetails.atStart = this.editState.atStart; + aDetails.atEnd = this.editState.atEnd; } - aDetails.atStart = this.editState.atStart; - aDetails.atEnd = this.editState.atEnd; - let mm = Utils.getMessageManager(Utils.CurrentBrowser); - mm.sendAsyncMessage('AccessFu:MoveCaret', aDetails); + let type = this.editState.editing ? 'AccessFu:MoveCaret' : + 'AccessFu:MoveByGranularity'; + mm.sendAsyncMessage(type, aDetails); }, activateCurrent: function activateCurrent(aData) { let mm = Utils.getMessageManager(Utils.CurrentBrowser); let offset = aData && typeof aData.keyIndex === 'number' ? aData.keyIndex - Output.brailleState.startOffset : -1; mm.sendAsyncMessage('AccessFu:Activate', {offset: offset}); }, sendContextMenuMessage: function sendContextMenuMessage() { let mm = Utils.getMessageManager(Utils.CurrentBrowser); mm.sendAsyncMessage('AccessFu:ContextMenu', {}); }, - activateContextMenu: function activateContextMenu(aMessage) { + activateContextMenu: function activateContextMenu(aDetails) { if (Utils.MozBuildApp === 'mobile/android') { - let p = AccessFu.adjustContentBounds(aMessage.bounds, Utils.CurrentBrowser, + let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser, true, true).center(); Services.obs.notifyObservers(null, 'Gesture:LongPress', JSON.stringify({x: p.x, y: p.y})); } }, setEditState: function setEditState(aEditState) { this.editState = aEditState; }, + // XXX: This is here for backwards compatability with screen reader simulator + // it should be removed when the extension is updated on amo. scroll: function scroll(aPage, aHorizontal) { + this.sendScrollMessage(aPage, aHorizontal); + }, + + sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) { let mm = Utils.getMessageManager(Utils.CurrentBrowser); mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'}); }, + doScroll: function doScroll(aDetails) { + let horizontal = aDetails.horizontal; + let page = aDetails.page; + let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser, + true, true).center(); + let wu = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + wu.sendWheelEvent(p.x, p.y, + horizontal ? page : 0, horizontal ? 0 : page, 0, + Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0); + }, + get keyMap() { delete this.keyMap; this.keyMap = { a: ['moveNext', 'Anchor'], A: ['movePrevious', 'Anchor'], b: ['moveNext', 'Button'], B: ['movePrevious', 'Button'], c: ['moveNext', 'Combobox'],
--- a/accessible/src/jsat/EventManager.jsm +++ b/accessible/src/jsat/EventManager.jsm @@ -151,21 +151,22 @@ this.EventManager.prototype = { QueryInterface(Ci.nsIAccessibleDocument).virtualCursor; let position = pivot.position; if (position && position.role == ROLE_INTERNAL_FRAME) break; let event = aEvent. QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent); let reason = event.reason; - if (this.editState.editing) + if (this.editState.editing) { aEvent.accessibleDocument.takeFocus(); - + } this.present( - Presentation.pivotChanged(position, event.oldAccessible, reason)); + Presentation.pivotChanged(position, event.oldAccessible, reason, + pivot.startOffset, pivot.endOffset)); break; } case EVENT_STATE_CHANGE: { let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent); if (event.state == Ci.nsIAccessibleStates.STATE_CHECKED && !(event.isExtraState)) {
--- a/accessible/src/jsat/OutputGenerator.jsm +++ b/accessible/src/jsat/OutputGenerator.jsm @@ -159,16 +159,29 @@ this.OutputGenerator = { _addName: function _addName(aOutput, aAccessible, aFlags) { let name; if (Utils.getAttributes(aAccessible)['explicit-name'] === 'true' || (aFlags & INCLUDE_NAME)) { name = aAccessible.name; } + let description = aAccessible.description; + if (description) { + // Compare against the calculated name unconditionally, regardless of name rule, + // so we can make sure we don't speak duplicated descriptions + let tmpName = name || aAccessible.name; + if (tmpName && (description !== tmpName)) { + name = name || ''; + name = this.outputOrder === OUTPUT_DESC_FIRST ? + description + ' - ' + name : + name + ' - ' + description; + } + } + if (name) { aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'](name); } }, /** * Adds a landmark role to the output if available.
--- a/accessible/src/jsat/Presentation.jsm +++ b/accessible/src/jsat/Presentation.jsm @@ -119,47 +119,63 @@ VisualPresenter.prototype = { type: 'Visual', /** * The padding in pixels between the object and the highlight border. */ BORDER_PADDING: 2, viewportChanged: function VisualPresenter_viewportChanged(aWindow) { - let currentAcc = this._displayedAccessibles.get(aWindow); + let currentDisplay = this._displayedAccessibles.get(aWindow); + if (!currentDisplay) { + return null; + } + + let currentAcc = currentDisplay.accessible; + let start = currentDisplay.startOffset; + let end = currentDisplay.endOffset; if (Utils.isAliveAndVisible(currentAcc)) { - let bounds = Utils.getBounds(currentAcc); + let bounds = (start === -1 && end === -1) ? Utils.getBounds(currentAcc) : + Utils.getTextBounds(currentAcc, start, end); + return { type: this.type, details: { method: 'showBounds', bounds: bounds, padding: this.BORDER_PADDING } }; } return null; }, pivotChanged: function VisualPresenter_pivotChanged(aContext, aReason) { this._displayedAccessibles.set(aContext.accessible.document.window, - aContext.accessible); + { accessible: aContext.accessible, + startOffset: aContext.startOffset, + endOffset: aContext.endOffset }); if (!aContext.accessible) return {type: this.type, details: {method: 'hideBounds'}}; try { aContext.accessible.scrollTo( Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE); + + let bounds = (aContext.startOffset === -1 && aContext.endOffset === -1) ? + aContext.bounds : Utils.getTextBounds(aContext.accessible, + aContext.startOffset, aContext.endOffset); + return { type: this.type, details: { method: 'showBounds', - bounds: aContext.bounds, + bounds: bounds, padding: this.BORDER_PADDING } }; } catch (e) { Logger.logException(e, 'Failed to get bounds'); return null; } }, @@ -227,37 +243,49 @@ AndroidPresenter.prototype = { this.ANDROID_VIEW_FOCUSED; if (isExploreByTouch) { // This isn't really used by TalkBack so this is a half-hearted attempt // for now. androidEvents.push({eventType: this.ANDROID_VIEW_HOVER_EXIT, text: []}); } - let state = Utils.getStates(aContext.accessible)[0]; - let brailleOutput = {}; if (Utils.AndroidSdkVersion >= 16) { if (!this._braillePresenter) { this._braillePresenter = new BraillePresenter(); } brailleOutput = this._braillePresenter.pivotChanged(aContext, aReason). details; } - androidEvents.push({eventType: (isExploreByTouch) ? - this.ANDROID_VIEW_HOVER_ENTER : focusEventType, - text: UtteranceGenerator.genForContext(aContext).output, - bounds: aContext.bounds, - clickable: aContext.accessible.actionCount > 0, - checkable: !!(state & - Ci.nsIAccessibleStates.STATE_CHECKABLE), - checked: !!(state & - Ci.nsIAccessibleStates.STATE_CHECKED), - brailleOutput: brailleOutput}); + if (aReason === Ci.nsIAccessiblePivot.REASON_TEXT) { + if (Utils.AndroidSdkVersion >= 16) { + let adjustedText = aContext.textAndAdjustedOffsets; + + androidEvents.push({ + eventType: this.ANDROID_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY, + text: [adjustedText.text], + fromIndex: adjustedText.startOffset, + toIndex: adjustedText.endOffset + }); + } + } else { + let state = Utils.getStates(aContext.accessible)[0]; + androidEvents.push({eventType: (isExploreByTouch) ? + this.ANDROID_VIEW_HOVER_ENTER : focusEventType, + text: UtteranceGenerator.genForContext(aContext).output, + bounds: aContext.bounds, + clickable: aContext.accessible.actionCount > 0, + checkable: !!(state & + Ci.nsIAccessibleStates.STATE_CHECKABLE), + checked: !!(state & + Ci.nsIAccessibleStates.STATE_CHECKED), + brailleOutput: brailleOutput}); + } return { type: this.type, details: androidEvents }; }, @@ -489,20 +517,19 @@ this.Presentation = { } else { this.presenters.push(new SpeechPresenter()); this.presenters.push(new HapticPresenter()); } return this.presenters; }, - pivotChanged: function Presentation_pivotChanged(aPosition, - aOldPosition, - aReason) { - let context = new PivotContext(aPosition, aOldPosition); + pivotChanged: function Presentation_pivotChanged(aPosition, aOldPosition, aReason, + aStartOffset, aEndOffset) { + let context = new PivotContext(aPosition, aOldPosition, aStartOffset, aEndOffset); return [p.pivotChanged(context, aReason) for each (p in this.presenters)]; }, actionInvoked: function Presentation_actionInvoked(aObject, aActionName) { return [p.actionInvoked(aObject, aActionName) for each (p in this.presenters)]; },
--- a/accessible/src/jsat/TouchAdapter.jsm +++ b/accessible/src/jsat/TouchAdapter.jsm @@ -37,16 +37,19 @@ this.TouchAdapter = { DWELL_REPEAT_DELAY: 300, // maximum distance the mouse could move during a tap in inches TAP_MAX_RADIUS: 0.2, // The virtual touch ID generated by a mouse event. MOUSE_ID: 'mouse', + // Synthesized touch ID. + SYNTH_ID: -1, + start: function TouchAdapter_start() { Logger.info('TouchAdapter.start'); this._touchPoints = {}; this._dwellTimeout = 0; this._prevGestures = {}; this._lastExploreTime = 0; this._dpi = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor). @@ -126,29 +129,34 @@ this.TouchAdapter = { aEvent.view.top instanceof Ci.nsIDOMChromeWindow) { return; } if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_UNKNOWN) { return; } + let changedTouches = aEvent.changedTouches || [aEvent]; + + if (changedTouches.length == 1 && + changedTouches[0].identifier == this.SYNTH_ID) { + return; + } + if (!this.eventsOfInterest[aEvent.type]) { aEvent.preventDefault(); aEvent.stopImmediatePropagation(); return; } if (this._delayedEvent) { Utils.win.clearTimeout(this._delayedEvent); delete this._delayedEvent; } - let changedTouches = aEvent.changedTouches || [aEvent]; - // XXX: Until bug 77992 is resolved, on desktop we get microseconds // instead of milliseconds. let timeStamp = (Utils.OS == 'Android') ? aEvent.timeStamp : Date.now(); switch (aEvent.type) { case 'mousedown': case 'mouseenter': case 'touchstart': for (var i = 0; i < changedTouches.length; i++) {
--- a/accessible/src/jsat/TraversalRules.jsm +++ b/accessible/src/jsat/TraversalRules.jsm @@ -34,20 +34,23 @@ const ROLE_TOGGLE_BUTTON = Ci.nsIAccessi const ROLE_ENTRY = Ci.nsIAccessibleRole.ROLE_ENTRY; const ROLE_LIST = Ci.nsIAccessibleRole.ROLE_LIST; const ROLE_DEFINITION_LIST = Ci.nsIAccessibleRole.ROLE_DEFINITION_LIST; const ROLE_LISTITEM = Ci.nsIAccessibleRole.ROLE_LISTITEM; const ROLE_BUTTONDROPDOWNGRID = Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID; const ROLE_LISTBOX = Ci.nsIAccessibleRole.ROLE_LISTBOX; const ROLE_SLIDER = Ci.nsIAccessibleRole.ROLE_SLIDER; const ROLE_HEADING = Ci.nsIAccessibleRole.ROLE_HEADING; +const ROLE_HEADER = Ci.nsIAccessibleRole.ROLE_HEADER; const ROLE_TERM = Ci.nsIAccessibleRole.ROLE_TERM; const ROLE_SEPARATOR = Ci.nsIAccessibleRole.ROLE_SEPARATOR; const ROLE_TABLE = Ci.nsIAccessibleRole.ROLE_TABLE; const ROLE_INTERNAL_FRAME = Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME; +const ROLE_PARAGRAPH = Ci.nsIAccessibleRole.ROLE_PARAGRAPH; +const ROLE_SECTION = Ci.nsIAccessibleRole.ROLE_SECTION; this.EXPORTED_SYMBOLS = ['TraversalRules']; Cu.import('resource://gre/modules/accessibility/Utils.jsm'); Cu.import('resource://gre/modules/XPCOMUtils.jsm'); let gSkipEmptyImages = new PrefCache('accessibility.accessfu.skip_empty_images'); @@ -96,73 +99,75 @@ var gSimpleTraversalRoles = ROLE_PROGRESSBAR, ROLE_BUTTONDROPDOWN, ROLE_BUTTONMENU, ROLE_CHECK_MENU_ITEM, ROLE_PASSWORD_TEXT, ROLE_RADIO_MENU_ITEM, ROLE_TOGGLE_BUTTON, ROLE_ENTRY, + ROLE_HEADER, + ROLE_HEADING, // Used for traversing in to child OOP frames. ROLE_INTERNAL_FRAME]; this.TraversalRules = { Simple: new BaseTraversalRule( gSimpleTraversalRoles, function Simple_match(aAccessible) { + function hasZeroOrSingleChildDescendants () { + for (let acc = aAccessible; acc.childCount > 0; acc = acc.firstChild) { + if (acc.childCount > 1) { + return false; + } + } + + return true; + } + switch (aAccessible.role) { case ROLE_COMBOBOX: // We don't want to ignore the subtree because this is often // where the list box hangs out. return FILTER_MATCH; case ROLE_TEXT_LEAF: { // Nameless text leaves are boring, skip them. let name = aAccessible.name; if (name && name.trim()) return FILTER_MATCH; else return FILTER_IGNORE; } - case ROLE_LINK: - // If the link has children we should land on them instead. - // Image map links don't have children so we need to match those. - if (aAccessible.childCount == 0) - return FILTER_MATCH; - else - return FILTER_IGNORE; case ROLE_STATICTEXT: { let parent = aAccessible.parent; // Ignore prefix static text in list items. They are typically bullets or numbers. if (parent.childCount > 1 && aAccessible.indexInParent == 0 && parent.role == ROLE_LISTITEM) return FILTER_IGNORE; return FILTER_MATCH; } case ROLE_GRAPHIC: return TraversalRules._shouldSkipImage(aAccessible); + case ROLE_LINK: + case ROLE_HEADER: + case ROLE_HEADING: + return hasZeroOrSingleChildDescendants() ? + (FILTER_MATCH | FILTER_IGNORE_SUBTREE) : (FILTER_IGNORE); default: // Ignore the subtree, if there is one. So that we don't land on // the same content that was already presented by its parent. return FILTER_MATCH | FILTER_IGNORE_SUBTREE; } } ), - SimpleTouch: new BaseTraversalRule( - gSimpleTraversalRoles, - function Simple_match(aAccessible) { - return FILTER_MATCH | - FILTER_IGNORE_SUBTREE; - } - ), - Anchor: new BaseTraversalRule( [ROLE_LINK], function Anchor_match(aAccessible) { // We want to ignore links, only focus named anchors. let state = {}; let extraState = {}; aAccessible.getState(state, extraState); @@ -243,16 +248,29 @@ this.TraversalRules = { List: new BaseTraversalRule( [ROLE_LIST, ROLE_DEFINITION_LIST]), PageTab: new BaseTraversalRule( [ROLE_PAGETAB]), + Paragraph: new BaseTraversalRule( + [ROLE_PARAGRAPH, + ROLE_SECTION], + function Paragraph_match(aAccessible) { + for (let child = aAccessible.firstChild; child; child = child.nextSibling) { + if (child.role === ROLE_TEXT_LEAF) { + return FILTER_MATCH | FILTER_IGNORE_SUBTREE; + } + } + + return FILTER_IGNORE; + }), + RadioButton: new BaseTraversalRule( [ROLE_RADIOBUTTON, ROLE_RADIO_MENU_ITEM]), Separator: new BaseTraversalRule( [ROLE_SEPARATOR]), Table: new BaseTraversalRule(
--- a/accessible/src/jsat/Utils.jsm +++ b/accessible/src/jsat/Utils.jsm @@ -222,16 +222,24 @@ this.Utils = { }, getBounds: function getBounds(aAccessible) { let objX = {}, objY = {}, objW = {}, objH = {}; aAccessible.getBounds(objX, objY, objW, objH); return new Rect(objX.value, objY.value, objW.value, objH.value); }, + getTextBounds: function getTextBounds(aAccessible, aStart, aEnd) { + let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText); + let objX = {}, objY = {}, objW = {}, objH = {}; + accText.getRangeExtents(aStart, aEnd, objX, objY, objW, objH, + Ci.nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE); + return new Rect(objX.value, objY.value, objW.value, objH.value); + }, + inHiddenSubtree: function inHiddenSubtree(aAccessible) { for (let acc=aAccessible; acc; acc=acc.parent) { let hidden = Utils.getAttributes(acc).hidden; if (hidden && JSON.parse(hidden)) { return true; } } return false; @@ -408,31 +416,73 @@ this.Logger = { this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i), aIndent + 1); } }; /** * PivotContext: An object that generates and caches context information * for a given accessible and its relationship with another accessible. */ -this.PivotContext = function PivotContext(aAccessible, aOldAccessible) { +this.PivotContext = function PivotContext(aAccessible, aOldAccessible, + aStartOffset, aEndOffset) { this._accessible = aAccessible; this._oldAccessible = this._isDefunct(aOldAccessible) ? null : aOldAccessible; + this.startOffset = aStartOffset; + this.endOffset = aEndOffset; } PivotContext.prototype = { get accessible() { return this._accessible; }, get oldAccessible() { return this._oldAccessible; }, + get textAndAdjustedOffsets() { + if (this.startOffset === -1 && this.endOffset === -1) { + return null; + } + + if (!this._textAndAdjustedOffsets) { + let result = {startOffset: this.startOffset, + endOffset: this.endOffset, + text: this._accessible.QueryInterface(Ci.nsIAccessibleText). + getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT)}; + let hypertextAcc = this._accessible.QueryInterface(Ci.nsIAccessibleHyperText); + + // Iterate through the links in backwards order so text replacements don't + // affect the offsets of links yet to be processed. + for (let i = hypertextAcc.linkCount - 1; i >= 0; i--) { + let link = hypertextAcc.getLinkAt(i); + let linkText = ''; + if (link instanceof Ci.nsIAccessibleText) { + linkText = link.QueryInterface(Ci.nsIAccessibleText). + getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT); + } + + let start = link.startIndex; + let end = link.endIndex; + for (let offset of ['startOffset', 'endOffset']) { + if (this[offset] >= end) { + result[offset] += linkText.length - (end - start); + } + } + result.text = result.text.substring(0, start) + linkText + + result.text.substring(end); + } + + this._textAndAdjustedOffsets = result; + } + + return this._textAndAdjustedOffsets; + }, + /** * Get a list of |aAccessible|'s ancestry up to the root. * @param {nsIAccessible} aAccessible. * @return {Array} Ancestry list. */ _getAncestry: function _getAncestry(aAccessible) { let ancestry = []; let parent = aAccessible;
--- a/accessible/src/jsat/content-script.js +++ b/accessible/src/jsat/content-script.js @@ -3,16 +3,20 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ let Ci = Components.interfaces; let Cu = Components.utils; const ROLE_ENTRY = Ci.nsIAccessibleRole.ROLE_ENTRY; const ROLE_INTERNAL_FRAME = Ci.nsIAccessibleRole.ROLE_INTERNAL_FRAME; +const MOVEMENT_GRANULARITY_CHARACTER = 1; +const MOVEMENT_GRANULARITY_WORD = 2; +const MOVEMENT_GRANULARITY_PARAGRAPH = 8; + Cu.import('resource://gre/modules/XPCOMUtils.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Logger', 'resource://gre/modules/accessibility/Utils.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Presentation', 'resource://gre/modules/accessibility/Presentation.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules', 'resource://gre/modules/accessibility/TraversalRules.jsm'); XPCOMUtils.defineLazyModuleGetter(this, 'Utils', @@ -112,17 +116,18 @@ function showCurrent(aMessage) { let vc = Utils.getVirtualCursor(content.document); if (!forwardToChild(vc, showCurrent, aMessage)) { if (!vc.position && aMessage.json.move) { vc.moveFirst(TraversalRules.Simple); } else { sendAsyncMessage('AccessFu:Present', Presentation.pivotChanged( - vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE)); + vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE, + vc.startOffset, vc.endOffset)); } } } function forwardToParent(aMessage) { // XXX: This is a silly way to make a deep copy let newJSON = JSON.parse(JSON.stringify(aMessage.json)); newJSON.origin = 'child'; @@ -221,21 +226,40 @@ function activateContextMenu(aMessage) { } let position = Utils.getVirtualCursor(content.document).position; if (!forwardToChild(aMessage, activateContextMenu, position)) { sendContextMenuCoordinates(position); } } +function moveByGranularity(aMessage) { + let direction = aMessage.json.direction; + let vc = Utils.getVirtualCursor(content.document); + let granularity; + + switch(aMessage.json.granularity) { + case MOVEMENT_GRANULARITY_CHARACTER: + granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY; + break; + case MOVEMENT_GRANULARITY_WORD: + granularity = Ci.nsIAccessiblePivot.WORD_BOUNDARY; + break; + default: + return; + } + + if (direction === 'Previous') { + vc.movePreviousByText(granularity); + } else if (direction === 'Next') { + vc.moveNextByText(granularity); + } +} + function moveCaret(aMessage) { - const MOVEMENT_GRANULARITY_CHARACTER = 1; - const MOVEMENT_GRANULARITY_WORD = 2; - const MOVEMENT_GRANULARITY_PARAGRAPH = 8; - let direction = aMessage.json.direction; let granularity = aMessage.json.granularity; let accessible = Utils.getVirtualCursor(content.document).position; let accText = accessible.QueryInterface(Ci.nsIAccessibleText); let oldOffset = accText.caretOffset; let text = accText.getText(0, accText.characterCount); let start = {}, end = {}; @@ -277,90 +301,27 @@ function presentCaretChange(aText, aOldO if (aOldOffset !== aNewOffset) { let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset, aOldOffset, aOldOffset, true); sendAsyncMessage('AccessFu:Present', msg); } } function scroll(aMessage) { - let vc = Utils.getVirtualCursor(content.document); - - function tryToScroll() { - let horiz = aMessage.json.horizontal; - let page = aMessage.json.page; - - // Search up heirarchy for scrollable element. - let acc = vc.position; - while (acc) { - let elem = acc.DOMNode; - - // This is inspired by IndieUI events. Once they are - // implemented, it should be easy to transition to them. - // https://dvcs.w3.org/hg/IndieUI/raw-file/tip/src/indie-ui-events.html#scrollrequest - let uiactions = elem.getAttribute ? elem.getAttribute('uiactions') : ''; - if (uiactions && uiactions.split(' ').indexOf('scroll') >= 0) { - let evt = elem.ownerDocument.createEvent('CustomEvent'); - let details = horiz ? { deltaX: page * elem.clientWidth } : - { deltaY: page * elem.clientHeight }; - evt.initCustomEvent( - 'scrollrequest', true, true, - ObjectWrapper.wrap(details, elem.ownerDocument.defaultView)); - if (!elem.dispatchEvent(evt)) - return; - } - - // We will do window scrolling next. - if (elem == content.document) - break; - - if (!horiz && elem.clientHeight < elem.scrollHeight) { - let s = content.getComputedStyle(elem); - if (s.overflowY == 'scroll' || s.overflowY == 'auto') { - elem.scrollTop += page * elem.clientHeight; - return true; - } - } - - if (horiz) { - if (elem.clientWidth < elem.scrollWidth) { - let s = content.getComputedStyle(elem); - if (s.overflowX == 'scroll' || s.overflowX == 'auto') { - elem.scrollLeft += page * elem.clientWidth; - return true; - } - } - } - acc = acc.parent; - } - - // Scroll window. - if (!horiz && content.scrollMaxY && - ((page > 0 && content.scrollY < content.scrollMaxY) || - (page < 0 && content.scrollY > 0))) { - content.scroll(0, content.innerHeight * page + content.scrollY); - return true; - } else if (horiz && content.scrollMaxX && - ((page > 0 && content.scrollX < content.scrollMaxX) || - (page < 0 && content.scrollX > 0))) { - content.scroll(content.innerWidth * page + content.scrollX); - return true; - } - - return false; + function sendScrollCoordinates(aAccessible) { + let bounds = Utils.getBounds(aAccessible); + sendAsyncMessage('AccessFu:DoScroll', + { bounds: bounds, + page: aMessage.json.page, + horizontal: aMessage.json.horizontal }); } - if (aMessage.json.origin != 'child' && - forwardToChild(aMessage, scroll, vc.position)) { - return; - } - - if (!tryToScroll()) { - // Failed to scroll anything in this document. Try in parent document. - forwardToParent(aMessage); + let position = Utils.getVirtualCursor(content.document).position; + if (!forwardToChild(aMessage, scroll, position)) { + sendScrollCoordinates(position); } } addMessageListener( 'AccessFu:Start', function(m) { Logger.debug('AccessFu:Start'); if (m.json.buildApp) @@ -368,16 +329,17 @@ addMessageListener( addMessageListener('AccessFu:MoveToPoint', moveToPoint); addMessageListener('AccessFu:MoveCursor', moveCursor); addMessageListener('AccessFu:ShowCurrent', showCurrent); addMessageListener('AccessFu:Activate', activateCurrent); addMessageListener('AccessFu:ContextMenu', activateContextMenu); addMessageListener('AccessFu:Scroll', scroll); addMessageListener('AccessFu:MoveCaret', moveCaret); + addMessageListener('AccessFu:MoveByGranularity', moveByGranularity); if (!eventManager) { eventManager = new EventManager(this); } eventManager.start(); }); addMessageListener( @@ -387,13 +349,14 @@ addMessageListener( removeMessageListener('AccessFu:MoveToPoint', moveToPoint); removeMessageListener('AccessFu:MoveCursor', moveCursor); removeMessageListener('AccessFu:ShowCurrent', showCurrent); removeMessageListener('AccessFu:Activate', activateCurrent); removeMessageListener('AccessFu:ContextMenu', activateContextMenu); removeMessageListener('AccessFu:Scroll', scroll); removeMessageListener('AccessFu:MoveCaret', moveCaret); + removeMessageListener('AccessFu:MoveByGranularity', moveByGranularity); eventManager.stop(); }); sendAsyncMessage('AccessFu:Ready');
--- a/accessible/src/windows/msaa/AccessibleWrap.cpp +++ b/accessible/src/windows/msaa/AccessibleWrap.cpp @@ -1821,34 +1821,32 @@ AccessibleWrap::GetXPAccessibleFor(const // If lVal negative then it is treated as child ID and we should look for // accessible through whole accessible subtree including subdocuments. // Otherwise we treat lVal as index in parent. if (aVarChild.lVal < 0) { // Convert child ID to unique ID. void* uniqueID = reinterpret_cast<void*>(-aVarChild.lVal); - // Document. + DocAccessible* document = Document(); + Accessible* child = + document->GetAccessibleByUniqueIDInSubtree(uniqueID); + + // If it is a document then just return an accessible. if (IsDoc()) - return AsDoc()->GetAccessibleByUniqueIDInSubtree(uniqueID); + return child; - // ARIA document and menu popups. - if (ARIARole() == roles::DOCUMENT || IsMenuPopup()) { - DocAccessible* document = Document(); - Accessible* child = - document->GetAccessibleByUniqueIDInSubtree(uniqueID); + // Otherwise check whether the accessible is a child (this path works for + // ARIA documents and popups). + Accessible* parent = child; + while (parent && parent != document) { + if (parent == this) + return child; - // Check whether the accessible for the given ID is a child. - Accessible* parent = child ? child->Parent() : nullptr; - while (parent && parent != document) { - if (parent == this) - return child; - - parent = parent->Parent(); - } + parent = parent->Parent(); } return nullptr; } // Gecko child indices are 0-based in contrast to indices used in MSAA. return GetChildAt(aVarChild.lVal - 1); }
--- a/accessible/src/windows/sdn/sdnTextAccessible.cpp +++ b/accessible/src/windows/sdn/sdnTextAccessible.cpp @@ -29,17 +29,17 @@ IMPL_IUNKNOWN_QUERY_TAIL_AGGREGATED(mAcc STDMETHODIMP sdnTextAccessible::get_domText(BSTR __RPC_FAR* aText) { A11Y_TRYBLOCK_BEGIN if (!aText) return E_INVALIDARG; - *aText = NULL; + *aText = nullptr; if (mAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; nsAutoString nodeValue; nsCOMPtr<nsIDOMNode> DOMNode(do_QueryInterface(mAccessible->GetContent())); DOMNode->GetNodeValue(nodeValue); @@ -165,17 +165,17 @@ sdnTextAccessible::scrollToSubstring(uns STDMETHODIMP sdnTextAccessible::get_fontFamily(BSTR __RPC_FAR* aFontFamily) { A11Y_TRYBLOCK_BEGIN if (!aFontFamily) return E_INVALIDARG; - *aFontFamily = NULL; + *aFontFamily = nullptr; if (mAccessible->IsDefunct()) return CO_E_OBJNOTCONNECTED; nsIFrame* frame = mAccessible->GetFrame(); if (!frame) return E_FAIL;
--- a/accessible/tests/mochitest/attributes/test_obj.html +++ b/accessible/tests/mochitest/attributes/test_obj.html @@ -55,42 +55,42 @@ https://bugzilla.mozilla.org/show_bug.cg // ARIA testAttrs("live", {"live" : "polite"}, true); testAttrs("live2", {"live" : "polite"}, true); testAbsentAttrs("live3", {"live" : ""}); testAttrs("log", {"live" : "polite"}, true); testAttrs("logAssertive", {"live" : "assertive"}, true); testAttrs("marquee", {"live" : "off"}, true); testAttrs("status", {"live" : "polite"}, true); - testAttrs("tablist", {"live" : "polite"}, true); testAttrs("timer", {"live" : "off"}, true); + testAbsentAttrs("tablist", {"live" : "polite"}); // container-live object attribute testAttrs("liveChild", {"container-live" : "polite"}, true); testAttrs("live2Child", {"container-live" : "polite"}, true); testAttrs("logChild", {"container-live" : "polite"}, true); testAttrs("logAssertiveChild", {"container-live" : "assertive"}, true); testAttrs("marqueeChild", {"container-live" : "off"}, true); testAttrs("statusChild", {"container-live" : "polite"}, true); - testAttrs("tablistChild", {"container-live" : "polite"}, true); testAttrs("timerChild", {"container-live" : "off"}, true); + testAbsentAttrs("tablistChild", {"container-live" : "polite"}); // container-live-role object attribute testAttrs("log", {"container-live-role" : "log"}, true); testAttrs("logAssertive", {"container-live-role" : "log"}, true); testAttrs("marquee", {"container-live-role" : "marquee"}, true); testAttrs("status", {"container-live-role" : "status"}, true); testAttrs("timer", {"container-live-role" : "timer"}, true); testAttrs("logChild", {"container-live-role" : "log"}, true); testAttrs("logAssertive", {"container-live-role" : "log"}, true); testAttrs("logAssertiveChild", {"container-live-role" : "log"}, true); testAttrs("marqueeChild", {"container-live-role" : "marquee"}, true); testAttrs("statusChild", {"container-live-role" : "status"}, true); - testAttrs("tablistChild", {"container-live-role" : "tablist"}, true); testAttrs("timerChild", {"container-live-role" : "timer"}, true); + testAbsentAttrs("tablistChild", {"container-live-role" : "tablist"}); // absent aria-label and aria-labelledby object attribute testAbsentAttrs("label", {"label" : "foo"}); testAbsentAttrs("labelledby", {"labelledby" : "label"}); // container that has no default live attribute testAttrs("liveGroup", {"live" : "polite"}, true); testAttrs("liveGroupChild", {"container-live" : "polite"}, true); @@ -148,19 +148,19 @@ https://bugzilla.mozilla.org/show_bug.cg Mozilla Bug 475006 </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=558036" title="make HTML <output> accessible"> Mozilla Bug 558036 </a> <a target="_blank" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=663136" - title="Add test coverage for tablist as implicit live region"> - Mozilla Bug 663136 + href="https://bugzilla.mozilla.org/show_bug.cgi?id=896400" + title="Tablist should no longer be an implicit live region"> + Mozilla Bug 896400 </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=563862" title="Expand support for nsIAccessibleEvent::OBJECT_ATTRIBUTE_CHANGE"> Mozilla Bug 563862 </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=819303"
--- a/accessible/tests/mochitest/common.js +++ b/accessible/tests/mochitest/common.js @@ -153,28 +153,28 @@ function isObject(aObj, aExpectedObj, aM } //////////////////////////////////////////////////////////////////////////////// // Helpers for getting DOM node/accessible /** * Return the DOM node by identifier (may be accessible, DOM node or ID). */ -function getNode(aAccOrNodeOrID) +function getNode(aAccOrNodeOrID, aDocument) { if (!aAccOrNodeOrID) return null; if (aAccOrNodeOrID instanceof nsIDOMNode) return aAccOrNodeOrID; if (aAccOrNodeOrID instanceof nsIAccessible) return aAccOrNodeOrID.DOMNode; - node = document.getElementById(aAccOrNodeOrID); + node = (aDocument || document).getElementById(aAccOrNodeOrID); if (!node) { ok(false, "Can't get DOM element for " + aAccOrNodeOrID); return null; } return node; }
--- a/accessible/tests/mochitest/events.js +++ b/accessible/tests/mochitest/events.js @@ -1433,16 +1433,39 @@ function moveToLineEnd(aID, aCaretOffset this.getID = function moveToLineEnd_getID() { return "move to line end in " + prettyName(aID); } } /** + * Move the caret to the end of previous line if any. + */ +function moveToPrevLineEnd(aID, aCaretOffset) +{ + this.__proto__ = new synthAction(aID, new caretMoveChecker(aCaretOffset, aID)); + + this.invoke = function moveToPrevLineEnd_invoke() + { + synthesizeKey("VK_UP", { }); + + if (MAC) + synthesizeKey("VK_RIGHT", { metaKey: true }); + else + synthesizeKey("VK_END", { }); + } + + this.getID = function moveToPrevLineEnd_getID() + { + return "move to previous line end in " + prettyName(aID); + } +} + +/** * Move the caret to begining of the line. */ function moveToLineStart(aID, aCaretOffset) { if (MAC) { this.__proto__ = new synthKey(aID, "VK_LEFT", { metaKey: true }, new caretMoveChecker(aCaretOffset, aID)); } else {
--- a/accessible/tests/mochitest/jsat/test_utterance_order.html +++ b/accessible/tests/mochitest/jsat/test_utterance_order.html @@ -19,16 +19,33 @@ https://bugzilla.mozilla.org/show_bug.cg function doTest() { // Test the following accOrElmOrID (with optional old accOrElmOrID). // Note: each accOrElmOrID entry maps to a unique object utterance // generator function within the UtteranceGenerator. var tests = [{ accOrElmOrID: "anchor", expected: [["link", "title"], ["title", "link"]] }, { + accOrElmOrID: "anchor_titleandtext", + expected: [[ + "link", "goes to the tests -", "Tests" + ], [ + "Tests", "- goes to the tests", "link" + ]] + }, { + accOrElmOrID: "anchor_duplicatedtitleandtext", + expected: [["link", "Tests"], ["Tests", "link"]] + }, { + accOrElmOrID: "anchor_arialabelandtext", + expected: [[ + "link", "goes to the tests - Tests" + ], [ + "Tests - goes to the tests", "link" + ]] + }, { accOrElmOrID: "textarea", expected: [[ "text area", "This is the text area text." ], [ "This is the text area text.", "text area" ]] }, { accOrElmOrID: "heading", @@ -137,20 +154,27 @@ https://bugzilla.mozilla.org/show_bug.cg </script> </head> <body> <div id="root"> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=753984" title="[AccessFu] utterance order test"> Mozilla Bug 753984</a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=758675" + title="[AccessFu] Add support for accDescription"> + Mozilla Bug 758675</a> <p id="display"></p> <div id="content" style="display: none"></div> <pre id="test"></pre> <a id="anchor" href="#test" title="title"></a> + <a id="anchor_titleandtext" href="#test" title="goes to the tests">Tests</a> + <a id="anchor_duplicatedtitleandtext" href="#test" title="Tests">Tests</a> + <a id="anchor_arialabelandtext" href="#test" aria-label="Tests" title="goes to the tests">Tests</a> <textarea id="textarea" cols="80" rows="5"> This is the text area text. </textarea> <h1 id="heading" title="Test heading"></h1> <ol id="list"> <li id="li_one">list one</li> </ol> <dl id="dlist">
--- a/accessible/tests/mochitest/pivot.js +++ b/accessible/tests/mochitest/pivot.js @@ -3,16 +3,18 @@ Components.utils.import("resource://gre/ //////////////////////////////////////////////////////////////////////////////// // Constants const PREFILTER_INVISIBLE = nsIAccessibleTraversalRule.PREFILTER_INVISIBLE; const PREFILTER_ARIA_HIDDEN = nsIAccessibleTraversalRule.PREFILTER_ARIA_HIDDEN; const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH; const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE; const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; +const CHAR_BOUNDARY = nsIAccessiblePivot.CHAR_BOUNDARY; +const WORD_BOUNDARY = nsIAccessiblePivot.WORD_BOUNDARY; const NS_ERROR_NOT_IN_TREE = 0x80780026; const NS_ERROR_INVALID_ARG = 0x80070057; //////////////////////////////////////////////////////////////////////////////// // Traversal rules /** @@ -150,16 +152,18 @@ VCChangedChecker.getPreviousPosAndOffset }; VCChangedChecker.methodReasonMap = { 'moveNext': nsIAccessiblePivot.REASON_NEXT, 'movePrevious': nsIAccessiblePivot.REASON_PREV, 'moveFirst': nsIAccessiblePivot.REASON_FIRST, 'moveLast': nsIAccessiblePivot.REASON_LAST, 'setTextRange': nsIAccessiblePivot.REASON_TEXT, + 'moveNextByText': nsIAccessiblePivot.REASON_TEXT, + 'movePreviousByText': nsIAccessiblePivot.REASON_TEXT, 'moveToPoint': nsIAccessiblePivot.REASON_POINT }; /** * Set a text range in the pivot and wait for virtual cursor change event. * * @param aDocAcc [in] document that manages the virtual cursor * @param aTextAccessible [in] accessible to set to virtual cursor's position @@ -229,16 +233,59 @@ function setVCPosInvoker(aDocAcc, aPivot this.eventSeq = []; this.unexpectedEventSeq = [ new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) ]; } } /** + * Move the pivot by text and wait for virtual cursor change event. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) + * @param aBoundary [in] boundary constant + * @param aTextOffsets [in] start and end offsets of text range to set in + * virtual cursor. + * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect + * virtual cursor to land on after performing move method. + * false if no move is expected. + */ +function setVCTextInvoker(aDocAcc, aPivotMoveMethod, aBoundary, aTextOffsets, aIdOrNameOrAcc) +{ + var expectMove = (aIdOrNameOrAcc != false); + this.invoke = function virtualCursorChangedInvoker_invoke() + { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + SimpleTest.info(aDocAcc.virtualCursor.position); + var moved = aDocAcc.virtualCursor[aPivotMoveMethod](aBoundary); + SimpleTest.is(!!moved, !!expectMove, + "moved pivot by text with " + aPivotMoveMethod + + " to " + aIdOrNameOrAcc); + }; + + this.getID = function setVCPosInvoker_getID() + { + return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod; + }; + + if (expectMove) { + this.eventSeq = [ + new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, aTextOffsets, aPivotMoveMethod) + ]; + } else { + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc) + ]; + } +} + + +/** * Move the pivot to the position under the point. * * @param aDocAcc [in] document that manages the virtual cursor * @param aX [in] screen x coordinate * @param aY [in] screen y coordinate * @param aIgnoreNoMatch [in] don't unset position if no object was found at * point. * @param aRule [in] traversal rule object
--- a/accessible/tests/mochitest/pivot/Makefile.in +++ b/accessible/tests/mochitest/pivot/Makefile.in @@ -8,12 +8,14 @@ topsrcdir = @top_srcdir@ srcdir = @srcdir@ VPATH = @srcdir@ relativesrcdir = @relativesrcdir@ include $(DEPTH)/config/autoconf.mk MOCHITEST_A11Y_FILES = \ doc_virtualcursor.html \ + doc_virtualcursor_text.html \ test_virtualcursor.html \ + test_virtualcursor_text.html \ $(NULL) include $(topsrcdir)/config/rules.mk
new file mode 100644 --- /dev/null +++ b/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> + <title>Pivot test document</title> + <meta charset="utf-8" /> +</head> +<body> + <p id="paragraph-1"> + This <b>is</b> <a href="#">the</a> test of text. + </p> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/accessible/tests/mochitest/pivot/test_virtualcursor_text.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<html> +<head> + <title>Tests pivot functionality in virtual cursors</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"> + </script> + <script type="application/javascript" + src="chrome://mochikit/content/chrome-harness.js"> + </script> + + <script type="application/javascript" src="../common.js"></script> + <script type="application/javascript" src="../browser.js"></script> + <script type="application/javascript" src="../events.js"></script> + <script type="application/javascript" src="../role.js"></script> + <script type="application/javascript" src="../states.js"></script> + <script type="application/javascript" src="../pivot.js"></script> + <script type="application/javascript" src="../layout.js"></script> + + <script type="application/javascript"> + var gBrowserWnd = null; + var gQueue = null; + + function doTest() + { + var doc = currentTabDocument(); + var docAcc = getAccessible(doc, [nsIAccessibleDocument]); + + gQueue = new eventQueue(); + + gQueue.onFinish = function onFinish() + { + closeBrowserWindow(); + } + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById('paragraph-1')))); + + gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [0,4], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', CHAR_BOUNDARY, [4,5], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, 'movePreviousByText', CHAR_BOUNDARY, [3,4], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [5,7], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [8,9], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, 'moveNextByText', WORD_BOUNDARY, [10,14], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, 'movePreviousByText', WORD_BOUNDARY, [8,9], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, 'movePreviousByText', WORD_BOUNDARY, [5,7], + getAccessible(doc.getElementById('paragraph-1'), nsIAccessibleText))); + + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function () { + /* We open a new browser because we need to test with a top-level content + document. */ + openBrowserWindow( + doTest, + getRootDirectory(window.location.href) + "doc_virtualcursor_text.html"); + }); + </script> +</head> +<body id="body"> + + <a target="_blank" + title="Support Movement By Granularity" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=886076">Mozilla Bug 886076</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html>
--- a/accessible/tests/mochitest/states/test_aria.html +++ b/accessible/tests/mochitest/states/test_aria.html @@ -193,16 +193,24 @@ testStates("aria_vslider", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); // indeterminate ARIA progressbars (no aria-valuenow or aria-valuetext attribute) // should expose mixed state testStates("aria_progressbar", STATE_MIXED); testStates("aria_progressbar_valuenow", 0, 0, STATE_MIXED); testStates("aria_progressbar_valuetext", 0, 0, STATE_MIXED); + testStates("aria_listbox", STATE_FOCUSABLE); + testStates("aria_grid", STATE_FOCUSABLE); + testStates("aria_tree", STATE_FOCUSABLE); + testStates("aria_treegrid", STATE_FOCUSABLE); + testStates("aria_listbox_disabled", 0, 0, STATE_FOCUSABLE); + testStates("aria_grid_disabled", 0, 0, STATE_FOCUSABLE); + testStates("aria_tree_disabled", 0, 0, STATE_FOCUSABLE); + testStates("aria_treegrid_disabled", 0, 0, STATE_FOCUSABLE); SimpleTest.finish(); } SimpleTest.waitForExplicitFinish(); addA11yLoadEvent(doTest); </script> </head> @@ -215,60 +223,66 @@ Mozilla Bug 457219 </a><br /> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=429285" title="Propagate aria-disabled to descendants"> Mozilla Bug 429285 </a> <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=457226" + title="Mochitests for ARIA states"> + Mozilla Bug 457226 + </a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=499653" title="Unify ARIA state attributes mapping rules"> Mozilla Bug 499653 </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674" title="aria-autocomplete not supported on standard form text input controls"> Mozilla Bug 681674 </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674" title="aria-orientation should be applied to separator and slider roles"> Mozilla Bug 681674 </a> <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847" + title="Expose active state on current item of selectable widgets"> + Mozilla Bug 689847 + </a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017" title="File input control should be propogate states to descendants"> Mozilla Bug 699017 </a> <a target="_blank" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847" - title="Expose active state on current item of selectable widgets"> - Mozilla Bug 689847 - </a> - <a target="_blank" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=457226" - title="Mochitests for ARIA states"> - Mozilla Bug 457226 + href="https://bugzilla.mozilla.org/show_bug.cgi?id=690199" + title="ARIA select widget should expose focusable state regardless the way they manage its children"> + Mozilla Bug 690199 </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=740851" title="ARIA undetermined progressmeters should expose mixed state"> Mozilla Bug 740851 </a> <a target="_blank" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=762876 + href="https://bugzilla.mozilla.org/show_bug.cgi?id=762876" title="fix default horizontal / vertical state of role=scrollbar and ensure only one of horizontal / vertical states is exposed"> Mozilla Bug 762876 </a> <a target="_blank" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=835121 + href="https://bugzilla.mozilla.org/show_bug.cgi?id=835121" title="ARIA grid should be editable by default"> Mozilla Bug 835121 </a> + <p id="display"></p> <div id="content" style="display: none"></div> <pre id="test"> </pre> <div id="textbox_autocomplete_inline" role="textbox" aria-autocomplete="inline"></div> <div id="textbox_autocomplete_list" role="textbox" aria-autocomplete="list"></div> <div id="textbox_autocomplete_both" role="textbox" aria-autocomplete="both"></div> @@ -408,10 +422,44 @@ <div id="aria_slider" role="slider">slider</div> <div id="aria_hslider" role="slider" aria-orientation="horizontal">horizontal slider</div> <div id="aria_vslider" role="slider" aria-orientation="vertical">vertical slider</div> <!-- indeterminate ARIA progressbars should expose mixed state --> <div id="aria_progressbar" role="progressbar"></div> <div id="aria_progressbar_valuenow" role="progressbar" aria-valuenow="1"></div> <div id="aria_progressbar_valuetext" role="progressbar" aria-valuetext="value"></div> + + <!-- ARIA select widget should expose focusable state regardless the way they manage its children --> + <div id="aria_listbox" role="listbox"> + <div role="option" tabindex="0">A</div> + <div role="option" tabindex="0">a</div> + </div> + <div id="aria_grid" role="grid"> + <div role="row"><div role="gridcell" tabindex="0">B</div></div></div> + <div role="row"><div role="gridcell" tabindex="0">b</div></div></div> + <div id="aria_tree" role="tree"> + <div role="treeitem" tabindex="0">C</div> + <div role="treeitem" tabindex="0">c</div> + </div> + <div id="aria_treegrid" role="treegrid"> + <div role="row"><div role="gridcell" tabindex="0">D</div></div> + <div role="row"><div role="gridcell" tabindex="0">d</div></div> + </div> + <div id="aria_listbox_disabled" role="listbox" aria-disabled="true"> + <div role="option">E</div> + <div role="option">e</div> + </div> + <div id="aria_grid_disabled" role="grid" aria-disabled="true"> + <div role="row"><div role="gridcell">F</div></div> + <div role="row"><div role="gridcell">f</div></div> + </div> + <div id="aria_tree_disabled" role="tree" aria-disabled="true"> + <div role="treeitem">G</div> + <div role="treeitem">g</div> + </div> + <div id="aria_treegrid_disabled" role="treegrid" aria-disabled="true"> + <div role="row"><div role="gridcell">H</div></div> + <div role="row"><div role="gridcell">h</div></div> + </div> + </body> </html>
--- a/accessible/tests/mochitest/text/Makefile.in +++ b/accessible/tests/mochitest/text/Makefile.in @@ -13,18 +13,17 @@ include $(DEPTH)/config/autoconf.mk MOCHITEST_A11Y_FILES = \ doc.html \ test_atcaretoffset.html \ test_charboundary.html \ test_doc.html \ test_gettext.html \ test_hypertext.html \ + test_lineboundary.html \ test_label.xul \ - test_multiline.html \ test_passwords.html \ test_selection.html \ - test_singleline.html \ test_wordboundary.html \ test_words.html \ $(NULL) include $(topsrcdir)/config/rules.mk
--- a/accessible/tests/mochitest/text/test_atcaretoffset.html +++ b/accessible/tests/mochitest/text/test_atcaretoffset.html @@ -22,220 +22,118 @@ <script type="application/javascript" src="../events.js"></script> <script type="application/javascript" src="../text.js"></script> <script type="application/javascript"> //gA11yEventDumpToConsole = true; // debugging - // __a__w__o__r__d__\n - // 0 1 2 3 4 5 - // __t__w__o__ (soft line break) - // 6 7 8 9 - // __w__o__r__d__s - // 10 11 12 13 14 15 + function traverseTextByLines(aQueue, aID, aLines) + { + var baseInvoker = new synthFocus(aID); + var baseInvokerID = "move to last line end"; - function moveToLastLineEnd() - { - this.__proto__ = new synthFocus("textarea"); - - this.finalCheck = function moveToLastLineEnd_finalCheck() - { - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "", 15, 15, - ["textarea"]); + for (var i = aLines.length - 1; i >= 0 ; i--) { + var [ ppLineText, ppLineEndChar, ppLineStart, ppLineEnd ] = + (i - 2 >= 0) ? aLines[i - 2] : [ "", "", 0, 0 ]; + var [ pLineText, pLineEndChar, pLineStart, pLineEnd ] = + (i - 1 >= 0) ? aLines[i - 1] : [ "", "", 0, 0 ]; + var [ lineText, lineEndChar, lineStart, lineEnd ] = aLines[i]; - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "", 15, 15, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15, - [ "textarea" ]); + var [ nLineText, nLineEndChar, nLineStart, nLineEnd ] = + (i + 1 < aLines.length) ? + aLines[i + 1] : + [ "", "", lineEnd + lineEndChar.length, lineEnd + lineEndChar.length ]; - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15, - [ "textarea" ]); + var [ nnLineText, nnLineEndChar, nnLineStart, nnLineEnd ] = + (i + 2 < aLines.length) ? + aLines[i + 2] : + [ "", "", nLineEnd + nLineEndChar.length, nLineEnd + nLineEndChar.length ]; - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10, - "textarea", kTodo, kTodo, kOk); - } + var tests = [ + [ testTextBeforeOffset, BOUNDARY_LINE_START, + pLineText + pLineEndChar, pLineStart, lineStart], - this.getID = function moveToLastLineEnd_getID() - { - return "move to last line end"; - } - } + [ testTextBeforeOffset, BOUNDARY_LINE_END, + ppLineEndChar + pLineText, ppLineEnd, pLineEnd], + + [ testTextAtOffset, BOUNDARY_LINE_START, + lineText + lineEndChar, lineStart, nLineStart], - function moveToLastLineStart() - { - this.__proto__ = new moveToLineStart("textarea", 10); + [ testTextAtOffset, BOUNDARY_LINE_END, + pLineEndChar + lineText, pLineEnd, lineEnd], - this.finalCheck = function moveToLastLineStart_finalCheck() - { - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "", 15, 15, - [ "textarea" ]); + [ testTextAfterOffset, BOUNDARY_LINE_START, + nLineText + nnLineEndChar, nLineStart, nnLineStart], - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "", 15, 15, - [ "textarea" ]); + [ testTextAfterOffset, BOUNDARY_LINE_END, + lineEndChar + nLineText, lineEnd, nLineEnd], + ]; - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15, - [ "textarea" ]); + aQueue.push(new tmpl_moveTo(aID, baseInvoker, baseInvokerID, tests)); - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10, - [ "textarea" ]); + baseInvoker = new moveToLineStart(aID, lineStart); + baseInvokerID = "move to " + i + "th line start"; - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10, - "textarea", kTodo, kTodo, kOk); - } + aQueue.push(new tmpl_moveTo(aID, baseInvoker, baseInvokerID, tests)); - this.getID = function moveToLastLineStart_getID() - { - return "move to last line start"; + baseInvoker = new moveToPrevLineEnd(aID, pLineEnd); + baseInvokerID = "move to " + (i - 1) + "th line end"; } } - function moveToMiddleLineStart() + /** + * A template invoker to move through the text. + */ + function tmpl_moveTo(aID, aInvoker, aInvokerID, aTests) { - this.__proto__ = new synthUpKey("textarea", - new caretMoveChecker(6, "textarea")); - - this.finalCheck = function moveToMiddleLineStart_finalCheck() - { - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15, - [ "textarea" ]); - - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15, - [ "textarea" ]); + this.__proto__ = aInvoker; - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5, - [ "textarea" ]); - } - - this.getID = function moveToMiddleLineStart_getID() + this.finalCheck = function genericMoveTo_finalCheck() { - return "move to middle line start"; - } - } - - function moveToMiddleLineEnd() - { - this.__proto__ = new moveToLineEnd("textarea", 10); - - this.finalCheck = function moveToMiddleLineEnd_finalCheck() - { - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "words", 10, 15, - [ "textarea" ]); - - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "words", 10, 15, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5, - [ "textarea" ]); + for (var i = 0; i < aTests.length; i++) { + aTests[i][0].call(null, kCaretOffset, aTests[i][1], + aTests[i][2], aTests[i][3], aTests[i][4], aID, + kOk, kOk, kOk); + } } - this.getID = function moveToMiddleLineEnd_getID() - { - return "move to middle line end"; - } - } - - function moveToFirstLineStart() - { - this.__proto__ = new moveToTextStart("textarea"); - - this.finalCheck = function moveToFirstLineStart_finalCheck() - { - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10, - [ "textarea" ]); - - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "", 0, 0, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "", 0, 0, - "textarea", kOk, kOk, kOk); - } - - this.getID = function moveToFirstLineStart_getID() + this.getID = function genericMoveTo_getID() { - return "move to first line start"; - } - } - - function moveToFirstLineEnd() - { - this.__proto__ = new moveToLineEnd("textarea", 5); - - this.finalCheck = function moveToFirstLineStart_finalCheck() - { - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_START, "two ", 6, 10, - [ "textarea" ]); - - testTextAfterOffset(kCaretOffset, BOUNDARY_LINE_END, "\ntwo ", 5, 10, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_START, "aword\n", 0, 6, - [ "textarea" ]); - - testTextAtOffset(kCaretOffset, BOUNDARY_LINE_END, "aword", 0, 5, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_START, "", 0, 0, - [ "textarea" ]); - - testTextBeforeOffset(kCaretOffset, BOUNDARY_LINE_END, "", 0, 0, - [ "textarea" ]); - } - - this.getID = function moveToFirstLineEnd_getID() - { - return "move to first line end"; + return aInvokerID; } } var gQueue = null; function doTest() { gQueue = new eventQueue(); - gQueue.push(new moveToLastLineEnd()); - gQueue.push(new moveToLastLineStart()); - gQueue.push(new moveToMiddleLineStart()); - gQueue.push(new moveToMiddleLineEnd()); - gQueue.push(new moveToFirstLineStart()); - gQueue.push(new moveToFirstLineEnd()); + + // __a__w__o__r__d__\n + // 0 1 2 3 4 5 + // __t__w__o__ (soft line break) + // 6 7 8 9 + // __w__o__r__d__s + // 10 11 12 13 14 15 + + traverseTextByLines(gQueue, "textarea", + [ [ "aword", "\n", 0, 5 ], + [ "two ", "", 6, 10 ], + [ "words", "", 10, 15 ]] ); + + traverseTextByLines(gQueue, "ta_wrapped", + [ [ "hi ", "", 0, 3 ], + [ "hello", "", 3, 8 ], + [ " my ", "", 8, 12 ], + [ "longf", "", 12, 17 ], + [ "riend", "", 17, 22 ], + [ " t ", "", 22, 25 ], + [ "sq t", "", 25, 29 ]] ); + gQueue.invoke(); // will call SimpleTest.finish(); } SimpleTest.waitForExplicitFinish(); addA11yLoadEvent(doTest); </script> </head> <body> @@ -246,11 +144,13 @@ Bug 852021 </a> <p id="display"></p> <div id="content" style="display: none"></div> <pre id="test"> <textarea id="textarea" cols="5">aword two words</textarea> + + <textarea id="ta_wrapped" cols="5">hi hello my longfriend t sq t</textarea> </pre> </body> </html>
rename from accessible/tests/mochitest/text/test_singleline.html rename to accessible/tests/mochitest/text/test_lineboundary.html --- a/accessible/tests/mochitest/text/test_singleline.html +++ b/accessible/tests/mochitest/text/test_lineboundary.html @@ -1,112 +1,156 @@ <!DOCTYPE html> <html> <head> - <title>nsIAccessibleText getText related function tests for html:input,html:div and html:textarea</title> + <title>Line boundary getText* functions tests</title> <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> <script type="application/javascript" src="../common.js"></script> <script type="application/javascript" src="../text.js"></script> <script type="application/javascript"> function doTest() { + ////////////////////////////////////////////////////////////////////////// // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 - //////////////////////////////////////////////////////////////////////// - // getTextAfterOffset + var IDs = [ "input", "div", "editable", "textarea", + getNode("ta", getNode("ta_cntr").contentDocument) ]; + + testTextBeforeOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 15, "", 0, 0 ] ]); + testTextBeforeOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 15, "", 0, 0 ] ]); - var IDs = [ "input", "div", "editable", "textarea" ]; - var regularIDs = [ "input", "div", "editable" ]; + testTextAtOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 15, "hello my friend", 0, 15 ] ]); + testTextAtOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 15, "hello my friend", 0, 15 ] ]); + + testTextAfterOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 15, "", 15, 15 ] ]); + testTextAfterOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 15, "", 15, 15 ] ]); - // BOUNDARY_LINE_START - testTextAfterOffset(0, BOUNDARY_LINE_START, "", 15, 15, IDs); - testTextAfterOffset(1, BOUNDARY_LINE_START, "", 15, 15, IDs); - testTextAfterOffset(14, BOUNDARY_LINE_START, "", 15, 15, IDs); - testTextAfterOffset(15, BOUNDARY_LINE_START, "", 15, 15, IDs); + ////////////////////////////////////////////////////////////////////////// + // __o__n__e__w__o__r__d__\n + // 0 1 2 3 4 5 6 7 + // __\n + // 8 + // __t__w__o__ __w__o__r__d__s__\n + // 9 10 11 12 13 14 15 16 17 18 + + IDs = [ "ml_div", "ml_divbr", "ml_editable", "ml_editablebr", "ml_textarea"]; - // BOUNDARY_LINE_END - testTextAfterOffset(0, BOUNDARY_LINE_END, "", 15, 15, IDs); - testTextAfterOffset(1, BOUNDARY_LINE_END, "", 15, 15, IDs); - testTextAfterOffset(14, BOUNDARY_LINE_END, "", 15, 15, IDs); - testTextAfterOffset(15, BOUNDARY_LINE_END, "", 15, 15, IDs); - - //////////////////////////////////////////////////////////////////////// - // getTextBeforeOffset - - var IDs = [ "input", "div", "editable", "textarea" ]; + testTextBeforeOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 7, "", 0, 0 ], + [ 8, 8, "oneword\n", 0, 8 ], + [ 9, 18, "\n", 8, 9 ], + [ 19, 19, "two words\n", 9, 19 ]]); + testTextBeforeOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 7, "", 0, 0 ], + [ 8, 8, "oneword", 0, 7 ], + [ 9, 18, "\n", 7, 8 ], + [ 19, 19, "\ntwo words", 8, 18 ]]); - // BOUNDARY_LINE_START - testTextBeforeOffset(0, BOUNDARY_LINE_START, "", 0, 0, IDs); - testTextBeforeOffset(1, BOUNDARY_LINE_START, "", 0, 0, IDs); - testTextBeforeOffset(14, BOUNDARY_LINE_START, "", 0, 0, IDs); - testTextBeforeOffset(15, BOUNDARY_LINE_START, "", 0, 0, IDs); + testTextAtOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 7, "oneword\n", 0, 8 ], + [ 8, 8, "\n", 8, 9 ], + [ 9, 18, "two words\n", 9, 19 ], + [ 19, 19, "", 19, 19 ]]); + testTextAtOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 7, "oneword", 0, 7 ], + [ 8, 8, "\n", 7, 8 ], + [ 9, 18, "\ntwo words", 8, 18 ], + [ 19, 19, "\n", 18, 19 ]]); - // BOUNDARY_LINE_END - testTextBeforeOffset(0, BOUNDARY_LINE_END, "", 0, 0, IDs); - testTextBeforeOffset(1, BOUNDARY_LINE_END, "", 0, 0, IDs); - testTextBeforeOffset(14, BOUNDARY_LINE_END, "", 0, 0, IDs); - testTextBeforeOffset(15, BOUNDARY_LINE_END, "", 0, 0, IDs); - - //////////////////////////////////////////////////////////////////////// - // getTextAtOffset + testTextAfterOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 7, "\n", 8, 9 ], + [ 8, 8, "two words\n", 9, 19 ], + [ 9, 19, "", 19, 19 ]]); + testTextAfterOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 7, "\n", 7, 8 ], + [ 8, 8, "\ntwo words", 8, 18 ], + [ 9, 18, "\n", 18, 19 ], + [ 19, 19, "", 19, 19 ]]); - IDs = [ "input", "div", "editable", "textarea" ]; - regularIDs = [ "input", "div", "editable" ]; + ////////////////////////////////////////////////////////////////////////// + // a * b (* is embedded char for link) + testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START, + [ [ 0, 5, "", 0, 0 ] ]); + + testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END, + [ [ 0, 5, "", 0, 0 ] ]); - // BOUNDARY_LINE_START - testTextAtOffset(0, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs); - testTextAtOffset(1, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs); - testTextAtOffset(14, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs); - testTextAtOffset(15, BOUNDARY_LINE_START, "hello my friend", 0, 15, IDs); + testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START, + [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]); + + testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END, + [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]); - // BOUNDARY_LINE_END - testTextAtOffset(0, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs); - testTextAtOffset(1, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs); - testTextAtOffset(14, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs); - testTextAtOffset(15, BOUNDARY_LINE_END, "hello my friend", 0, 15, IDs); + testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START, + [ [ 0, 5, "", 5, 5 ] ]); + testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END, + [ [ 0, 5, "", 5, 5 ] ]); + SimpleTest.finish(); } SimpleTest.waitForExplicitFinish(); addA11yLoadEvent(doTest); </script> </head> <body> <a target="_blank" - title="nsIAccessibleText getText related function tests for html:input,html:div and html:textarea" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=452769"> - Bug 452769 - </a> - <a target="_blank" title="getTextAtOffset for word boundaries: beginning of a new life" href="https://bugzilla.mozilla.org/show_bug.cgi?id=853340"> Bug 853340 </a> <a target="_blank" title="getTextBeforeOffset for word boundaries: evolving" href="https://bugzilla.mozilla.org/show_bug.cgi?id=855732"> Bug 855732 </a> <a target="_blank" title=" getTextAfterOffset for line boundary on new rails" href="https://bugzilla.mozilla.org/show_bug.cgi?id=882292"> Bug 882292 </a> + <p id="display"></p> <div id="content" style="display: none"></div> <pre id="test"> </pre> <input id="input" value="hello my friend"/> <div id="div">hello my friend</div> <div id="editable" contenteditable="true">hello my friend</div> <textarea id="textarea">hello my friend</textarea> + <iframe id="ta_cntr" + src="data:text/html,<html><body><textarea id='ta'>hello my friend</textarea></body></html>"></iframe> + <pre> + <div id="ml_div">oneword + +two words +</div> + <div id="ml_divbr">oneword<br/><br/>two words<br/></div> + <div id="ml_editable" contenteditable="true">oneword + +two words +</div> + <div id="ml_editablebr" contenteditable="true">oneword<br/><br/>two words<br/></div> + <textarea id="ml_textarea" cols="300">oneword + +two words +</textarea> + </pre> + + <iframe id="ht_1" src="data:text/html,<html><body>a <a href=''>b</a> c</body></html>"></iframe> </body> </html>
deleted file mode 100644 --- a/accessible/tests/mochitest/text/test_multiline.html +++ /dev/null @@ -1,137 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>nsIAccessibleText getText related function in multiline text</title> - <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> - - <script type="application/javascript" - src="chrome://mochikit/content/MochiKit/packed.js"></script> - <script type="application/javascript" - src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> - <script type="application/javascript" - src="../common.js"></script> - <script type="application/javascript" - src="../text.js"></script> - <script type="application/javascript"> - - function doTest() - { - // __o__n__e__w__o__r__d__\n - // 0 1 2 3 4 5 6 7 - // __\n - // 8 - // __t__w__o__ __w__o__r__d__s__\n - // 9 10 11 12 13 14 15 16 17 18 - - //////////////////////////////////////////////////////////////////////// - // getText - - var IDs = ["div", "divbr", "editable", "editablebr", "textarea"]; - - //////////////////////////////////////////////////////////////////////// - // getTextAfterOffset - - // BOUNDARY_LINE_START - testTextAfterOffset(0, BOUNDARY_LINE_START, "\n", 8, 9, IDs); - testTextAfterOffset(7, BOUNDARY_LINE_START, "\n", 8, 9, IDs); - testTextAfterOffset(8, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs); - testTextAfterOffset(9, BOUNDARY_LINE_START, "", 19, 19, IDs); - testTextAfterOffset(19, BOUNDARY_LINE_START, "", 19, 19, IDs); - - // BOUNDARY_LINE_END - testTextAfterOffset(0, BOUNDARY_LINE_END, "\n", 7, 8, IDs); - testTextAfterOffset(7, BOUNDARY_LINE_END, "\n", 7, 8, IDs); - testTextAfterOffset(8, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs); - testTextAfterOffset(9, BOUNDARY_LINE_END, "\n", 18, 19, IDs); - testTextAfterOffset(18, BOUNDARY_LINE_END, "\n", 18, 19, IDs); - testTextAfterOffset(19, BOUNDARY_LINE_END, "", 19, 19, IDs); - - //////////////////////////////////////////////////////////////////////// - // getTextBeforeOffset - - // BOUNDARY_LINE_START - testTextBeforeOffset(0, BOUNDARY_LINE_START, "", 0, 0, IDs); - testTextBeforeOffset(8, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs); - testTextBeforeOffset(9, BOUNDARY_LINE_START, "\n", 8, 9, IDs); - testTextBeforeOffset(18, BOUNDARY_LINE_START, "\n", 8, 9, IDs); - testTextBeforeOffset(19, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs); - - // BOUNDARY_LINE_END - testTextBeforeOffset(0, BOUNDARY_LINE_END, "", 0, 0, IDs); - testTextBeforeOffset(7, BOUNDARY_LINE_END, "", 0, 0, IDs); - testTextBeforeOffset(8, BOUNDARY_LINE_END, "oneword", 0, 7, IDs); - testTextBeforeOffset(9, BOUNDARY_LINE_END, "\n", 7, 8, IDs); - testTextBeforeOffset(18, BOUNDARY_LINE_END, "\n", 7, 8, IDs); - testTextBeforeOffset(19, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs); - - //////////////////////////////////////////////////////////////////////// - // getTextAtOffset - - // BOUNDARY_LINE_START - testTextAtOffset(0, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs); - testTextAtOffset(7, BOUNDARY_LINE_START, "oneword\n", 0, 8, IDs); - testTextAtOffset(8, BOUNDARY_LINE_START, "\n", 8, 9, IDs); - testTextAtOffset(9, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs); - testTextAtOffset(13, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs); - testTextAtOffset(18, BOUNDARY_LINE_START, "two words\n", 9, 19, IDs); - testTextAtOffset(19, BOUNDARY_LINE_START, "", 19, 19, IDs); - - // BOUNDARY_LINE_END - testTextAtOffset(0, BOUNDARY_LINE_END, "oneword", 0, 7, IDs); - testTextAtOffset(7, BOUNDARY_LINE_END, "oneword", 0, 7, IDs); - testTextAtOffset(8, BOUNDARY_LINE_END, "\n", 7, 8, IDs); - testTextAtOffset(9, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs); - testTextAtOffset(17, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs); - testTextAtOffset(18, BOUNDARY_LINE_END, "\ntwo words", 8, 18, IDs); - testTextAtOffset(19, BOUNDARY_LINE_END, "\n", 18, 19, IDs); - - SimpleTest.finish(); - } - - SimpleTest.waitForExplicitFinish(); - addA11yLoadEvent(doTest); - </script> -</head> -<body> - - <a target="_blank" - title="nsIAccessibleText getText related functions test in multiline text" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=612331"> - Bug 612331 - </a> - <a target="_blank" - title="getTextAtOffset for word boundaries: beginning of a new life" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=853340"> - Bug 853340 - </a> - <a target="_blank" - title="getTextBeforeOffset for word boundaries: evolving" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=855732"> - Bug 855732 - </a> - <a target="_blank" - title=" getTextAfterOffset for line boundary on new rails" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=882292"> - Bug 882292 - </a> - <p id="display"></p> - <div id="content" style="display: none"></div> - <pre id="test"> - - <div id="div">oneword - -two words -</div> - <div id="divbr">oneword<br/><br/>two words<br/></div> - <div id="editable" contenteditable="true">oneword - -two words -</div> - <div id="editablebr" contenteditable="true">oneword<br/><br/>two words<br/></div> - <textarea id="textarea" cols="300">oneword - -two words -</textarea> - </pre> -</body> -</html>
--- a/accessible/tests/mochitest/value/test_general.html +++ b/accessible/tests/mochitest/value/test_general.html @@ -46,16 +46,21 @@ testValue("aria_application_link", ""); // roles that can live as HTMLLinkAccessibles testValue("aria_link_link", href); testValue("aria_main_link", href); testValue("aria_navigation_link", href); ////////////////////////////////////////////////////////////////////////// + // ARIA textboxes + + testValue("aria_textbox1", "helo"); + + ////////////////////////////////////////////////////////////////////////// // ARIA comboboxes // aria-activedescendant defines a current item the value is computed from testValue("aria_combobox1", kDiscBulletText + "Zoom"); // aria-selected defines a selected item the value is computed from, // list control is pointed by aria-owns relation. testValue("aria_combobox2", kDiscBulletText + "Zoom"); @@ -78,22 +83,27 @@ </head> <body> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=494807" title="Do not expose a11y info specific to hyperlinks when role is overridden using ARIA"> - Mozilla Bug 494807 + Bug 494807 </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=819273" - title=" ARIA combobox should have accessible value"> - Mozilla Bug 819273 + title="ARIA combobox should have accessible value"> + Bug 819273 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=887250" + title="ARIA textbox role doesn't expose value"> + Bug 887250 </a> <p id="display"></p> <div id="content" style="display: none"> </div> <pre id="test"> </pre> <a id="aria_menuitem_link" role="menuitem" href="foo">menuitem</a> @@ -103,16 +113,18 @@ <!-- landmark links --> <a id="aria_application_link" role="application" href="foo">app</a> <a id="aria_main_link" role="main" href="foo">main</a> <a id="aria_navigation_link" role="navigation" href="foo">nav</a> <!-- strange edge case: please don't do this in the wild --> <a id="aria_link_link" role="link" href="foo">link</a> + <div id="aria_textbox1" role="textbox">helo</div> + <div id="aria_combobox1" role="combobox" aria-owns="aria_combobox1_owned_listbox" aria-activedescendant="aria_combobox1_selected_option"> </div> <ul role="listbox" id="aria_combobox1_owned_listbox"> <li role="option">Zebra</li> <li role="option" id="aria_combobox1_selected_option">Zoom</li> </ul> @@ -137,10 +149,11 @@ <select id="combobox1"> <option id="cb1_item1">item1</option> <option id="cb1_item2">item2</option> </select> <select id="combobox2"> <option id="cb2_item1">item1</option> <option id="cb2_item2" selected="true">item2</option> </select> + </body> </html>
--- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -172,16 +172,18 @@ pref("geo.enabled", true); // see https://bugzilla.mozilla.org/show_bug.cgi?id=481566#c9 pref("content.sink.enable_perf_mode", 2); // 0 - switch, 1 - interactive, 2 - perf pref("content.sink.pending_event_mode", 0); pref("content.sink.perf_deflect_count", 1000000); pref("content.sink.perf_parse_time", 50000000); // Maximum scripts runtime before showing an alert pref("dom.max_chrome_script_run_time", 0); // disable slow script dialog for chrome +// Disable the watchdog thread for B2G. See bug 870043 comment 31. +pref("dom.use_watchdog", false); // plugins pref("plugin.disable", true); pref("dom.ipc.plugins.enabled", true); // product URLs // The breakpad report server to link to in about:crashes pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
--- a/b2g/chrome/content/settings.js +++ b/b2g/chrome/content/settings.js @@ -193,17 +193,21 @@ Components.utils.import('resource://gre/ lock.set('deviceinfo.product_model', product_model, null, null); })(); // =================== Debugger ==================== SettingsListener.observe('devtools.debugger.remote-enabled', false, function(value) { Services.prefs.setBoolPref('devtools.debugger.remote-enabled', value); // This preference is consulted during startup Services.prefs.savePrefFile(null); - value ? RemoteDebugger.start() : RemoteDebugger.stop(); + try { + value ? RemoteDebugger.start() : RemoteDebugger.stop(); + } catch(e) { + dump("Error while initializing devtools: " + e + "\n" + e.stack + "\n"); + } #ifdef MOZ_WIDGET_GONK let enableAdb = value; try { if (Services.prefs.getBoolPref('marionette.defaultPrefs.enabled')) { // Marionette is enabled. Force adb on, since marionette requires remote // debugging to be disabled (we don't want adb to track the remote debugger
--- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -551,17 +551,18 @@ var shell = { openAppForSystemMessage: function shell_openAppForSystemMessage(msg) { let origin = Services.io.newURI(msg.manifest, null, null).prePath; this.sendChromeEvent({ type: 'open-app', url: msg.uri, manifestURL: msg.manifest, isActivity: (msg.type == 'activity'), target: msg.target, - expectingSystemMessage: true + expectingSystemMessage: true, + extra: msg.extra }); }, receiveMessage: function shell_receiveMessage(message) { var activities = { 'content-handler': { name: 'view', response: null }, 'dial-handler': { name: 'dial', response: null }, 'mail-handler': { name: 'new', response: null }, 'sms-handler': { name: 'new', response: null }, @@ -972,26 +973,29 @@ let RemoteDebugger = { }, // Start the debugger server. start: function debugger_start() { if (!DebuggerServer.initialized) { // Ask for remote connections. DebuggerServer.init(this.prompt.bind(this)); DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js"); -#ifndef MOZ_WIDGET_GONK - DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js"); - DebuggerServer.addGlobalActor(DebuggerServer.ChromeDebuggerActor, "chromeDebugger"); - DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js"); - DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js"); -#endif - if ("nsIProfiler" in Ci) { - DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js"); + // Until we implement unix domain socket, we enable content actors + // only on development devices + if (Services.prefs.getBoolPref("devtools.debugger.enable-content-actors")) { + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js"); + DebuggerServer.addGlobalActor(DebuggerServer.ChromeDebuggerActor, "chromeDebugger"); + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js"); + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js"); + if ("nsIProfiler" in Ci) { + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js"); + } + DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js"); + DebuggerServer.enableWebappsContentActor = true; } - DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js"); DebuggerServer.addActors('chrome://browser/content/dbg-browser-actors.js'); DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webapps.js"); } let port = Services.prefs.getIntPref('devtools.debugger.remote-port') || 6000; try { DebuggerServer.openListener(port); } catch (e) {
--- a/b2g/components/TelURIParser.jsm +++ b/b2g/components/TelURIParser.jsm @@ -7,28 +7,28 @@ this.EXPORTED_SYMBOLS = ["TelURIParser"]; /** * Singleton providing functionality for parsing tel: and sms: URIs */ this.TelURIParser = { parseURI: function(scheme, uri) { // https://www.ietf.org/rfc/rfc2806.txt - let subscriber = uri.slice((scheme + ':').length); + let subscriber = decodeURIComponent(uri.slice((scheme + ':').length)); if (!subscriber.length) { return null; } let number = ''; let pos = 0; let len = subscriber.length; // visual-separator - let visualSeparator = [ '-', '.', '(', ')' ]; + let visualSeparator = [ ' ', '-', '.', '(', ')' ]; let digits = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ]; let dtmfDigits = [ '*', '#', 'A', 'B', 'C', 'D' ]; let pauseCharacter = [ 'p', 'w' ]; // global-phone-number if (subscriber[pos] == '+') { number += '+'; for (++pos; pos < len; ++pos) {
--- a/b2g/components/test/unit/test_bug793310.js +++ b/b2g/components/test/unit/test_bug793310.js @@ -1,16 +1,19 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ function run_test() { Components.utils.import("resource:///modules/TelURIParser.jsm") // global-phone-number - do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234'), '+1234'); + do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234'), '+1234'); + + // global-phone-number => white space separator + do_check_eq(TelURIParser.parseURI('tel', 'tel:+123 456 789'), '+123 456 789'); // global-phone-number => ignored chars do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234_123'), '+1234'); // global-phone-number => visualSeparator + digits do_check_eq(TelURIParser.parseURI('tel', 'tel:+-.()1234567890'), '+-.()1234567890'); // local-phone-number
--- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "e04bb3527c33dd6771e63397a3b52a4b9a5fce4e", + "revision": "878cc221e0fdadb4d42dc110945533104f6dd572", "repo_path": "/integration/gaia-central" }
--- a/b2g/config/leo/releng-leo.tt +++ b/b2g/config/leo/releng-leo.tt @@ -1,12 +1,12 @@ [ { -"size": 141304652, -"digest": "0bbba7483a483803fa0277ddeeb3f8f9592b57cb83d57794c366fb8a3541b47f0ab901c071b7ceb6828bceec7170e5d5c05c5b1dcc926c70f3da46a7e224f078", +"size": 117247732, +"digest": "16e74278e4e9b0d710df77d68af1677c91823dccfc611ab00ee617298a63787f9f9892bd1a41eccb8d45fb18d61bfda0dbd1de88f1861c14b4b44da3b94a4eca", "algorithm": "sha512", "filename": "backup-leo.tar.xz" }, { "size": 1570553, "digest": "ea03de74df73b05e939c314cd15c54aac7b5488a407b7cc4f5f263f3049a1f69642c567dd35c43d0bc3f0d599d0385a26ab2dd947a6b18f9044e4918b382eea7", "algorithm": "sha512", "filename": "Adreno200-AU_LINUX_ANDROID_ICS_CHOCO_CS.04.00.03.06.001.zip"
--- a/b2g/confvars.sh +++ b/b2g/confvars.sh @@ -50,9 +50,8 @@ MOZ_EXTENSION_MANAGER=1 MOZ_TIME_MANAGER=1 MOZ_B2G_CERTDATA=1 MOZ_PAY=1 MOZ_TOOLKIT_SEARCH= MOZ_PLACES= MOZ_B2G=1 MOZ_FOLD_LIBS=1 -MOZ_WBMP=1
--- a/browser/app/nsBrowserApp.cpp +++ b/browser/app/nsBrowserApp.cpp @@ -236,16 +236,19 @@ static int do_main(int argc, char* argv[ // relaunches Metro Firefox with this command line arg. mainFlags = XRE_MAIN_FLAG_USE_METRO; } else { // This command-line flag is used to test the metro browser in a desktop // environment. for (int idx = 1; idx < argc; idx++) { if (IsArg(argv[idx], "metrodesktop")) { metroOnDesktop = true; + // Disable crash reporting when running in metrodesktop mode. + char crashSwitch[] = "MOZ_CRASHREPORTER_DISABLE=1"; + putenv(crashSwitch); break; } } } } #endif // Desktop browser launch
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -467,16 +467,20 @@ pref("general.warnOnAboutConfig", pref("dom.disable_window_open_feature.location", true); // prevent JS from setting status messages pref("dom.disable_window_status_change", true); // allow JS to move and resize existing windows pref("dom.disable_window_move_resize", false); // prevent JS from monkeying with window focus, etc pref("dom.disable_window_flip", true); +// Disable touch events on Desktop Firefox by default until they are properly +// supported (bug 736048) +pref("dom.w3c_touch_events.enabled", 0); + // popups.policy 1=allow,2=reject pref("privacy.popups.policy", 1); pref("privacy.popups.usecustom", true); pref("privacy.popups.showBrowserMessage", true); pref("privacy.item.cookies", false); pref("privacy.clearOnShutdown.history", true); @@ -754,16 +758,22 @@ pref("browser.safebrowsing.reportURL", " pref("browser.safebrowsing.reportGenericURL", "http://%LOCALE%.phish-generic.mozilla.com/?hl=%LOCALE%"); pref("browser.safebrowsing.reportErrorURL", "http://%LOCALE%.phish-error.mozilla.com/?hl=%LOCALE%"); pref("browser.safebrowsing.reportPhishURL", "http://%LOCALE%.phish-report.mozilla.com/?hl=%LOCALE%"); pref("browser.safebrowsing.reportMalwareURL", "http://%LOCALE%.malware-report.mozilla.com/?hl=%LOCALE%"); pref("browser.safebrowsing.reportMalwareErrorURL", "http://%LOCALE%.malware-error.mozilla.com/?hl=%LOCALE%"); pref("browser.safebrowsing.warning.infoURL", "https://www.mozilla.org/%LOCALE%/firefox/phishing-protection/"); pref("browser.safebrowsing.malware.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site="); +// Since the application reputation query isn't hooked in anywhere yet, this +// preference does not matter. To be extra safe, don't turn this preference on +// for official builds without whitelisting (bug 842828). +#ifndef MOZILLA_OFFICIAL +pref("browser.safebrowsing.appRepURL", "https://sb-ssl.google.com/safebrowsing/clientreport/download"); +#endif #ifdef MOZILLA_OFFICIAL // Normally the "client ID" sent in updates is appinfo.name, but for // official Firefox releases from Mozilla we use a special identifier. pref("browser.safebrowsing.id", "navclient-auto-ffox"); #endif // Name of the about: page contributed by safebrowsing to handle display of error @@ -1268,8 +1278,12 @@ pref("media.webaudio.enabled", true); #endif // If this turns true, Moz*Gesture events are not called stopPropagation() // before content. pref("dom.debug.propagate_gesture_events_through_content", false); // The request URL of the GeoLocation backend. pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%GOOGLE_API_KEY%"); + +// Necko IPC security checks only needed for app isolation for cookies/cache/etc: +// currently irrelevant for desktop e10s +pref("network.disable.ipc.security", true);
--- a/browser/base/content/abouthome/aboutHome.js +++ b/browser/base/content/abouthome/aboutHome.js @@ -289,31 +289,59 @@ function ensureSnippetsMapThen(aCallback } function onSearchSubmit(aEvent) { let searchTerms = document.getElementById("searchText").value; let searchURL = document.documentElement.getAttribute("searchEngineURL"); if (searchURL && searchTerms.length > 0) { - const SEARCH_TOKENS = { - "_searchTerms_": encodeURIComponent(searchTerms) - } - for (let key in SEARCH_TOKENS) { - searchURL = searchURL.replace(key, SEARCH_TOKENS[key]); - } - // Send an event that a search was performed. This was originally // added so Firefox Health Report could record that a search from // about:home had occurred. let engineName = document.documentElement.getAttribute("searchEngineName"); let event = new CustomEvent("AboutHomeSearchEvent", {detail: engineName}); document.dispatchEvent(event); - window.location.href = searchURL; + const SEARCH_TOKEN = "_searchTerms_"; + let searchPostData = document.documentElement.getAttribute("searchEnginePostData"); + if (searchPostData) { + // Check if a post form already exists. If so, remove it. + const POST_FORM_NAME = "searchFormPost"; + let form = document.forms[POST_FORM_NAME]; + if (form) { + form.parentNode.removeChild(form); + } + + // Create a new post form. + form = document.body.appendChild(document.createElement("form")); + form.setAttribute("name", POST_FORM_NAME); + // Set the URL to submit the form to. + form.setAttribute("action", searchURL.replace(SEARCH_TOKEN, searchTerms)); + form.setAttribute("method", "post"); + + // Create new <input type=hidden> elements for search param. + searchPostData = searchPostData.split("&"); + for (let postVar of searchPostData) { + let [name, value] = postVar.split("="); + if (value == SEARCH_TOKEN) { + value = searchTerms; + } + let input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", name); + input.setAttribute("value", value); + form.appendChild(input); + } + // Submit the form. + form.submit(); + } else { + searchURL = searchURL.replace(SEARCH_TOKEN, encodeURIComponent(searchTerms)); + window.location.href = searchURL; + } } aEvent.preventDefault(); } function setupSearchEngine() {
--- a/browser/base/content/baseMenuOverlay.xul +++ b/browser/base/content/baseMenuOverlay.xul @@ -47,16 +47,21 @@ onclick="checkForMiddleClick(this, event);" label="&productHelp.label;" accesskey="&productHelp.accesskey;" #ifdef XP_MACOSX key="key_openHelpMac"/> #else /> #endif + <menuitem id="menu_keyboardShortcuts" + oncommand="openHelpLink('keyboard-shortcuts')" + onclick="checkForMiddleClick(this, event);" + label="&helpKeyboardShortcuts.label;" + accesskey="&helpKeyboardShortcuts.accesskey;"/> #ifdef MOZ_SERVICES_HEALTHREPORT <menuitem id="healthReport" label="&healthReport.label;" accesskey="&healthReport.accesskey;" oncommand="openHealthReport()" onclick="checkForMiddleClick(this, event);"/> #endif <menuitem id="troubleShooting"
--- a/browser/base/content/browser-appmenu.inc +++ b/browser/base/content/browser-appmenu.inc @@ -366,16 +366,20 @@ <menuitem id="appmenu_openHelp" label="&helpMenu.label;" oncommand="openHelpLink('firefox-help')" onclick="checkForMiddleClick(this, event);"/> <menuitem id="appmenu_gettingStarted" label="&appMenuGettingStarted.label;" oncommand="gBrowser.loadOneTab('https://www.mozilla.org/firefox/central/', {inBackground: false});" onclick="checkForMiddleClick(this, event);"/> + <menuitem id="appmenu_keyboardShortcuts" + label="&helpKeyboardShortcuts.label;" + oncommand="openHelpLink('keyboard-shortcuts')" + onclick="checkForMiddleClick(this, event);"/> #ifdef MOZ_SERVICES_HEALTHREPORT <menuitem id="appmenu_healthReport" label="&healthReport.label;" oncommand="openHealthReport()" onclick="checkForMiddleClick(this, event);"/> #endif <menuitem id="appmenu_troubleshootingInfo" label="&helpTroubleshootingInfo.label;"
--- a/browser/base/content/browser-context.inc +++ b/browser/base/content/browser-context.inc @@ -306,17 +306,17 @@ <menuitem id="context-openframeintab" label="&openFrameCmdInTab.label;" accesskey="&openFrameCmdInTab.accesskey;" oncommand="gContextMenu.openFrameInTab();"/> <menuitem id="context-openframe" label="&openFrameCmd.label;" accesskey="&openFrameCmd.accesskey;" oncommand="gContextMenu.openFrame();"/> - <menuseparator/> + <menuseparator id="open-frame-sep"/> <menuitem id="context-reloadframe" label="&reloadFrameCmd.label;" accesskey="&reloadFrameCmd.accesskey;" oncommand="gContextMenu.reloadFrame();"/> <menuseparator/> <menuitem id="context-bookmarkframe" label="&bookmarkThisFrameCmd.label;" accesskey="&bookmarkThisFrameCmd.accesskey;"
--- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -209,34 +209,16 @@ label="&historyButton.label;"/> <menuitem id="menu_socialSidebar" type="checkbox" autocheck="false" command="Social:ToggleSidebar"/> </menupopup> </menu> <menuseparator/> - <menuitem id="menu_stop" - class="show-only-for-keyboard" - label="&stopCmd.label;" - accesskey="&stopCmd.accesskey;" - command="Browser:Stop" -#ifdef XP_MACOSX - key="key_stop_mac"/> -#else - key="key_stop"/> -#endif - <menuitem id="menu_reload" - class="show-only-for-keyboard" - label="&reloadCmd.label;" - accesskey="&reloadCmd.accesskey;" - key="key_reload" - command="Browser:ReloadOrDuplicate" - onclick="checkForMiddleClick(this, event);"/> - <menuseparator class="show-only-for-keyboard"/> <menu id="viewFullZoomMenu" label="&fullZoom.label;" accesskey="&fullZoom.accesskey;" onpopupshowing="FullZoom.updateMenu();"> <menupopup> <menuitem id="menu_zoomEnlarge" key="key_fullZoomEnlarge" label="&fullZoomEnlargeCmd.label;" accesskey="&fullZoomEnlargeCmd.accesskey;" @@ -327,44 +309,16 @@ placespopup="true" #endif oncommand="this.parentNode._placesView._onCommand(event);" onclick="checkForMiddleClick(this, event);" onpopupshowing="if (!this.parentNode._placesView) new HistoryMenu(event);" tooltip="bhTooltip" popupsinherittooltip="true"> - <menuitem id="historyMenuBack" - class="show-only-for-keyboard" - label="&backCmd.label;" -#ifdef XP_MACOSX - key="goBackKb2" -#else - key="goBackKb" -#endif - command="Browser:BackOrBackDuplicate" - onclick="checkForMiddleClick(this, event);"/> - <menuitem id="historyMenuForward" - class="show-only-for-keyboard" - label="&forwardCmd.label;" -#ifdef XP_MACOSX - key="goForwardKb2" -#else - key="goForwardKb" -#endif - command="Browser:ForwardOrForwardDuplicate" - onclick="checkForMiddleClick(this, event);"/> - <menuitem id="historyMenuHome" - class="show-only-for-keyboard" - label="&historyHomeCmd.label;" - oncommand="BrowserGoHome(event);" - onclick="checkForMiddleClick(this, event);" - key="goHome"/> - <menuseparator id="historyMenuHomeSeparator" - class="show-only-for-keyboard"/> <menuitem id="menu_showAllHistory" label="&showAllHistoryCmd2.label;" #ifndef XP_MACOSX key="showAllHistoryKb" #endif command="Browser:ShowAllHistory"/> <menuitem id="sanitizeItem" label="&clearRecentHistory.label;" @@ -483,24 +437,16 @@ <menu id="tools-menu" label="&toolsMenu.label;" accesskey="&toolsMenu.accesskey;"> <menupopup id="menu_ToolsPopup" #ifdef MOZ_SERVICES_SYNC onpopupshowing="gSyncUI.updateUI();" #endif > - <menuitem id="menu_search" - class="show-only-for-keyboard" - label="&search.label;" - accesskey="&search.accesskey;" - key="key_search" - command="Tools:Search"/> - <menuseparator id="browserToolsSeparator" - class="show-only-for-keyboard"/> <menuitem id="menu_openDownloads" label="&downloads.label;" accesskey="&downloads.accesskey;" key="key_openDownloads" command="Tools:Downloads"/> <menuitem id="menu_openAddons" label="&addons.label;" accesskey="&addons.accesskey;"
--- a/browser/base/content/browser-plugins.js +++ b/browser/base/content/browser-plugins.js @@ -264,28 +264,28 @@ var gPluginHandler = { // plugin. Object tags can, and often do, deal with that themselves, // so don't stomp on the page developers toes. if (installable && !(plugin instanceof HTMLObjectElement)) { let installStatus = doc.getAnonymousElementByAttribute(plugin, "class", "installStatus"); installStatus.setAttribute("installable", "true"); let iconStatus = doc.getAnonymousElementByAttribute(plugin, "class", "icon"); iconStatus.setAttribute("installable", "true"); - let installLink = doc.getAnonymousElementByAttribute(plugin, "class", "installPluginLink"); + let installLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "installPluginLink"); this.addLinkClickCallback(installLink, "installSinglePlugin", plugin); } break; case "PluginBlocklisted": case "PluginOutdated": shouldShowNotification = true; break; case "PluginVulnerableUpdatable": - let updateLink = doc.getAnonymousElementByAttribute(plugin, "class", "checkForUpdatesLink"); + let updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink"); this.addLinkClickCallback(updateLink, "openPluginUpdatePage"); /* FALLTHRU */ case "PluginVulnerableNoUpdate": case "PluginClickToPlay": this._handleClickToPlayEvent(plugin); let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); let pluginName = this._getPluginInfo(plugin).pluginName; @@ -301,17 +301,17 @@ var gPluginHandler = { shouldShowNotification = true; break; case "PluginPlayPreview": this._handlePlayPreviewEvent(plugin); break; case "PluginDisabled": - let manageLink = doc.getAnonymousElementByAttribute(plugin, "class", "managePluginsLink"); + let manageLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "managePluginsLink"); this.addLinkClickCallback(manageLink, "managePlugins"); shouldShowNotification = true; break; case "PluginInstantiated": case "PluginRemoved": shouldShowNotification = true; break; @@ -340,18 +340,18 @@ var gPluginHandler = { // if this isn't a known plugin, we can't activate it // (this also guards pluginHost.getPermissionStringForType against // unexpected input) if (!gPluginHandler.isKnownPlugin(objLoadingContent)) return false; let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); - let browser = gBrowser.getBrowserForDocument(objLoadingContent.ownerDocument.defaultView.top.document); - let pluginPermission = Services.perms.testPermission(browser.currentURI, permissionString); + let principal = objLoadingContent.ownerDocument.defaultView.top.document.nodePrincipal; + let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString); let isFallbackTypeValid = objLoadingContent.pluginFallbackType >= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY && objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE; if (objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) { // checking if play preview is subject to CTP rules let playPreviewInfo = pluginHost.getPlayPreviewInfo(objLoadingContent.actualType); @@ -506,17 +506,18 @@ var gPluginHandler = { let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); // guard against giving pluginHost.getPermissionStringForType a type // not associated with any known plugin if (!gPluginHandler.isKnownPlugin(objLoadingContent)) return; let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); - let pluginPermission = Services.perms.testPermission(browser.currentURI, permissionString); + let principal = doc.defaultView.top.document.nodePrincipal; + let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString); let overlay = doc.getAnonymousElementByAttribute(aPlugin, "class", "mainBox"); if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) { if (overlay) overlay.style.visibility = "hidden"; return; } @@ -625,23 +626,38 @@ var gPluginHandler = { } else if (event == "dismissed") { // Once the popup is dismissed, clicking the icon should show the full // list again this.options.primaryPlugin = null; } }, + // Match the behaviour of nsPermissionManager + _getHostFromPrincipal: function PH_getHostFromPrincipal(principal) { + if (!principal.URI || principal.URI.schemeIs("moz-nullprincipal")) { + return "(null)"; + } + + try { + if (principal.URI.host) + return principal.URI.host; + } catch (e) {} + + return principal.origin; + }, + _makeCenterActions: function PH_makeCenterActions(notification) { - let browser = notification.browser; - let contentWindow = browser.contentWindow; + let contentWindow = notification.browser.contentWindow; let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); - let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(browser.currentURI); + let principal = contentWindow.document.nodePrincipal; + // This matches the behavior of nsPermssionManager, used for display purposes only + let principalHost = this._getHostFromPrincipal(principal); let centerActions = []; let pluginsFound = new Set(); for (let plugin of cwu.plugins) { plugin.QueryInterface(Ci.nsIObjectLoadingContent); if (plugin.getContentTypeForMIMEType(plugin.actualType) != Ci.nsIObjectLoadingContent.TYPE_PLUGIN) { continue; } @@ -661,17 +677,17 @@ var gPluginHandler = { // the tighter loop above. let permissionObj = Services.perms. getPermissionObject(principal, pluginInfo.permissionString, false); if (permissionObj) { pluginInfo.pluginPermissionHost = permissionObj.host; pluginInfo.pluginPermissionType = permissionObj.expireType; } else { - pluginInfo.pluginPermissionHost = browser.currentURI.host; + pluginInfo.pluginPermissionHost = principalHost; pluginInfo.pluginPermissionType = undefined; } let url; // TODO: allow the blocklist to specify a better link, bug 873093 if (pluginInfo.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) { url = Services.urlFormatter.formatURLPref("plugins.update.url"); } @@ -724,28 +740,29 @@ var gPluginHandler = { case "continue": break; default: Cu.reportError(Error("Unexpected plugin state: " + aNewState)); return; } let browser = aNotification.browser; + let contentWindow = browser.contentWindow; if (aNewState != "continue") { - Services.perms.add(browser.currentURI, aPluginInfo.permissionString, - permission, expireType, expireTime); + let principal = contentWindow.document.nodePrincipal; + Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString, + permission, expireType, expireTime); if (aNewState == "block") { return; } } // Manually activate the plugins that would have been automatically // activated. - let contentWindow = browser.contentWindow; let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); let plugins = cwu.plugins; let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); for (let plugin of plugins) { plugin.QueryInterface(Ci.nsIObjectLoadingContent); // canActivatePlugin will return false if this isn't a known plugin type,
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -132,16 +132,19 @@ XPCOMUtils.defineLazyModuleGetter(this, "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions", "resource:///modules/SitePermissions.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); + let gInitialPages = [ "about:blank", "about:newtab", "about:home", "about:privatebrowsing", "about:welcomeback", "about:sessionrestore" ]; @@ -723,28 +726,16 @@ const gFormSubmitObserver = { } this.panel.openPopup(element, position, offset, 0); } }; var gBrowserInit = { onLoad: function() { - // window.arguments[0]: URI to load (string), or an nsISupportsArray of - // nsISupportsStrings to load, or a xul:tab of - // a tabbrowser, which will be replaced by this - // window (for this case, all other arguments are - // ignored). - // [1]: character set (string) - // [2]: referrer (nsIURI) - // [3]: postData (nsIInputStream) - // [4]: allowThirdPartyFixup (bool) - if ("arguments" in window && window.arguments[0]) - var uriToLoad = window.arguments[0]; - gMultiProcessBrowser = gPrefService.getBoolPref("browser.tabs.remote"); var mustLoadSidebar = false; Cc["@mozilla.org/eventlistenerservice;1"] .getService(Ci.nsIEventListenerService) .addSystemEventListener(gBrowser, "click", contentAreaClick, true); @@ -775,16 +766,17 @@ var gBrowserInit = { .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIXULWindow) .XULBrowserWindow = window.XULBrowserWindow; window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); // set default character set if provided + // window.arguments[1]: character set (string) if ("arguments" in window && window.arguments.length > 1 && window.arguments[1]) { if (window.arguments[1].startsWith("charset=")) { var arrayArgComponents = window.arguments[1].split("="); if (arrayArgComponents) { //we should "inherit" the charset menu setting in a new window getMarkupDocumentViewer().defaultCharacterSet = arrayArgComponents[1]; } } @@ -941,28 +933,28 @@ var gBrowserInit = { // Misc. inits. CombinedStopReload.init(); TabsOnTop.init(); gPrivateBrowsingUI.init(); TabsInTitlebar.init(); retrieveToolbarIconsizesFromTheme(); // Wait until chrome is painted before executing code not critical to making the window visible - this._boundDelayedStartup = this._delayedStartup.bind(this, uriToLoad, mustLoadSidebar); + this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar); window.addEventListener("MozAfterPaint", this._boundDelayedStartup); this._loadHandled = true; }, _cancelDelayedStartup: function () { window.removeEventListener("MozAfterPaint", this._boundDelayedStartup); this._boundDelayedStartup = null; }, - _delayedStartup: function(uriToLoad, mustLoadSidebar) { + _delayedStartup: function(mustLoadSidebar) { let tmp = {}; Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp); let TelemetryTimestamps = tmp.TelemetryTimestamps; TelemetryTimestamps.add("delayedStartupStarted"); this._cancelDelayedStartup(); // We need to set the MozApplicationManifest event listeners up @@ -971,16 +963,17 @@ var gBrowserInit = { // will be fired. gBrowser.addEventListener("MozApplicationManifest", OfflineApps, false); // listen for offline apps on social let socialBrowser = document.getElementById("social-sidebar-browser"); socialBrowser.addEventListener("MozApplicationManifest", OfflineApps, false); + let uriToLoad = this._getUriToLoad(); var isLoadingBlank = isBlankPageURL(uriToLoad); // This pageshow listener needs to be registered before we may call // swapBrowsersAndCloseOther() to receive pageshow events fired by that. gBrowser.addEventListener("pageshow", function(event) { // Filter out events that are not about the document load we are interested in if (content && event.target == content.document) setTimeout(pageShowEventHandlers, 0, event.persisted); @@ -1007,16 +1000,19 @@ var gBrowserInit = { // Stop the about:blank load gBrowser.stop(); // make sure it has a docshell gBrowser.docShell; gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, uriToLoad); } + // window.arguments[2]: referrer (nsIURI) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) else if (window.arguments.length >= 3) { loadURI(uriToLoad, window.arguments[2], window.arguments[3] || null, window.arguments[4] || false); window.focus(); } // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3. // Such callers expect that window.arguments[0] is handled as a single URI. else @@ -1094,25 +1090,16 @@ var gBrowserInit = { // Bug 666804 - NetworkPrioritizer support for e10s if (!gMultiProcessBrowser) { let NP = {}; Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP); NP.trackBrowserWindow(window); } - // initialize the session-restore service (in case it's not already running) - let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); - ss.init(window); - - // Enable the Restore Last Session command if needed - if (ss.canRestoreLastSession && - !PrivateBrowsingUtils.isWindowPrivate(window)) - goSetCommandEnabled("Browser:RestoreLastSession", true); - PlacesToolbarHelper.init(); ctrlTab.readPref(); gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false); // Initialize the download manager some time after the app starts so that // auto-resume downloads begin (such as after crashing or quitting with // active downloads) and speeds up the first-load of the download manager UI. @@ -1164,17 +1151,16 @@ var gBrowserInit = { gSyncUI.init(); #endif #ifdef MOZ_DATA_REPORTING gDataNotificationInfoBar.init(); #endif gBrowserThumbnails.init(); - TabView.init(); setUrlAndSearchBarWidthForConditionalForwardButton(); window.addEventListener("resize", function resizeHandler(event) { if (event.target == window) setUrlAndSearchBarWidthForConditionalForwardButton(); }); // Enable developer toolbar? @@ -1279,21 +1265,56 @@ var gBrowserInit = { #ifdef MOZ_METRO gMetroPrefs.prefDomain.forEach(function(prefName) { gMetroPrefs.pushDesktopControlledPrefToMetro(prefName); Services.prefs.addObserver(prefName, gMetroPrefs, false); }, this); #endif #endif + SessionStore.promiseInitialized.then(() => { + // Enable the Restore Last Session command if needed + if (SessionStore.canRestoreLastSession && + !PrivateBrowsingUtils.isWindowPrivate(window)) + goSetCommandEnabled("Browser:RestoreLastSession", true); + + TabView.init(); + + setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0); + }); + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); - setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0); TelemetryTimestamps.add("delayedStartupFinished"); }, + // Returns the URI(s) to load at startup. + _getUriToLoad: function () { + // window.arguments[0]: URI to load (string), or an nsISupportsArray of + // nsISupportsStrings to load, or a xul:tab of + // a tabbrowser, which will be replaced by this + // window (for this case, all other arguments are + // ignored). + if (!window.arguments || !window.arguments[0]) + return null; + + let uri = window.arguments[0]; + let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"] + .getService(Ci.nsISessionStartup); + let defaultArgs = Cc["@mozilla.org/browser/clh;1"] + .getService(Ci.nsIBrowserHandler) + .defaultArgs; + + // If the given URI matches defaultArgs (the default homepage) we want + // to block its load if we're going to restore a session anyway. + if (uri == defaultArgs && sessionStartup.willOverrideHomepage) + return null; + + return uri; + }, + onUnload: function() { // In certain scenarios it's possible for unload to be fired before onload, // (e.g. if the window is being closed after browser.js loads but before the // load completes). In that case, there's nothing to do here. if (!this._loadHandled) return; gDevToolsBrowser.forgetBrowserWindow(window); @@ -2305,16 +2326,19 @@ function BrowserOnAboutPageLoad(doc) { let currentVersion = Services.prefs.getIntPref("browser.rights.version"); Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true); } docElt.setAttribute("snippetsVersion", AboutHomeUtils.snippetsVersion); let updateSearchEngine = function() { let engine = AboutHomeUtils.defaultSearchEngine; docElt.setAttribute("searchEngineName", engine.name); + docElt.setAttribute("searchEnginePostData", engine.postDataString || ""); + // Again, keep the searchEngineURL as the last attribute, because the + // mutation observer in aboutHome.js is counting on that. docElt.setAttribute("searchEngineURL", engine.searchURL); }; updateSearchEngine(); // Listen for the event that's triggered when the user changes search engine. // At this point we simply reload about:home to reflect the change. Services.obs.addObserver(updateSearchEngine, "browser-search-engine-modified", false);
--- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -29,17 +29,19 @@ addMessageListener("Browser:HideSessionR let doc = content.document; let container; if (doc.documentURI.toLowerCase() == "about:home" && (container = doc.getElementById("sessionRestoreContainer"))){ container.hidden = true; } }); -addEventListener("DOMContentLoaded", function(event) { - LoginManagerContent.onContentLoaded(event); -}); -addEventListener("DOMAutoComplete", function(event) { - LoginManagerContent.onUsernameInput(event); -}); -addEventListener("blur", function(event) { - LoginManagerContent.onUsernameInput(event); -}); +if (!Services.prefs.getBoolPref("browser.tabs.remote")) { + addEventListener("DOMContentLoaded", function(event) { + LoginManagerContent.onContentLoaded(event); + }); + addEventListener("DOMAutoComplete", function(event) { + LoginManagerContent.onUsernameInput(event); + }); + addEventListener("blur", function(event) { + LoginManagerContent.onUsernameInput(event); + }); +}
--- a/browser/base/content/newtab/dropTargetShim.js +++ b/browser/base/content/newtab/dropTargetShim.js @@ -18,59 +18,141 @@ let gDropTargetShim = { /** * The last drop target that was hovered. */ _lastDropTarget: null, /** * Initializes the drop target shim. */ - init: function DropTargetShim_init() { - let node = gGrid.node; + init: function () { + gGrid.node.addEventListener("dragstart", this, true); + }, + + /** + * Add all event listeners needed during a drag operation. + */ + _addEventListeners: function () { + gGrid.node.addEventListener("dragend", this); - // Add drag event handlers. - node.addEventListener("dragstart", this, true); - node.addEventListener("dragend", this, true); + let docElement = document.documentElement; + docElement.addEventListener("dragover", this); + docElement.addEventListener("dragenter", this); + docElement.addEventListener("drop", this); + }, + + /** + * Remove all event listeners that were needed during a drag operation. + */ + _removeEventListeners: function () { + gGrid.node.removeEventListener("dragend", this); + + let docElement = document.documentElement; + docElement.removeEventListener("dragover", this); + docElement.removeEventListener("dragenter", this); + docElement.removeEventListener("drop", this); }, /** * Handles all shim events. */ - handleEvent: function DropTargetShim_handleEvent(aEvent) { + handleEvent: function (aEvent) { switch (aEvent.type) { case "dragstart": - this._start(aEvent); + this._dragstart(aEvent); + break; + case "dragenter": + aEvent.preventDefault(); break; case "dragover": this._dragover(aEvent); break; + case "drop": + this._drop(aEvent); + break; case "dragend": - this._end(aEvent); + this._dragend(aEvent); break; } }, /** * Handles the 'dragstart' event. * @param aEvent The 'dragstart' event. */ - _start: function DropTargetShim_start(aEvent) { + _dragstart: function (aEvent) { if (aEvent.target.classList.contains("newtab-link")) { gGrid.lock(); + this._addEventListeners(); + } + }, - // XXX bug 505521 - Listen for dragover on the document. - document.documentElement.addEventListener("dragover", this, false); + /** + * Handles the 'dragover' event. + * @param aEvent The 'dragover' event. + */ + _dragover: function (aEvent) { + // XXX bug 505521 - Use the dragover event to retrieve the + // current mouse coordinates while dragging. + let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode; + gDrag.drag(sourceNode._newtabSite, aEvent); + + // Find the current drop target, if there's one. + this._updateDropTarget(aEvent); + + // If we have a valid drop target, + // let the drag-and-drop service know. + if (this._lastDropTarget) { + aEvent.preventDefault(); } }, /** - * Handles the 'drag' event and determines the current drop target. - * @param aEvent The 'drag' event. + * Handles the 'drop' event. + * @param aEvent The 'drop' event. + */ + _drop: function (aEvent) { + // We're accepting all drops. + aEvent.preventDefault(); + + // Make sure to determine the current drop target + // in case the dragover event hasn't been fired. + this._updateDropTarget(aEvent); + + // A site was successfully dropped. + this._dispatchEvent(aEvent, "drop", this._lastDropTarget); + }, + + /** + * Handles the 'dragend' event. + * @param aEvent The 'dragend' event. */ - _drag: function DropTargetShim_drag(aEvent) { + _dragend: function (aEvent) { + if (this._lastDropTarget) { + if (aEvent.dataTransfer.mozUserCancelled) { + // The drag operation was cancelled. + this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); + this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + } + + // Clean up. + this._lastDropTarget = null; + this._cellPositions = null; + } + + gGrid.unlock(); + this._removeEventListeners(); + }, + + /** + * Tries to find the current drop target and will fire + * appropriate dragenter, dragexit, and dragleave events. + * @param aEvent The current drag event. + */ + _updateDropTarget: function (aEvent) { // Let's see if we find a drop target. let target = this._findDropTarget(aEvent); if (target != this._lastDropTarget) { if (this._lastDropTarget) // We left the last drop target. this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); @@ -82,63 +164,21 @@ let gDropTargetShim = { // We left the last drop target. this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); this._lastDropTarget = target; } }, /** - * Handles the 'dragover' event as long as bug 505521 isn't fixed to get - * current mouse cursor coordinates while dragging. - * @param aEvent The 'dragover' event. - */ - _dragover: function DropTargetShim_dragover(aEvent) { - let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode; - gDrag.drag(sourceNode._newtabSite, aEvent); - - this._drag(aEvent); - }, - - /** - * Handles the 'dragend' event. - * @param aEvent The 'dragend' event. - */ - _end: function DropTargetShim_end(aEvent) { - // Make sure to determine the current drop target in case the dragenter - // event hasn't been fired. - this._drag(aEvent); - - if (this._lastDropTarget) { - if (aEvent.dataTransfer.mozUserCancelled) { - // The drag operation was cancelled. - this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); - this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); - } else { - // A site was successfully dropped. - this._dispatchEvent(aEvent, "drop", this._lastDropTarget); - } - - // Clean up. - this._lastDropTarget = null; - this._cellPositions = null; - } - - gGrid.unlock(); - - // XXX bug 505521 - Remove the document's dragover listener. - document.documentElement.removeEventListener("dragover", this, false); - }, - - /** * Determines the current drop target by matching the dragged site's position * against all cells in the grid. * @return The currently hovered drop target or null. */ - _findDropTarget: function DropTargetShim_findDropTarget() { + _findDropTarget: function () { // These are the minimum intersection values - we want to use the cell if // the site is >= 50% hovering its position. let minWidth = gDrag.cellWidth / 2; let minHeight = gDrag.cellHeight / 2; let cellPositions = this._getCellPositions(); let rect = gTransformation.getNodePosition(gDrag.draggedSite.node); @@ -169,20 +209,19 @@ let gDropTargetShim = { }, /** * Dispatches a custom DragEvent on the given target node. * @param aEvent The source event. * @param aType The event type. * @param aTarget The target node that receives the event. */ - _dispatchEvent: - function DropTargetShim_dispatchEvent(aEvent, aType, aTarget) { - + _dispatchEvent: function (aEvent, aType, aTarget) { let node = aTarget.node; let event = document.createEvent("DragEvents"); - event.initDragEvent(aType, true, true, window, 0, 0, 0, 0, 0, false, false, + // The event should not bubble to prevent recursion. + event.initDragEvent(aType, false, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, node, aEvent.dataTransfer); node.dispatchEvent(event); } };
--- a/browser/base/content/newtab/page.js +++ b/browser/base/content/newtab/page.js @@ -29,25 +29,33 @@ let gPage = { this._init(); this._updateAttributes(enabled); }, /** * Listens for notifications specific to this page. */ - observe: function Page_observe() { - let enabled = gAllPages.enabled; - this._updateAttributes(enabled); + observe: function Page_observe(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + let enabled = gAllPages.enabled; + this._updateAttributes(enabled); - // Initialize the whole page if we haven't done that, yet. - if (enabled) { - this._init(); - } else { - gUndoDialog.hide(); + // Initialize the whole page if we haven't done that, yet. + if (enabled) { + this._init(); + } else { + gUndoDialog.hide(); + } + } else if (aTopic == "page-thumbnail:create" && gGrid.ready) { + for (let site of gGrid.sites) { + if (site && site.url === aData) { + site.refreshThumbnail(); + } + } } }, /** * Updates the whole page and the grid when the storage has changed. */ update: function Page_update() { // The grid might not be ready yet as we initialize it asynchronously.
--- a/browser/base/content/newtab/sites.js +++ b/browser/base/content/newtab/sites.js @@ -126,19 +126,33 @@ Site.prototype = { let link = this._querySelector(".newtab-link"); link.setAttribute("title", tooltip); link.setAttribute("href", url); this._querySelector(".newtab-title").textContent = title; if (this.isPinned()) this._updateAttributes(true); + // request a staleness check for the thumbnail, which will cause page.js + // to be notified and call our refreshThumbnail() method. + PageThumbs.captureIfStale(this.url); + // but still display whatever thumbnail might be available now. + this.refreshThumbnail(); + }, + /** + * Refreshes the thumbnail for the site. + */ + refreshThumbnail: function Site_refreshThumbnail() { let thumbnailURL = PageThumbs.getThumbnailURL(this.url); let thumbnail = this._querySelector(".newtab-thumbnail"); + // if this is being called due to the thumbnail being updated we will + // be setting it to the same value it had before. To be confident the + // change wont be optimized away we remove the property first. + thumbnail.style.removeProperty("backgroundImage"); thumbnail.style.backgroundImage = "url(" + thumbnailURL + ")"; }, /** * Adds event handlers for the site and its buttons. */ _addEventHandlers: function Site_addEventHandlers() { // Register drag-and-drop event handlers. @@ -176,17 +190,14 @@ Site.prototype = { break; case "mouseover": this._node.removeEventListener("mouseover", this, false); this._speculativeConnect(); break; case "dragstart": gDrag.start(this, aEvent); break; - case "drag": - gDrag.drag(this, aEvent); - break; case "dragend": gDrag.end(this, aEvent); break; } } };
--- a/browser/base/content/newtab/transformations.js +++ b/browser/base/content/newtab/transformations.js @@ -151,17 +151,17 @@ let gTransformation = { targetPosition.top += this._cellBorderWidths.top; // Nothing to do here if the positions already match. if (currentPosition.left == targetPosition.left && currentPosition.top == targetPosition.top) { finish(); } else { this.setSitePosition(aSite, targetPosition); - this._whenTransitionEnded(aSite.node, finish); + this._whenTransitionEnded(aSite.node, ["left", "top"], finish); } }, /** * Rearranges a given array of sites and moves them to their new positions or * fades in/out new/removed sites. * @param aSites An array of sites to rearrange. * @param aOptions Set of options (see below). @@ -197,25 +197,29 @@ let gTransformation = { let wait = Promise.promised(function () callback && callback()); wait.apply(null, batch); }, /** * Listens for the 'transitionend' event on a given node and calls the given * callback. * @param aNode The node that is transitioned. + * @param aProperties The properties we'll wait to be transitioned. * @param aCallback The callback to call when finished. */ _whenTransitionEnded: - function Transformation_whenTransitionEnded(aNode, aCallback) { + function Transformation_whenTransitionEnded(aNode, aProperties, aCallback) { - aNode.addEventListener("transitionend", function onEnd() { - aNode.removeEventListener("transitionend", onEnd, false); - aCallback(); - }, false); + let props = new Set(aProperties); + aNode.addEventListener("transitionend", function onEnd(e) { + if (props.has(e.propertyName)) { + aNode.removeEventListener("transitionend", onEnd); + aCallback(); + } + }); }, /** * Gets a given node's opacity value. * @param aNode The node to get the opacity value from. * @return The node's opacity value. */ _getNodeOpacity: function Transformation_getNodeOpacity(aNode) { @@ -231,18 +235,19 @@ let gTransformation = { */ _setNodeOpacity: function Transformation_setNodeOpacity(aNode, aOpacity, aCallback) { if (this._getNodeOpacity(aNode) == aOpacity) { if (aCallback) aCallback(); } else { - if (aCallback) - this._whenTransitionEnded(aNode, aCallback); + if (aCallback) { + this._whenTransitionEnded(aNode, ["opacity"], aCallback); + } aNode.style.opacity = aOpacity; } }, /** * Moves a site to the cell with the given index. * @param aSite The site to move.
--- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -272,16 +272,28 @@ nsContextMenu.prototype = { !(this.isContentSelected || this.onTextInput || this.onLink || this.onImage || this.onVideo || this.onAudio || this.onSocial)); this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink && !this.onSocial) || this.onPlainTextLink); this.showItem("context-searchselect", isTextSelected); this.showItem("context-keywordfield", this.onTextInput && this.onKeywordField); this.showItem("frame", this.inFrame); + + // srcdoc cannot be opened separately due to concerns about web + // content with about:srcdoc in location bar masquerading as trusted + // chrome/addon content. + // No need to also test for this.inFrame as this is checked in the parent + // submenu. + this.showItem("context-showonlythisframe", !this.inSrcdocFrame); + this.showItem("context-openframeintab", !this.inSrcdocFrame); + this.showItem("context-openframe", !this.inSrcdocFrame); + this.showItem("context-bookmarkframe", !this.inSrcdocFrame); + this.showItem("open-frame-sep", !this.inSrcdocFrame); + this.showItem("frame-sep", this.inFrame && isTextSelected); // Hide menu entries for images, show otherwise if (this.inFrame) { if (mimeTypeIsTextBased(this.target.ownerDocument.contentType)) this.isFrameImage.removeAttribute('hidden'); else this.isFrameImage.setAttribute('hidden', 'true'); @@ -511,16 +523,17 @@ nsContextMenu.prototype = { this.onMailtoLink = false; this.onSaveableLink = false; this.link = null; this.linkURL = ""; this.linkURI = null; this.linkProtocol = ""; this.onMathML = false; this.inFrame = false; + this.inSrcdocFrame = false; this.inSyntheticDoc = false; this.hasBGImage = false; this.bgImageURL = ""; this.onEditableArea = false; this.isDesignMode = false; this.onCTPPlugin = false; // Remember the node that was clicked. @@ -674,21 +687,21 @@ nsContextMenu.prototype = { if ((this.target.nodeType == Node.TEXT_NODE && this.target.parentNode.namespaceURI == NS_MathML) || (this.target.namespaceURI == NS_MathML)) this.onMathML = true; // See if the user clicked in a frame. var docDefaultView = this.target.ownerDocument.defaultView; if (docDefaultView != docDefaultView.top) { - // srcdoc iframes are not considered frames for concerns about web - // content with about:srcdoc in location bar masqurading as trusted - // chrome/addon content. - if (!this.target.ownerDocument.isSrcdocDocument) - this.inFrame = true; + this.inFrame = true; + + if (this.target.ownerDocument.isSrcdocDocument) { + this.inSrcdocFrame = true; + } } // if the document is editable, show context menu like in text inputs if (!this.onEditableArea) { var win = this.target.ownerDocument.defaultView; if (win) { var isEditable = false; try { @@ -708,16 +721,17 @@ nsContextMenu.prototype = { if (isEditable) { this.onTextInput = true; this.onKeywordField = false; this.onImage = false; this.onLoadedImage = false; this.onCompletedImage = false; this.onMathML = false; this.inFrame = false; + this.inSrcdocFrame = false; this.hasBGImage = false; this.isDesignMode = true; this.onEditableArea = true; InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win)); var canSpell = InlineSpellCheckerUI.canSpellCheck; InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); this.showItem("spell-check-enabled", canSpell); this.showItem("spell-separator", canSpell);
--- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1462,17 +1462,18 @@ b.droppedLinkHandler = handleDroppedLink; // If we just created a new tab that loads the default // newtab url, swap in a preloaded page if possible. // Do nothing if we're a private window. let docShellsSwapped = false; if (aURI == BROWSER_NEW_TAB_URL && - !PrivateBrowsingUtils.isWindowPrivate(window)) { + !PrivateBrowsingUtils.isWindowPrivate(window) && + !gMultiProcessBrowser) { docShellsSwapped = gBrowserNewTabPreloader.newTab(t); } // Dispatch a new tab notification. We do this once we're // entirely done, so that things are in a consistent state // even if the event listener opens or closes tabs. var evt = document.createEvent("Events"); evt.initEvent("TabOpen", true, false);
--- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -118,16 +118,17 @@ MOCHITEST_BROWSER_FILES = \ browser_bug537474.js \ browser_bug550565.js \ browser_bug553455.js \ browser_bug555224.js \ browser_bug555767.js \ browser_bug556061.js \ browser_bug559991.js \ browser_bug561623.js \ + browser_bug561636.js \ browser_bug562649.js \ browser_bug563588.js \ browser_bug565575.js \ browser_bug567306.js \ browser_bug575561.js \ browser_bug575830.js \ browser_bug577121.js \ browser_bug578534.js \ @@ -187,16 +188,17 @@ MOCHITEST_BROWSER_FILES = \ browser_bug887515.js \ browser_canonizeURL.js \ browser_clearplugindata_noage.html \ browser_clearplugindata.html \ browser_clearplugindata.js \ browser_contentAreaClick.js \ browser_contextSearchTabPosition.js \ browser_CTP_drag_drop.js \ + browser_CTP_data_urls.js \ browser_ctrlTab.js \ browser_customize_popupNotification.js \ browser_customize.js \ browser_disablechrome.js \ browser_discovery.js \ browser_duplicateIDs.js \ browser_findbarClose.js \ browser_fullscreen-window-open.js \ @@ -312,16 +314,17 @@ MOCHITEST_BROWSER_FILES = \ plugin_bug820497.html \ plugin_clickToPlayAllow.html \ plugin_clickToPlayDeny.html \ plugin_hidden_to_visible.html \ plugin_test.html \ plugin_test2.html \ plugin_test3.html \ plugin_two_types.html \ + plugin_data_url.html \ plugin_unknown.html \ pluginCrashCommentAndURL.html \ POSTSearchEngine.xml \ print_postdata.sjs \ redirect_bug623155.sjs \ test_bug435035.html \ test_bug462673.html \ test_bug628179.html \ @@ -345,22 +348,15 @@ MOCHITEST_BROWSER_FILES += \ browser_bug462289.js \ $(NULL) else MOCHITEST_BROWSER_FILES += \ browser_bug565667.js \ $(NULL) endif -ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT)) -# Bug 766546. -MOCHITEST_BROWSER_FILES += \ - browser_bug561636.js \ - $(NULL) -endif - ifdef MOZ_DATA_REPORTING MOCHITEST_BROWSER_FILES += \ browser_datareporting_notification.js \ $(NULL) endif include $(topsrcdir)/config/rules.mk
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/browser_CTP_data_urls.js @@ -0,0 +1,239 @@ +var rootDir = getRootDirectory(gTestPath); +const gTestRoot = rootDir; +const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + +var gTestBrowser = null; +var gNextTest = null; +var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); + +Components.utils.import("resource://gre/modules/Services.jsm"); + +// This listens for the next opened tab and checks it is of the right url. +// opencallback is called when the new tab is fully loaded +// closecallback is called when the tab is closed +function TabOpenListener(url, opencallback, closecallback) { + this.url = url; + this.opencallback = opencallback; + this.closecallback = closecallback; + + gBrowser.tabContainer.addEventListener("TabOpen", this, false); +} + +TabOpenListener.prototype = { + url: null, + opencallback: null, + closecallback: null, + tab: null, + browser: null, + + handleEvent: function(event) { + if (event.type == "TabOpen") { + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + this.tab = event.originalTarget; + this.browser = this.tab.linkedBrowser; + gBrowser.addEventListener("pageshow", this, false); + } else if (event.type == "pageshow") { + if (event.target.location.href != this.url) + return; + gBrowser.removeEventListener("pageshow", this, false); + this.tab.addEventListener("TabClose", this, false); + var url = this.browser.contentDocument.location.href; + is(url, this.url, "Should have opened the correct tab"); + this.opencallback(this.tab, this.browser.contentWindow); + } else if (event.type == "TabClose") { + if (event.originalTarget != this.tab) + return; + this.tab.removeEventListener("TabClose", this, false); + this.opencallback = null; + this.tab = null; + this.browser = null; + // Let the window close complete + executeSoon(this.closecallback); + this.closecallback = null; + } + } +}; + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + clearAllPluginPermissions(); + Services.prefs.clearUserPref("extensions.blocklist.suppressUI"); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true); + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + prepareTest(test1a, gHttpTestRoot + "plugin_data_url.html"); +} + +function finishTest() { + clearAllPluginPermissions(); + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url) { + gNextTest = nextTest; + gTestBrowser.contentWindow.location = url; +} + +// Test that the click-to-play doorhanger still works when navigating to data URLs +function test1a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 1a, Should have a click-to-play notification"); + + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 1a, Plugin should not be activated"); + + gNextTest = test1b; + gTestBrowser.contentDocument.getElementById("data-link-1").click(); +} + +function test1b() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 1b, Should have a click-to-play notification"); + + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated"); + + // Simulate clicking the "Allow Always" button. + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test1c, "Test 1b, Waited too long for plugin to activate"); +} + +function test1c() { + clearAllPluginPermissions(); + prepareTest(test2a, gHttpTestRoot + "plugin_data_url.html"); +} + +// Test that the click-to-play notification doesn't break when navigating to data URLs with multiple plugins +function test2a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 2a, Should have a click-to-play notification"); + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 2a, Plugin should not be activated"); + + gNextTest = test2b; + gTestBrowser.contentDocument.getElementById("data-link-2").click(); +} + +function test2b() { + let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 2b, Should have a click-to-play notification"); + + // Simulate choosing "Allow now" for the test plugin + notification.reshow(); + is(notification.options.centerActions.length, 2, "Test 2b, Should have two types of plugin in the notification"); + + var centerAction = null; + for (var action of notification.options.centerActions) { + if (action.pluginName == "Test") { + centerAction = action; + break; + } + } + ok(centerAction, "Test 2b, found center action for the Test plugin"); + + var centerItem = null; + for (var item of PopupNotifications.panel.firstChild.childNodes) { + is(item.value, "block", "Test 2b, all plugins should start out blocked"); + if (item.action == centerAction) { + centerItem = item; + break; + } + } + ok(centerItem, "Test 2b, found center item for the Test plugin"); + + // "click" the button to activate the Test plugin + centerItem.value = "allownow"; + PopupNotifications.panel.firstChild._primaryButton.click(); + + let plugin = gTestBrowser.contentDocument.getElementById("test1"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test2c, "Test 2b, Waited too long for plugin to activate"); +} + +function test2c() { + let plugin = gTestBrowser.contentDocument.getElementById("test1"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 2c, Plugin should be activated"); + + clearAllPluginPermissions(); + prepareTest(test3a, gHttpTestRoot + "plugin_data_url.html"); +} + +// Test that when navigating to a data url, the plugin permission is inherited +function test3a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 3a, Should have a click-to-play notification"); + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 3a, Plugin should not be activated"); + + // Simulate clicking the "Allow Always" button. + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test3b, "Test 3a, Waited too long for plugin to activate"); +} + +function test3b() { + gNextTest = test3c; + gTestBrowser.contentDocument.getElementById("data-link-1").click(); +} + +function test3c() { + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 3c, Plugin should be activated"); + + clearAllPluginPermissions(); + prepareTest(test4b, 'data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>'); +} + +// Test that the click-to-play doorhanger still works when directly navigating to data URLs +function test4a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 4a, Should have a click-to-play notification"); + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 4a, Plugin should not be activated"); + + // Simulate clicking the "Allow Always" button. + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test4b, "Test 4a, Waited too long for plugin to activate"); +} + +function test4b() { + clearAllPluginPermissions(); + finishTest(); +}
--- a/browser/base/content/test/browser_aboutHome.js +++ b/browser/base/content/test/browser_aboutHome.js @@ -237,28 +237,82 @@ let gTests = [ } // Do a sanity check that all attributes are correctly set to begin with checkSearchUI(currEngine); let deferred = Promise.defer(); promiseBrowserAttributes(gBrowser.selectedTab).then(function() { // Test if the update propagated checkSearchUI(unusedEngines[0]); + searchbar.currentEngine = currEngine; deferred.resolve(); }); - // The following cleanup function will set currentEngine back to the previous engine + // The following cleanup function will set currentEngine back to the previous + // engine if we fail to do so above. registerCleanupFunction(function() { searchbar.currentEngine = currEngine; }); // Set the current search engine to an unused one searchbar.currentEngine = unusedEngines[0]; searchbar.select(); return deferred.promise; } +}, + +{ + desc: "Check POST search engine support", + setup: function() {}, + run: function() + { + let deferred = Promise.defer(); + let currEngine = Services.search.defaultEngine; + let searchObserver = function search_observer(aSubject, aTopic, aData) { + let engine = aSubject.QueryInterface(Ci.nsISearchEngine); + info("Observer: " + aData + " for " + engine.name); + + if (aData != "engine-added") + return; + + if (engine.name != "POST Search") + return; + + Services.search.defaultEngine = engine; + + registerCleanupFunction(function() { + Services.search.removeEngine(engine); + Services.search.defaultEngine = currEngine; + }); + + + // Ready to execute the tests! + let needle = "Search for something awesome."; + let document = gBrowser.selectedTab.linkedBrowser.contentDocument; + let searchText = document.getElementById("searchText"); + + waitForLoad(function() { + let loadedText = gBrowser.contentDocument.body.textContent; + ok(loadedText, "search page loaded"); + is(loadedText, "searchterms=" + escape(needle.replace(/\s/g, "+")), + "Search text should arrive correctly"); + deferred.resolve(); + }); + + searchText.value = needle; + searchText.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); + }; + Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false); + registerCleanupFunction(function () { + Services.obs.removeObserver(searchObserver, "browser-search-engine-modified"); + }); + Services.search.addEngine("http://test:80/browser/browser/base/content/test/POSTSearchEngine.xml", + Ci.nsISearchEngine.DATA_XML, null, false); + return deferred.promise; + } } ]; function test() { waitForExplicitFinish(); requestLongerTimeout(2); @@ -437,8 +491,20 @@ function getNumberOfSearchesByDate(aEngi if (day.has(field)) { return day.get(field) || 0; } } return 0; // No records found. } + +function waitForLoad(cb) { + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function listener() { + if (browser.currentURI.spec == "about:blank") + return; + info("Page loaded: " + browser.currentURI.spec); + browser.removeEventListener("load", listener, true); + + cb(); + }, true); +}
--- a/browser/base/content/test/browser_bug561636.js +++ b/browser/base/content/test/browser_bug561636.js @@ -369,22 +369,25 @@ function() nextTest(); }); }; Services.obs.addObserver(gObserver, "invalidformsubmit", false); tab.linkedBrowser.addEventListener("load", function(e) { - let browser = e.currentTarget; - browser.removeEventListener("load", arguments.callee, true); + // Ignore load events from the iframe. + if (tab.linkedBrowser.contentDocument == e.target) { + let browser = e.currentTarget; + browser.removeEventListener("load", arguments.callee, true); - isnot(gBrowser.selectedTab.linkedBrowser, browser, - "This tab should have been loaded in background"); - browser.contentDocument.getElementById('s').click(); + isnot(gBrowser.selectedTab.linkedBrowser, browser, + "This tab should have been loaded in background"); + browser.contentDocument.getElementById('s').click(); + } }, true); tab.linkedBrowser.loadURI(uri); }, /** * In this test, we check that the author defined error message is shown. */
--- a/browser/base/content/test/browser_pluginnotification.js +++ b/browser/base/content/test/browser_pluginnotification.js @@ -129,17 +129,17 @@ function test3() { ok(!gTestBrowser.missingPlugins, "Test 3, Should not be a missing plugin list"); new TabOpenListener("about:addons", test4, prepareTest5); var pluginNode = gTestBrowser.contentDocument.getElementById("test"); ok(pluginNode, "Test 3, Found plugin in page"); var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent); is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_DISABLED, "Test 3, plugin fallback type should be PLUGIN_DISABLED"); - var manageLink = gTestBrowser.contentDocument.getAnonymousElementByAttribute(pluginNode, "class", "managePluginsLink"); + var manageLink = gTestBrowser.contentDocument.getAnonymousElementByAttribute(pluginNode, "anonid", "managePluginsLink"); ok(manageLink, "Test 3, found 'manage' link in plugin-problem binding"); EventUtils.synthesizeMouseAtCenter(manageLink, {}, gTestBrowser.contentWindow); } function test4(tab, win) { is(win.wrappedJSObject.gViewController.currentViewId, "addons://list/plugin", "Test 4, Should have displayed the plugins pane"); gBrowser.removeTab(tab); @@ -339,17 +339,17 @@ function test18a() { var doc = gTestBrowser.contentDocument; var plugin = doc.getElementById("test"); ok(plugin, "Test 18a, Found plugin in page"); var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "Test 18a, plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE"); ok(!objLoadingContent.activated, "Test 18a, Plugin should not be activated"); var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); ok(overlay.style.visibility != "hidden", "Test 18a, Plugin overlay should exist, not be hidden"); - var updateLink = doc.getAnonymousElementByAttribute(plugin, "class", "checkForUpdatesLink"); + var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink"); ok(updateLink.style.visibility != "hidden", "Test 18a, Plugin should have an update link"); var tabOpenListener = new TabOpenListener(Services.urlFormatter.formatURLPref("plugins.update.url"), false, false); tabOpenListener.handleEvent = function(event) { if (event.type == "TabOpen") { gBrowser.tabContainer.removeEventListener("TabOpen", this, false); this.tab = event.originalTarget; ok(event.target.label == this.url, "Test 18a, Update link should open up the plugin check page"); @@ -382,17 +382,17 @@ function test18c() { var doc = gTestBrowser.contentDocument; var plugin = doc.getElementById("test"); ok(plugin, "Test 18c, Found plugin in page"); var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE, "Test 18c, plugin fallback type should be PLUGIN_VULNERABLE_NO_UPDATE"); ok(!objLoadingContent.activated, "Test 18c, Plugin should not be activated"); var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); ok(overlay.style.visibility != "hidden", "Test 18c, Plugin overlay should exist, not be hidden"); - var updateLink = doc.getAnonymousElementByAttribute(plugin, "class", "checkForUpdatesLink"); + var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink"); ok(updateLink.style.display != "block", "Test 18c, Plugin should not have an update link"); // check that click "Always allow" works with blocklisted plugins clickToPlayNotification.reshow(); PopupNotifications.panel.firstChild._primaryButton.click(); var condition = function() objLoadingContent.activated; waitForCondition(condition, test18d, "Test 18d, Waited too long for plugin to activate");
--- a/browser/base/content/test/newtab/browser_newtab_bug735987.js +++ b/browser/base/content/test/newtab/browser_newtab_bug735987.js @@ -3,24 +3,30 @@ function runTests() { yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); - yield simulateDrop(1); + yield simulateExternalDrop(1); checkGrid("0,99p,1,2,3,4,5,6,7"); yield blockCell(1); checkGrid("0,1,2,3,4,5,6,7,8"); - yield simulateDrop(1); + yield simulateExternalDrop(1); checkGrid("0,99p,1,2,3,4,5,6,7"); + // Simulate a restart and force the next about:newtab + // instance to read its data from the storage again. NewTabUtils.blockedLinks.resetCache(); + + // Update all open pages, e.g. preloaded ones. + NewTabUtils.allPages.update(); + yield addNewTabPageTab(); checkGrid("0,99p,1,2,3,4,5,6,7"); yield blockCell(1); checkGrid("0,1,2,3,4,5,6,7,8"); }
--- a/browser/base/content/test/newtab/browser_newtab_drag_drop.js +++ b/browser/base/content/test/newtab/browser_newtab_drag_drop.js @@ -12,17 +12,17 @@ function runTests() { // test a simple drag-and-drop scenario yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); - yield simulateDrop(1, 0); + yield simulateDrop(0, 1); checkGrid("1,0p,2,3,4,5,6,7,8"); // drag a cell to its current cell and make sure it's not pinned afterwards yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); @@ -32,86 +32,86 @@ function runTests() { // ensure that pinned pages aren't moved if that's not necessary yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",1,2"); yield addNewTabPageTab(); checkGrid("0,1p,2p,3,4,5,6,7,8"); - yield simulateDrop(3, 0); + yield simulateDrop(0, 3); checkGrid("3,1p,2p,0p,4,5,6,7,8"); // pinned sites should always be moved around as blocks. if a pinned site is // moved around, neighboring pinned are affected as well yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks("0,1"); yield addNewTabPageTab(); checkGrid("0p,1p,2,3,4,5,6,7,8"); - yield simulateDrop(0, 2); + yield simulateDrop(2, 0); checkGrid("2p,0p,1p,3,4,5,6,7,8"); // pinned sites should not be pushed out of the grid (unless there are only // pinned ones left on the grid) yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,7,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7p,8p"); - yield simulateDrop(8, 2); + yield simulateDrop(2, 8); checkGrid("0,1,3,4,5,6,7p,8p,2p"); // make sure that pinned sites are re-positioned correctly yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks("0,1,2,,,5"); yield addNewTabPageTab(); checkGrid("0p,1p,2p,3,4,5p,6,7,8"); - yield simulateDrop(4, 0); + yield simulateDrop(0, 4); checkGrid("3,1p,2p,4,0p,5p,6,7,8"); // drag a new site onto the very first cell yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,7,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7p,8p"); - yield simulateDrop(0); + yield simulateExternalDrop(0); checkGrid("99p,0,1,2,3,4,5,7p,8p"); // drag a new site onto the grid and make sure that pinned cells don't get // pushed out yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,7,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7p,8p"); - yield simulateDrop(7); + yield simulateExternalDrop(7); checkGrid("0,1,2,3,4,5,7p,99p,8p"); // drag a new site beneath a pinned cell and make sure the pinned cell is // not moved yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8p"); - yield simulateDrop(7); + yield simulateExternalDrop(7); checkGrid("0,1,2,3,4,5,6,99p,8p"); // drag a new site onto a block of pinned sites and make sure they're shifted // around accordingly yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks("0,1,2,,,,,,"); yield addNewTabPageTab(); checkGrid("0p,1p,2p"); - yield simulateDrop(1); + yield simulateExternalDrop(1); checkGrid("0p,99p,1p,2p,3,4,5,6,7"); }
--- a/browser/base/content/test/newtab/browser_newtab_tabsync.js +++ b/browser/base/content/test/newtab/browser_newtab_tabsync.js @@ -38,22 +38,22 @@ function runTests() { // remove a cell yield blockCell(1); checkGrid("0,2,3,4,5,6,7,8,9"); checkGrid("0,2,3,4,5,6,7,8,9", oldSites); ok(resetButton.hasAttribute("modified"), "page is modified"); ok(oldResetButton.hasAttribute("modified"), "page is modified"); // insert a new cell by dragging - yield simulateDrop(1); + yield simulateExternalDrop(1); checkGrid("0,99p,2,3,4,5,6,7,8"); checkGrid("0,99p,2,3,4,5,6,7,8", oldSites); // drag a cell around - yield simulateDrop(1, 2); + yield simulateDrop(2, 1); checkGrid("0,2p,99p,3,4,5,6,7,8"); checkGrid("0,2p,99p,3,4,5,6,7,8", oldSites); // reset the new tab page yield getContentWindow().gToolbar.reset(TestRunner.next); checkGrid("0,1,2,3,4,5,6,7,8"); checkGrid("0,1,2,3,4,5,6,7,8", oldSites); ok(!resetButton.hasAttribute("modified"), "page is not modified");
--- a/browser/base/content/test/newtab/head.js +++ b/browser/base/content/test/newtab/head.js @@ -1,26 +1,29 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled"; Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true); let tmp = {}; +Cu.import("resource://gre/modules/Promise.jsm", tmp); Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp); Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader) .loadSubScript("chrome://browser/content/sanitize.js", tmp); - -let {NewTabUtils, Sanitizer} = tmp; +let {Promise, NewTabUtils, Sanitizer} = tmp; let uri = Services.io.newURI("about:newtab", null, null); let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri); +let isMac = ("nsILocalFileMac" in Ci); +let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc); +let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc); let gWindow = window; registerCleanupFunction(function () { while (gWindow.gBrowser.tabs.length > 1) gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]); Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED); }); @@ -300,36 +303,176 @@ function pinCell(aIndex, aPinIndex) { * @param aIndex The cell index. */ function unpinCell(aIndex) { whenPagesUpdated(); getCell(aIndex).site.unpin(); } /** - * Simulates a drop and drop operation. - * @param aDropIndex The cell index of the drop target. - * @param aDragIndex The cell index containing the dragged site (optional). + * Simulates a drag and drop operation. + * @param aSourceIndex The cell index containing the dragged site. + * @param aDestIndex The cell index of the drop target. + */ +function simulateDrop(aSourceIndex, aDestIndex) { + let src = getCell(aSourceIndex).site.node; + let dest = getCell(aDestIndex).node; + + // Drop 'src' onto 'dest' and continue testing when all newtab + // pages have been updated (i.e. the drop operation is completed). + startAndCompleteDragOperation(src, dest, whenPagesUpdated); +} + +/** + * Simulates a drag and drop operation. Instead of rearranging a site that is + * is already contained in the newtab grid, this is used to simulate dragging + * an external link onto the grid e.g. the text from the URL bar. + * @param aDestIndex The cell index of the drop target. */ -function simulateDrop(aDropIndex, aDragIndex) { - let draggedSite; - let {gDrag: drag, gDrop: drop} = getContentWindow(); - let event = createDragEvent("drop", "http://example.com/#99\nblank"); +function simulateExternalDrop(aDestIndex) { + let dest = getCell(aDestIndex).node; + + // Create an iframe that contains the external link we'll drag. + createExternalDropIframe().then(iframe => { + let link = iframe.contentDocument.getElementById("link"); + + // Drop 'link' onto 'dest'. + startAndCompleteDragOperation(link, dest, () => { + // Wait until the drop operation is complete + // and all newtab pages have been updated. + whenPagesUpdated(() => { + // Clean up and remove the iframe. + iframe.remove(); + // Continue testing. + TestRunner.next(); + }); + }); + }); +} + +/** + * Starts and complete a drag-and-drop operation. + * @param aSource The node that is being dragged. + * @param aDest The node we're dragging aSource onto. + * @param aCallback The function that is called when we're done. + */ +function startAndCompleteDragOperation(aSource, aDest, aCallback) { + // Start by pressing the left mouse button. + synthesizeNativeMouseLDown(aSource); + + // Move the mouse in 5px steps until the drag operation starts. + let offset = 0; + let interval = setInterval(() => { + synthesizeNativeMouseDrag(aSource, offset += 5); + }, 10); + + // When the drag operation has started we'll move + // the dragged element to its target position. + aSource.addEventListener("dragstart", function onDragStart() { + aSource.removeEventListener("dragstart", onDragStart); + clearInterval(interval); + + // Place the cursor above the drag target. + synthesizeNativeMouseMove(aDest); + }); + + // As soon as the dragged element hovers the target, we'll drop it. + aDest.addEventListener("dragenter", function onDragEnter() { + aDest.removeEventListener("dragenter", onDragEnter); + + // Finish the drop operation. + synthesizeNativeMouseLUp(aDest); + aCallback(); + }); +} - if (typeof aDragIndex != "undefined") - draggedSite = getCell(aDragIndex).site; +/** + * Helper function that creates a temporary iframe in the about:newtab + * document. This will contain a link we can drag to the test the dropping + * of links from external documents. + */ +function createExternalDropIframe() { + const url = "data:text/html;charset=utf-8," + + "<a id='link' href='http://example.com/%2399'>link</a>"; + + let deferred = Promise.defer(); + let doc = getContentDocument(); + let iframe = doc.createElement("iframe"); + iframe.setAttribute("src", url); + iframe.style.width = "50px"; + iframe.style.height = "50px"; + + let margin = doc.getElementById("newtab-margin-top"); + margin.appendChild(iframe); - if (draggedSite) - drag.start(draggedSite, event); + iframe.addEventListener("load", function onLoad() { + iframe.removeEventListener("load", onLoad); + executeSoon(() => deferred.resolve(iframe)); + }); + + return deferred.promise; +} + +/** + * Fires a synthetic 'mousedown' event on the current about:newtab page. + * @param aElement The element used to determine the cursor position. + */ +function synthesizeNativeMouseLDown(aElement) { + if (isLinux) { + let win = aElement.ownerDocument.defaultView; + EventUtils.synthesizeMouseAtCenter(aElement, {type: "mousedown"}, win); + } else { + let msg = isWindows ? 2 : 1; + synthesizeNativeMouseEvent(aElement, msg); + } +} - whenPagesUpdated(); - drop.drop(getCell(aDropIndex), event); +/** + * Fires a synthetic 'mouseup' event on the current about:newtab page. + * @param aElement The element used to determine the cursor position. + */ +function synthesizeNativeMouseLUp(aElement) { + let msg = isWindows ? 4 : (isMac ? 2 : 7); + synthesizeNativeMouseEvent(aElement, msg); +} + +/** + * Fires a synthetic mouse drag event on the current about:newtab page. + * @param aElement The element used to determine the cursor position. + * @param aOffsetX The left offset that is added to the position. + */ +function synthesizeNativeMouseDrag(aElement, aOffsetX) { + let msg = isMac ? 6 : 1; + synthesizeNativeMouseEvent(aElement, msg, aOffsetX); +} - if (draggedSite) - drag.end(draggedSite); +/** + * Fires a synthetic 'mousemove' event on the current about:newtab page. + * @param aElement The element used to determine the cursor position. + */ +function synthesizeNativeMouseMove(aElement) { + let msg = isMac ? 5 : 1; + synthesizeNativeMouseEvent(aElement, msg); +} + +/** + * Fires a synthetic mouse event on the current about:newtab page. + * @param aElement The element used to determine the cursor position. + * @param aOffsetX The left offset that is added to the position (optional). + * @param aOffsetY The top offset that is added to the position (optional). + */ +function synthesizeNativeMouseEvent(aElement, aMsg, aOffsetX = 0, aOffsetY = 0) { + let rect = aElement.getBoundingClientRect(); + let win = aElement.ownerDocument.defaultView; + let x = aOffsetX + win.mozInnerScreenX + rect.left + rect.width / 2; + let y = aOffsetY + win.mozInnerScreenY + rect.top + rect.height / 2; + + win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .sendNativeMouseEvent(x, y, aMsg, 0, null); } /** * Sends a custom drag event to a given DOM element. * @param aEventType The drag event's type. * @param aTarget The DOM element that the event is dispatched to. * @param aData The event's drag data (optional). */
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/plugin_data_url.html @@ -0,0 +1,11 @@ +<html> +<body> + <a id="data-link-1" href='data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>'> + data: with one plugin + </a><br /> + <a id="data-link-2" href='data:text/html,<embed id="test1" style="width: 200px; height: 200px" type="application/x-test"/><embed id="test2" style="width: 200px; height: 200px" type="application/x-second-test"/>'> + data: with two plugins + </a><br /> + <object id="test" style="width: 200px; height: 200px" type="application/x-test"></object> +</body> +</html>
--- a/browser/base/content/test/social/browser_social_window.js +++ b/browser/base/content/test/social/browser_social_window.js @@ -21,18 +21,17 @@ let createdWindows = []; function openWindowAndWaitForInit(callback) { // this notification tells us SocialUI.init() has been run... let topic = "browser-delayed-startup-finished"; let w = OpenBrowserWindow(); createdWindows.push(w); Services.obs.addObserver(function providerSet(subject, topic, data) { Services.obs.removeObserver(providerSet, topic); info(topic + " observer was notified - continuing test"); - // executeSoon to let the browser UI observers run first - executeSoon(function() {callback(w)}); + executeSoon(() => callback(w)); }, topic, false); } function postTestCleanup(cb) { for (let w of createdWindows) w.close(); createdWindows = []; Services.prefs.clearUserPref("social.enabled");
--- a/browser/base/content/test/subtst_contextmenu.html +++ b/browser/base/content/test/subtst_contextmenu.html @@ -62,10 +62,11 @@ Browser context menu subtest. </div> <div id="test-select-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div> <div id="test-select-text-link">http://mozilla.com</div> <a id="test-image-link" href="#"><img src="ctxmenu-image.png"></a> <input id="test-select-input-text" type="text" value="input"> <input id="test-select-input-text-type-password" type="password" value="password"> <embed id="test-plugin" style="width: 200px; height: 200px;" type="application/x-test"></embed> <img id="test-longdesc" src="ctxmenu-image.png" longdesc="http://www.mozilla.org"></embed> +<iframe id="test-srcdoc" width="98" height="98" srcdoc="Hello World" style="border: 1px solid black"></iframe> </body> </html>
--- a/browser/base/content/test/test_contextmenu.html +++ b/browser/base/content/test/test_contextmenu.html @@ -950,16 +950,43 @@ function runTest(testNum) { "---", null, "context-saveimage", true, "context-sendimage", true, "context-setDesktopBackground", true, "context-viewimageinfo", true, "context-viewimagedesc", true ].concat(inspectItems)); closeContextMenu(); + openContextMenuFor(srcdoc); + return; + + case 31: + // Context menu for an iframe with srcdoc attribute set + checkContextMenu(["context-back", false, + "context-forward", false, + "context-reload", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "frame", null, + ["context-reloadframe", true, + "---", null, + "context-saveframe", true, + "---", null, + "context-printframe", true, + "---", null, + "context-viewframesource", true, + "context-viewframeinfo", true], null, + "---", null, + "context-viewsource", true, + "context-viewinfo", true + ].concat(inspectItems)); // finish test subwindow.close(); SimpleTest.finish(); return; /* * Other things that would be nice to test: @@ -979,17 +1006,17 @@ function runTest(testNum) { var testNum = 1; var subwindow, chromeWin, contextMenu, lastElement; var text, link, mailto, input, img, canvas, video_ok, video_bad, video_bad2, iframe, video_in_iframe, image_in_iframe, textarea, contenteditable, inputspell, pagemenu, dom_full_screen, plainTextItems, audio_in_video, selecttext, selecttextlink, imagelink, select_inputtext, select_inputtext_password, - plugin, longdesc; + plugin, longdesc, iframe; function startTest() { chromeWin = SpecialPowers.wrap(subwindow) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShellTreeItem) .rootTreeItem .QueryInterface(Ci.nsIInterfaceRequestor) @@ -1029,16 +1056,17 @@ function startTest() { pagemenu = subwindow.document.getElementById("test-pagemenu"); dom_full_screen = subwindow.document.getElementById("test-dom-full-screen"); selecttext = subwindow.document.getElementById("test-select-text"); selecttextlink = subwindow.document.getElementById("test-select-text-link"); select_inputtext = subwindow.document.getElementById("test-select-input-text"); select_inputtext_password = subwindow.document.getElementById("test-select-input-text-type-password"); plugin = subwindow.document.getElementById("test-plugin"); longdesc = subwindow.document.getElementById("test-longdesc"); + srcdoc = subwindow.document.getElementById("test-srcdoc"); contextMenu.addEventListener("popupshown", function() { runTest(++testNum); }, false); runTest(1); } // We open this in a separate window, because the Mochitests run inside a frame. // The frame causes an extra menu item, and prevents running the test // standalone (ie, clicking the test name in the Mochitest window) to see
--- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -1595,17 +1595,18 @@ var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box"); if (this._states.SINGLE == state) { grid.hidden = true; this._setupSingleState(); return; } - this._setupDescription("pluginActivateMultiple.message"); + let host = gPluginHandler._getHostFromPrincipal(this.notification.browser.contentWindow.document.nodePrincipal); + this._setupDescription("pluginActivateMultiple.message", null, host); var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox"); var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties"); this._primaryButton.label = dialogStrings.GetStringFromName("button-accept"); this._primaryButton.setAttribute("default", "true"); this._secondaryButton.label = dialogStrings.GetStringFromName("button-cancel"); @@ -1758,19 +1759,16 @@ <parameter name="host" /> <body><![CDATA[ var bsn = this._brandShortName; var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); while (span.lastChild) { span.removeChild(span.lastChild); } - if (!host) { - host = this.notification.browser.currentURI.host; - } var args = ["__host__", this._brandShortName]; if (pluginName) { args.unshift(pluginName); } var bases = gNavigatorBundle.getFormattedString(baseString, args). split("__host__", 2); span.appendChild(document.createTextNode(bases[0]));
--- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -13,17 +13,18 @@ XPCOMUtils.defineLazyGetter(this, "BROWS const PREF = "browser.newtab.url"; function getNewTabPageURL() { if (!Services.prefs.prefHasUserValue(PREF)) { if (PrivateBrowsingUtils.isWindowPrivate(window) && !PrivateBrowsingUtils.permanentPrivateBrowsing) return "about:privatebrowsing"; } - return Services.prefs.getCharPref(PREF) || "about:blank"; + let url = Services.prefs.getComplexValue(PREF, Ci.nsISupportsString).data; + return url || "about:blank"; } function update() { BROWSER_NEW_TAB_URL = getNewTabPageURL(); } Services.prefs.addObserver(PREF, update, false);
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -63,16 +63,19 @@ XPCOMUtils.defineLazyModuleGetter(this, "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", "resource://gre/modules/PlacesBackups.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", + "resource:///modules/sessionstore/SessionStore.jsm"); + const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser"; const PREF_PLUGINS_UPDATEURL = "plugins.update.url"; // We try to backup bookmarks at idle times, to avoid doing that at shutdown. // Number of idle seconds before trying to backup bookmarks. 15 minutes. const BOOKMARKS_BACKUP_IDLE_TIME = 15 * 60; // Minimum interval in milliseconds between backups. const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000; @@ -168,17 +171,17 @@ BrowserGlue.prototype = { switch (topic) { case "prefservice:after-app-defaults": this._onAppDefaults(); break; case "final-ui-startup": this._finalUIStartup(); break; case "browser-delayed-startup-finished": - this._onFirstWindowLoaded(); + this._onFirstWindowLoaded(subject); Services.obs.removeObserver(this, "browser-delayed-startup-finished"); break; case "sessionstore-windows-restored": this._onWindowsRestored(); break; case "browser:purge-session-history": // reset the console service's error buffer Services.console.logStringMessage(null); // clear the console (in case it's open) @@ -574,36 +577,36 @@ BrowserGlue.prototype = { let nb = win.document.getElementById("global-notificationbox"); nb.appendNotification(message, "reset-unused-profile", "chrome://global/skin/icons/question-16.png", nb.PRIORITY_INFO_LOW, buttons); }, // the first browser window has finished initializing - _onFirstWindowLoaded: function BG__onFirstWindowLoaded() { + _onFirstWindowLoaded: function BG__onFirstWindowLoaded(aWindow) { #ifdef XP_WIN // For windows seven, initialize the jump list module. const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; if (WINTASKBAR_CONTRACTID in Cc && Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) { let temp = {}; Cu.import("resource:///modules/WindowsJumpLists.jsm", temp); temp.WinTaskbarJumpList.startup(); } #endif + SessionStore.init(aWindow); this._trackSlowStartup(); // Offer to reset a user's profile if it hasn't been used for 60 days. const OFFER_PROFILE_RESET_INTERVAL_MS = 60 * 24 * 60 * 60 * 1000; - let processStartupTime = Services.startup.getStartupInfo().process; let lastUse = Services.appinfo.replacedLockTime; - if (processStartupTime && lastUse && - processStartupTime.getTime() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) { + if (lastUse && + Date.now() - lastUse >= OFFER_PROFILE_RESET_INTERVAL_MS) { this._resetUnusedProfileNotification(); } }, /** * Profile shutdown handler (contains profile cleanup routines). * All components depending on Places should be shut down in * _onPlacesShutdown() and not here.
--- a/browser/components/places/content/places.js +++ b/browser/components/places/content/places.js @@ -114,23 +114,16 @@ var PlacesOrganizer = { var findKey = document.getElementById("key_find"); findKey.setAttribute("command", "OrganizerCommand_find:all"); // 2. Disable some keybindings from browser.xul var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"]; for (var i=0; i < elements.length; i++) { document.getElementById(elements[i]).setAttribute("disabled", "true"); } - - // 3. Disable the keyboard shortcut for the History menu back/forward - // in order to support those in the Library - var historyMenuBack = document.getElementById("historyMenuBack"); - historyMenuBack.removeAttribute("key"); - var historyMenuForward = document.getElementById("historyMenuForward"); - historyMenuForward.removeAttribute("key"); #endif // remove the "Properties" context-menu item, we've our own details pane document.getElementById("placesContext") .removeChild(document.getElementById("placesContext_show:info")); ContentArea.focus(); },
--- a/browser/components/sessionstore/content/content-sessionStore.js +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -1,40 +1,77 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * 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/. */ function debug(msg) { Services.console.logStringMessage("SessionStoreContent: " + msg); } +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); + /** * Listens for and handles content events that we need for the * session store service to be notified of state changes in content. */ let EventListener = { DOM_EVENTS: [ - "pageshow", "change", "input" + "pageshow", "change", "input", "MozStorageChanged" ], init: function () { this.DOM_EVENTS.forEach(e => addEventListener(e, this, true)); }, handleEvent: function (event) { switch (event.type) { case "pageshow": if (event.persisted) sendAsyncMessage("SessionStore:pageshow"); break; case "input": case "change": sendAsyncMessage("SessionStore:input"); break; + case "MozStorageChanged": { + let isSessionStorage = true; + // We are only interested in sessionStorage events + try { + if (event.storageArea != content.sessionStorage) { + isSessionStorage = false; + } + } catch (ex) { + // This page does not even have sessionStorage + // (this is typically the case of about: pages) + isSessionStorage = false; + } + if (isSessionStorage) { + sendAsyncMessage("SessionStore:MozStorageChanged"); + } + break; + } default: debug("received unknown event '" + event.type + "'"); break; } } }; +EventListener.init(); -EventListener.init(); +let ProgressListener = { + init: function() { + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); + }, + onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { + // We are changing page, so time to invalidate the state of the tab + sendAsyncMessage("SessionStore:loadStart"); + }, + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {}, + onProgressChange: function() {}, + onStatusChange: function() {}, + onSecurityChange: function() {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]) +}; +ProgressListener.init();
--- a/browser/components/sessionstore/nsISessionStartup.idl +++ b/browser/components/sessionstore/nsISessionStartup.idl @@ -5,34 +5,48 @@ #include "nsISupports.idl" /** * nsISessionStore keeps track of the current browsing state - i.e. * tab history, cookies, scroll state, form data, POSTDATA and window features * - and allows to restore everything into one window. */ -[scriptable, uuid(35235b39-7098-4b3b-8e28-cd004a88b06f)] +[scriptable, uuid(51f4b9f0-f3d2-11e2-bb62-2c24dd830245)] interface nsISessionStartup: nsISupports { /** * Return a promise that is resolved once initialization * is complete. */ readonly attribute jsval onceInitialized; // Get session state readonly attribute jsval state; /** - * Determine if session should be restored + * Determines whether there is a pending session restore and makes sure that + * we're initialized before returning. If we're not yet this will read the + * session file synchronously. */ boolean doRestore(); /** + * Returns whether we will restore a session that ends up replacing the + * homepage. The browser uses this to not start loading the homepage if + * we're going to stop its load anyway shortly after. + * + * This is meant to be an optimization for the average case that loading the + * session file finishes before we may want to start loading the default + * homepage. Should this be called before the session file has been read it + * will just return false. + */ + readonly attribute bool willOverrideHomepage; + + /** * What type of session we're restoring. * NO_SESSION There is no data available from the previous session * RECOVER_SESSION The last session crashed. It will either be restored or * about:sessionrestore will be shown. * RESUME_SESSION The previous session should be restored at startup * DEFER_SESSION The previous session is fine, but it shouldn't be restored * without explicit action (with the exception of pinned tabs) */
--- a/browser/components/sessionstore/nsISessionStore.idl +++ b/browser/components/sessionstore/nsISessionStore.idl @@ -20,25 +20,20 @@ interface nsIDOMNode; * global |window| object to the API, though (or |top| from a sidebar). * From elsewhere you can get browser windows through the nsIWindowMediator * by looking for "navigator:browser" windows. * * * "Tabbrowser tabs" are all the child nodes of a browser window's * |gBrowser.tabContainer| such as e.g. |gBrowser.selectedTab|. */ -[scriptable, uuid(0aa5492c-15ad-4376-8eac-28895796826e)] +[scriptable, uuid(700756cc-f5c7-11e2-b842-59d9dc830245)] interface nsISessionStore : nsISupports { /** - * Initialize the service - */ - void init(in nsIDOMWindow aWindow); - - /** * Is it possible to restore the previous session. Will always be false when * in Private Browsing mode. */ attribute boolean canRestoreLastSession; /** * Restore the previous session if possible. This will not overwrite the * current session. Instead the previous session will be merged into the
--- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -53,17 +53,25 @@ const MESSAGES = [ // The content script tells us that its form data (or that of one of its // subframes) might have changed. This can be the contents or values of // standard form fields or of ContentEditables. "SessionStore:input", // The content script has received a pageshow event. This happens when a // page is loaded from bfcache without any network activity, i.e. when // clicking the back or forward button. - "SessionStore:pageshow" + "SessionStore:pageshow", + + // The content script has received a MozStorageChanged event dealing + // with a change in the contents of the sessionStorage. + "SessionStore:MozStorageChanged", + + // The content script tells us that a new page just started loading in a + // browser. + "SessionStore:loadStart" ]; // These are tab events that we listen to. const TAB_EVENTS = [ "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned", "TabUnpinned" ]; @@ -74,17 +82,17 @@ const TAB_EVENTS = [ Cu.import("resource://gre/modules/Services.jsm", this); Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); // debug.js adds NS_ASSERT. cf. bug 669196 Cu.import("resource://gre/modules/debug.js", this); Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", this); Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this); -Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", this); +Cu.import("resource://gre/modules/Promise.jsm", this); Cu.import("resource://gre/modules/Task.jsm", this); XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup", "@mozilla.org/browser/sessionstartup;1", "nsISessionStartup"); XPCOMUtils.defineLazyServiceGetter(this, "gScreenManager", "@mozilla.org/gfx/screenmanager;1", "nsIScreenManager"); // List of docShell capabilities to (re)store. These are automatically @@ -115,24 +123,31 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", "resource:///modules/sessionstore/_SessionFile.jsm"); #ifdef MOZ_CRASHREPORTER XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", "@mozilla.org/xre/app-info;1", "nsICrashReporter"); #endif +/** + * |true| if we are in debug mode, |false| otherwise. + * Debug mode is controlled by preference browser.sessionstore.debug + */ +let gDebuggingEnabled = false; function debug(aMsg) { - aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); - Services.console.logStringMessage(aMsg); + if (gDebuggingEnabled) { + aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n"); + Services.console.logStringMessage(aMsg); + } } this.SessionStore = { get promiseInitialized() { - return SessionStoreInternal.promiseInitialized.promise; + return SessionStoreInternal.promiseInitialized; }, get canRestoreLastSession() { return SessionStoreInternal.canRestoreLastSession; }, set canRestoreLastSession(val) { SessionStoreInternal.canRestoreLastSession = val; @@ -320,17 +335,17 @@ let SessionStoreInternal = { _lastSessionState: null, // When starting Firefox with a single private window, this is the place // where we keep the session we actually wanted to restore in case the user // decides to later open a non-private window as well. _deferredInitialState: null, // A promise resolved once initialization is complete - _promiseInitialization: Promise.defer(), + _deferredInitialized: Promise.defer(), // Whether session has been initialized _sessionInitialized: false, // True if session store is disabled by multi-process browsing. // See bug 516755. _disabledForMultiProcess: false, @@ -343,56 +358,69 @@ let SessionStoreInternal = { // previous session is not always restored when // "sessionstore.resume_from_crash" is true. _resume_session_once_on_shutdown: null, /** * A promise fulfilled once initialization is complete. */ get promiseInitialized() { - return this._promiseInitialization; + return this._deferredInitialized.promise; }, /* ........ Public Getters .............. */ get canRestoreLastSession() { return this._lastSessionState; }, set canRestoreLastSession(val) { // Cheat a bit; only allow false. if (val) return; this._lastSessionState = null; }, - /* ........ Global Event Handlers .............. */ - /** - * Initialize the component + * Initialize the sessionstore service. */ - initService: function ssi_initService() { - if (this._sessionInitialized) { - return; - } + init: function (aWindow) { + if (this._initialized) { + throw new Error("SessionStore.init() must only be called once!"); + } + + if (!aWindow) { + throw new Error("SessionStore.init() must be called with a valid window."); + } + TelemetryTimestamps.add("sessionRestoreInitialized"); OBSERVING.forEach(function(aTopic) { Services.obs.addObserver(this, aTopic, true); }, this); this._initPrefs(); - + this._initialized = true; this._disabledForMultiProcess = this._prefBranch.getBoolPref("tabs.remote"); // this pref is only read at startup, so no need to observe it this._sessionhistory_max_entries = this._prefBranch.getIntPref("sessionhistory.max_entries"); - gSessionStartup.onceInitialized.then( - this.initSession.bind(this) - ); + // Wait until nsISessionStartup has finished reading the session data. + gSessionStartup.onceInitialized.then(() => { + // Parse session data and start restoring. + this.initSession(); + + // Start tracking the given (initial) browser window. + if (!aWindow.closed) { + this.onLoad(aWindow); + } + + // Let everyone know we're done. + this._deferredInitialized.resolve(); + }); }, initSession: function ssi_initSession() { let ss = gSessionStartup; try { if (ss.doRestore() || ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) this._initialState = ss.state; @@ -461,48 +489,27 @@ let SessionStoreInternal = { this._initialState.windows.forEach(function(aWindow) { delete aWindow.__lastSessionWindowID; }); } } catch (ex) { debug("The session file is invalid: " + ex); } } - // A Lazy getter for the sessionstore.js backup promise. - XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function () { - // We're creating a backup of sessionstore.js by moving it to .bak - // because that's a lot faster than creating a copy. sessionstore.js - // would be overwritten shortly afterwards anyway so we can save time - // and just move instead of copy. - return _SessionFile.moveToBackupPath(); - }); - // at this point, we've as good as resumed the session, so we can // clear the resume_session_once flag, if it's set if (this._loadState != STATE_QUITTING && this._prefBranch.getBoolPref("sessionstore.resume_session_once")) this._prefBranch.setBoolPref("sessionstore.resume_session_once", false); this._initEncoding(); this._performUpgradeBackup(); - // The service is ready. Backup-on-upgrade might still be in progress, - // but we do not have a race condition: - // - // - if the file to backup is named sessionstore.js, secondary - // backup will be started in this tick, so any further I/O will be - // scheduled to start after the secondary backup is complete; - // - // - if the file is named sessionstore.bak, it will only be erased - // by the getter to |_backupSessionFileOnce|, which specifically - // waits until the secondary backup has been completed or deemed - // useless before causing any side-effects. this._sessionInitialized = true; - this._promiseInitialization.resolve(); }, /** * If this is the first time we launc this build of Firefox, * backup sessionstore.js. */ _performUpgradeBackup: function ssi_performUpgradeBackup() { // Perform upgrade backup, if necessary @@ -532,19 +539,23 @@ let SessionStoreInternal = { _initEncoding : function ssi_initEncoding() { // The (UTF-8) encoder used to write to files. XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function () { return new TextEncoder(); }); }, _initPrefs : function() { - XPCOMUtils.defineLazyGetter(this, "_prefBranch", function () { - return Services.prefs.getBranch("browser."); - }); + this._prefBranch = Services.prefs.getBranch("browser."); + + gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); + + Services.prefs.addObserver("browser.sessionstore.debug", () => { + gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug"); + }, false); // minimal interval between two save operations (in milliseconds) XPCOMUtils.defineLazyGetter(this, "_interval", function () { // used often, so caching/observing instead of fetching on-demand this._prefBranch.addObserver("sessionstore.interval", this, true); return this._prefBranch.getIntPref("sessionstore.interval"); }); @@ -561,53 +572,29 @@ let SessionStoreInternal = { }); XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () { this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true); return this._prefBranch.getIntPref("sessionstore.max_windows_undo"); }); }, - _initWindow: function ssi_initWindow(aWindow) { - if (aWindow) { - this.onLoad(aWindow); - } else if (this._loadState == STATE_STOPPED) { - // If init is being called with a null window, it's possible that we - // just want to tell sessionstore that a session is live (as is the case - // with starting Firefox with -private, for example; see bug 568816), - // so we should mark the load state as running to make sure that - // things like setBrowserState calls will succeed in restoring the session. - this._loadState = STATE_RUNNING; - } - }, - - /** - * Start tracking a window. - * - * This function also initializes the component if it is not - * initialized yet. - */ - init: function ssi_init(aWindow) { - let self = this; - this.initService(); - this._promiseInitialization.promise.then( - function onSuccess() { - self._initWindow(aWindow); - } - ); - }, - /** * Called on application shutdown, after notifications: * quit-application-granted, quit-application */ _uninit: function ssi_uninit() { + if (!this._initialized) { + throw new Error("SessionStore is not initialized."); + } + // save all data for session resuming - if (this._sessionInitialized) + if (this._sessionInitialized) { this.saveState(true); + } // clear out priority queue in case it's still holding refs TabRestoreQueue.reset(); // Make sure to break our cycle with the save timer if (this._saveTimer) { this._saveTimer.cancel(); this._saveTimer = null; @@ -665,16 +652,23 @@ let SessionStoreInternal = { switch (aMessage.name) { case "SessionStore:pageshow": this.onTabLoad(win, browser); break; case "SessionStore:input": this.onTabInput(win, browser); break; + case "SessionStore:MozStorageChanged": + TabStateCache.delete(browser); + this.saveStateDelayed(win); + break; + case "SessionStore:loadStart": + TabStateCache.delete(browser); + break; default: debug("received unknown message '" + aMessage.name + "'"); break; } this._clearRestoringWindows(); }, @@ -689,16 +683,17 @@ let SessionStoreInternal = { var win = aEvent.currentTarget.ownerDocument.defaultView; switch (aEvent.type) { case "load": // If __SS_restore_data is set, then we need to restore the document // (form data, scrolling, etc.). This will only happen when a tab is // first restored. let browser = aEvent.currentTarget; + TabStateCache.delete(browser); if (browser.__SS_restore_data) this.restoreDocument(win, browser, aEvent); this.onTabLoad(win, browser); break; case "TabOpen": this.onTabAdd(win, aEvent.originalTarget); break; case "TabClose": @@ -712,21 +707,26 @@ let SessionStoreInternal = { break; case "TabShow": this.onTabShow(win, aEvent.originalTarget); break; case "TabHide": this.onTabHide(win, aEvent.originalTarget); break; case "TabPinned": - case "TabUnpinned": + // If possible, update cached data without having to invalidate it + TabStateCache.update(aEvent.originalTarget, "pinned", true); this.saveStateDelayed(win); break; - } - + case "TabUnpinned": + // If possible, update cached data without having to invalidate it + TabStateCache.update(aEvent.originalTarget, "pinned", false); + this.saveStateDelayed(win); + break; + } this._clearRestoringWindows(); }, /** * If it's the first window load since app start... * - determine if we're reloading after a crash or a forced-restart * - restore window state * - restart downloads @@ -1086,16 +1086,17 @@ let SessionStoreInternal = { // session data on disk as this notification fires after the // quit-application notification so the browser is about to exit. if (this._loadState == STATE_QUITTING) return; this._lastSessionState = null; let openWindows = {}; this._forEachBrowserWindow(function(aWindow) { Array.forEach(aWindow.gBrowser.tabs, function(aTab) { + TabStateCache.delete(aTab); delete aTab.linkedBrowser.__SS_data; delete aTab.linkedBrowser.__SS_tabStillLoading; delete aTab.linkedBrowser.__SS_formDataSaved; delete aTab.linkedBrowser.__SS_hostSchemeData; if (aTab.linkedBrowser.__SS_restoreState) this._resetTabRestoringState(aTab); }); openWindows[aWindow.__SSi] = true; @@ -1309,19 +1310,18 @@ let SessionStoreInternal = { event.initEvent("SSTabClosing", true, false); aTab.dispatchEvent(event); // don't update our internal state if we don't have to if (this._max_tabs_undo == 0) { return; } - // make sure that the tab related data is up-to-date - var tabState = this._collectTabData(aTab); - this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState); + // Get the latest data for this tab (generally, from the cache) + let tabState = this._collectTabData(aTab); // store closed-tab data for undo if (this._shouldSaveTabState(tabState)) { let tabTitle = aTab.label; let tabbrowser = aWindow.gBrowser; tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab); this._windows[aWindow.__SSi]._closedTabs.unshift({ @@ -1332,32 +1332,35 @@ let SessionStoreInternal = { }); var length = this._windows[aWindow.__SSi]._closedTabs.length; if (length > this._max_tabs_undo) this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo); } }, /** - * When a tab loads, save state. + * When a tab loads, invalidate its cached state, trigger async save. + * * @param aWindow * Window reference * @param aBrowser * Browser reference */ onTabLoad: function ssi_onTabLoad(aWindow, aBrowser) { // react on "load" and solitary "pageshow" events (the first "pageshow" // following "load" is too late for deleting the data caches) // It's possible to get a load event after calling stop on a browser (when // overwriting tabs). We want to return early if the tab hasn't been restored yet. if (aBrowser.__SS_restoreState && aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { return; } + TabStateCache.delete(aBrowser); + delete aBrowser.__SS_data; delete aBrowser.__SS_tabStillLoading; delete aBrowser.__SS_formDataSaved; this.saveStateDelayed(aWindow); // attempt to update the current URL we send in a crash report this._updateCrashReportURL(aWindow); }, @@ -1368,16 +1371,18 @@ let SessionStoreInternal = { * Window reference * @param aBrowser * Browser reference */ onTabInput: function ssi_onTabInput(aWindow, aBrowser) { // deleting __SS_formDataSaved will cause us to recollect form data delete aBrowser.__SS_formDataSaved; + TabStateCache.delete(aBrowser); + this.saveStateDelayed(aWindow, 3000); }, /** * When a tab is selected, save session data * @param aWindow * Window reference */ @@ -1404,28 +1409,34 @@ let SessionStoreInternal = { aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { TabRestoreQueue.hiddenToVisible(aTab); // let's kick off tab restoration again to ensure this tab gets restored // with "restore_hidden_tabs" == false (now that it has become visible) this.restoreNextTab(); } + // If possible, update cached data without having to invalidate it + TabStateCache.update(aTab, "hidden", false); + // Default delay of 2 seconds gives enough time to catch multiple TabShow // events due to changing groups in Panorama. this.saveStateDelayed(aWindow); }, onTabHide: function ssi_onTabHide(aWindow, aTab) { // If the tab hasn't been restored yet, move it into the right bucket if (aTab.linkedBrowser.__SS_restoreState && aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) { TabRestoreQueue.visibleToHidden(aTab); } + // If possible, update cached data without having to invalidate it + TabStateCache.update(aTab, "hidden", true); + // Default delay of 2 seconds gives enough time to catch multiple TabHide // events due to changing groups in Panorama. this.saveStateDelayed(aWindow); }, /* ........ nsISessionStore API .............. */ getBrowserState: function ssi_getBrowserState() { @@ -1491,42 +1502,63 @@ let SessionStoreInternal = { this.restoreWindow(aWindow, aState, aOverwrite); }, getTabState: function ssi_getTabState(aTab) { if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); - var tabState = this._collectTabData(aTab); - - var window = aTab.ownerDocument.defaultView; - this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState); + let tabState = this._collectTabData(aTab); return this._toJSONString(tabState); }, setTabState: function ssi_setTabState(aTab, aState) { - var tabState = JSON.parse(aState); - if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi) + // Remove the tab state from the cache. + // Note that we cannot simply replace the contents of the cache + // as |aState| can be an incomplete state that will be completed + // by |restoreHistoryPrecursor|. + let tabState = JSON.parse(aState); + if (!tabState) { + debug("Empty state argument"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + if (typeof tabState != "object") { + debug("State argument does not represent an object"); throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); - - var window = aTab.ownerDocument.defaultView; + } + if (!("entries" in tabState)) { + debug("State argument must contain field 'entries'"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + if (!aTab.ownerDocument) { + debug("Tab argument must have an owner document"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + + let window = aTab.ownerDocument.defaultView; + if (!("__SSi" in window)) { + debug("Default view of ownerDocument must have a unique identifier"); + throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); + } + + TabStateCache.delete(aTab); this._setWindowStateBusy(window); this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0); }, duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta) { if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi || !aWindow.getBrowser) throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); - var tabState = this._collectTabData(aTab, true); - var sourceWindow = aTab.ownerDocument.defaultView; - this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true); + // Duplicate the tab state + let tabState = this._cloneFullTabData(aTab); + tabState.index += aDelta; tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length)); tabState.pinned = false; this._setWindowStateBusy(aWindow); let newTab = aTab == aWindow.gBrowser.selectedTab ? aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) : aWindow.gBrowser.addTab(); @@ -1686,31 +1718,33 @@ let SessionStoreInternal = { throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG); } }, deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) { if (aWindow.__SSi && this._windows[aWindow.__SSi].extData && this._windows[aWindow.__SSi].extData[aKey]) delete this._windows[aWindow.__SSi].extData[aKey]; + this.saveStateDelayed(aWindow); }, getTabValue: function ssi_getTabValue(aTab, aKey) { let data = {}; if (aTab.__SS_extdata) { data = aTab.__SS_extdata; } else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { // If the tab hasn't been fully restored, get the data from the to-be-restored data data = aTab.linkedBrowser.__SS_data.extData; } return data[aKey] || ""; }, setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) { + TabStateCache.delete(aTab); // If the tab hasn't been restored, then set the data there, otherwise we // could lose newly added data. let saveTo; if (aTab.__SS_extdata) { saveTo = aTab.__SS_extdata; } else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { saveTo = aTab.linkedBrowser.__SS_data.extData; @@ -1719,33 +1753,36 @@ let SessionStoreInternal = { aTab.__SS_extdata = {}; saveTo = aTab.__SS_extdata; } saveTo[aKey] = aStringValue; this.saveStateDelayed(aTab.ownerDocument.defaultView); }, deleteTabValue: function ssi_deleteTabValue(aTab, aKey) { + TabStateCache.delete(aTab); // We want to make sure that if data is accessed early, we attempt to delete // that data from __SS_data as well. Otherwise we'll throw in cases where // data can be set or read. let deleteFrom; if (aTab.__SS_extdata) { deleteFrom = aTab.__SS_extdata; } else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) { deleteFrom = aTab.linkedBrowser.__SS_data.extData; } if (deleteFrom && deleteFrom[aKey]) delete deleteFrom[aKey]; + this.saveStateDelayed(aTab.ownerDocument.defaultView); }, persistTabAttribute: function ssi_persistTabAttribute(aName) { if (TabAttributes.persist(aName)) { + TabStateCache.clear(); this.saveStateDelayed(); } }, /** * Restores the session state stored in _lastSessionState. This will attempt * to merge data into the current session. If a window was opened at startup * with pinned tab(s), then the remaining data from the previous session for @@ -1909,47 +1946,66 @@ let SessionStoreInternal = { } return [true, canOverwriteTabs]; }, /* ........ Saving Functionality .............. */ /** - * Store all session data for a window - * @param aWindow - * Window reference + * Collect data related to a single tab + * + * @param aTab + * tabbrowser tab + * + * @returns {TabData} An object with the data for this tab. If the + * tab has not been invalidated since the last call to + * _collectTabData(aTab), the same object is returned. */ - _saveWindowHistory: function ssi_saveWindowHistory(aWindow) { - var tabbrowser = aWindow.gBrowser; - var tabs = tabbrowser.tabs; - var tabsData = this._windows[aWindow.__SSi].tabs = []; - - for (var i = 0; i < tabs.length; i++) - tabsData.push(this._collectTabData(tabs[i])); - - this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1; + _collectTabData: function ssi_collectTabData(aTab) { + if (!aTab) { + throw new TypeError("Expecting a tab"); + } + let tabData; + if ((tabData = TabStateCache.get(aTab))) { + return tabData; + } + tabData = new TabData(this._collectBaseTabData(aTab)); + if (this._updateTextAndScrollDataForTab(aTab, tabData)) { + TabStateCache.set(aTab, tabData); + } + return tabData; }, /** - * Collect data related to a single tab + * Collect data related to a single tab, including private data. + * Use with caution. + * * @param aTab * tabbrowser tab - * @param aFullData - * always return privacy sensitive data (use with care) - * @returns object + * + * @returns {object} An object with the data for this tab. This object + * is recomputed at every call. */ - _collectTabData: function ssi_collectTabData(aTab, aFullData) { - var tabData = { entries: [], lastAccessed: aTab.lastAccessed }; - var browser = aTab.linkedBrowser; - - if (!browser || !browser.currentURI) + _cloneFullTabData: function ssi_cloneFullTabData(aTab) { + let options = { includePrivateData: true }; + let tabData = this._collectBaseTabData(aTab, options); + this._updateTextAndScrollDataForTab(aTab, tabData, options); + return tabData; + }, + + _collectBaseTabData: function ssi_collectBaseTabData(aTab, aOptions = null) { + let includePrivateData = aOptions && aOptions.includePrivateData; + let tabData = {entries: [], lastAccessed: aTab.lastAccessed }; + let browser = aTab.linkedBrowser; + if (!browser || !browser.currentURI) { // can happen when calling this function right after .addTab() return tabData; - else if (browser.__SS_data && browser.__SS_tabStillLoading) { + } + if (browser.__SS_data && browser.__SS_tabStillLoading) { // use the data to be restored when the tab hasn't been completely loaded tabData = browser.__SS_data; if (aTab.pinned) tabData.pinned = true; else delete tabData.pinned; tabData.hidden = aTab.hidden; @@ -1969,26 +2025,26 @@ let SessionStoreInternal = { } catch (ex) { } // this could happen if we catch a tab during (de)initialization // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse // data even when we shouldn't (e.g. Back, different anchor) if (history && browser.__SS_data && browser.__SS_data.entries[history.index] && browser.__SS_data.entries[history.index].url == browser.currentURI.spec && - history.index < this._sessionhistory_max_entries - 1 && !aFullData) { + history.index < this._sessionhistory_max_entries - 1 && !includePrivateData) { tabData = browser.__SS_data; tabData.index = history.index + 1; } else if (history && history.count > 0) { browser.__SS_hostSchemeData = []; try { for (var j = 0; j < history.count; j++) { let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false), - aFullData, aTab.pinned, browser.__SS_hostSchemeData); + includePrivateData, aTab.pinned, browser.__SS_hostSchemeData); tabData.entries.push(entry); } // If we make it through the for loop, then we're ok and we should clear // any indicator of brokenness. delete aTab.__SS_broken_history; } catch (ex) { // In some cases, getEntryAtIndex will throw. This seems to be due to @@ -2004,17 +2060,17 @@ let SessionStoreInternal = { NS_ASSERT(false, "SessionStore failed gathering complete history " + "for the focused window/tab. See bug 669196."); aTab.__SS_broken_history = true; } } tabData.index = history.index + 1; // make sure not to cache privacy sensitive data which shouldn't get out - if (!aFullData) + if (!includePrivateData) browser.__SS_data = tabData; } else if (browser.currentURI.spec != "about:blank" || browser.contentDocument.body.hasChildNodes()) { tabData.entries[0] = { url: browser.currentURI.spec }; tabData.index = 1; } @@ -2053,39 +2109,39 @@ let SessionStoreInternal = { tabData.image = tabbrowser.getIcon(aTab); if (aTab.__SS_extdata) tabData.extData = aTab.__SS_extdata; else if (tabData.extData) delete tabData.extData; if (history && browser.docShell instanceof Ci.nsIDocShell) { - let storageData = SessionStorage.serialize(browser.docShell, aFullData) + let storageData = SessionStorage.serialize(browser.docShell, includePrivateData) if (Object.keys(storageData).length) tabData.storage = storageData; } return tabData; }, /** * Get an object that is a serialized representation of a History entry * Used for data storage * @param aEntry * nsISHEntry instance - * @param aFullData + * @param aIncludePrivateData * always return privacy sensitive data (use with care) * @param aIsPinned * the tab is pinned and should be treated differently for privacy * @param aHostSchemeData * an array of objects with host & scheme keys * @returns object */ _serializeHistoryEntry: - function ssi_serializeHistoryEntry(aEntry, aFullData, aIsPinned, aHostSchemeData) { + function ssi_serializeHistoryEntry(aEntry, aIncludePrivateData, aIsPinned, aHostSchemeData) { var entry = { url: aEntry.URI.spec }; try { // throwing is expensive, we know that about: pages will throw if (entry.url.indexOf("about:") != 0) aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme }); } catch (ex) { @@ -2126,26 +2182,26 @@ let SessionStoreInternal = { var x = {}, y = {}; aEntry.getScrollPosition(x, y); if (x.value != 0 || y.value != 0) entry.scroll = x.value + "," + y.value; try { var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata"); - if (aEntry.postData && (aFullData || prefPostdata && + if (aEntry.postData && (aIncludePrivateData || prefPostdata && this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) { aEntry.postData.QueryInterface(Ci.nsISeekableStream). seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); var stream = Cc["@mozilla.org/binaryinputstream;1"]. createInstance(Ci.nsIBinaryInputStream); stream.setInputStream(aEntry.postData); var postBytes = stream.readByteArray(stream.available()); var postdata = String.fromCharCode.apply(null, postBytes); - if (aFullData || prefPostdata == -1 || + if (aIncludePrivateData || prefPostdata == -1 || postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <= prefPostdata) { // We can stop doing base64 encoding once our serialization into JSON // is guaranteed to handle all chars in strings, including embedded // nulls. entry.postdata_b64 = btoa(postdata); } } @@ -2196,119 +2252,113 @@ let SessionStoreInternal = { if (child) { // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595) if (child.URI.schemeIs("wyciwyg")) { children = []; break; } - children.push(this._serializeHistoryEntry(child, aFullData, + children.push(this._serializeHistoryEntry(child, aIncludePrivateData, aIsPinned, aHostSchemeData)); } } if (children.length) entry.children = children; } return entry; }, /** - * go through all tabs and store the current scroll positions + * Go through all frames and store the current scroll positions * and innerHTML content of WYSIWYG editors - * @param aWindow - * Window reference - */ - _updateTextAndScrollData: function ssi_updateTextAndScrollData(aWindow) { - var browsers = aWindow.gBrowser.browsers; - this._windows[aWindow.__SSi].tabs.forEach(function (tabData, i) { - try { - this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData); - } - catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time) - }, this); - }, - - /** - * go through all frames and store the current scroll positions - * and innerHTML content of WYSIWYG editors - * @param aWindow - * Window reference - * @param aBrowser - * single browser reference + * + * @param aTab + * tabbrowser tab * @param aTabData * tabData object to add the information to - * @param aFullData - * always return privacy sensitive data (use with care) + * @param options + * An optional object that may contain the following field: + * - includePrivateData: always return privacy sensitive data + * (use with care) + * @return false if data should not be cached because the tab + * has not been fully initialized yet. */ _updateTextAndScrollDataForTab: - function ssi_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) { + function ssi_updateTextAndScrollDataForTab(aTab, aTabData, aOptions = null) { + let includePrivateData = aOptions && aOptions.includePrivateData; + let window = aTab.ownerDocument.defaultView; + let browser = aTab.linkedBrowser; // we shouldn't update data for incompletely initialized tabs - if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading) - return; - - var tabIndex = (aTabData.index || aTabData.entries.length) - 1; + if (!browser.currentURI + || (browser.__SS_data && browser.__SS_tabStillLoading)) { + return false; + } + + let tabIndex = (aTabData.index || aTabData.entries.length) - 1; // entry data needn't exist for tabs just initialized with an incomplete session state - if (!aTabData.entries[tabIndex]) - return; - - let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : - this._getSelectedPageStyle(aBrowser.contentWindow); + if (!aTabData.entries[tabIndex]) { + return false; + } + + let selectedPageStyle = browser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" : + this._getSelectedPageStyle(browser.contentWindow); if (selectedPageStyle) aTabData.pageStyle = selectedPageStyle; else if (aTabData.pageStyle) delete aTabData.pageStyle; - this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow, + this._updateTextAndScrollDataForFrame(window, browser.contentWindow, aTabData.entries[tabIndex], - !aBrowser.__SS_formDataSaved, aFullData, + !browser.__SS_formDataSaved, includePrivateData, !!aTabData.pinned); - aBrowser.__SS_formDataSaved = true; - if (aBrowser.currentURI.spec == "about:config") + browser.__SS_formDataSaved = true; + if (browser.currentURI.spec == "about:config") aTabData.entries[tabIndex].formdata = { id: { - "textbox": aBrowser.contentDocument.getElementById("textbox").value + "textbox": browser.contentDocument.getElementById("textbox").value }, xpath: {} }; + return true; }, /** * go through all subframes and store all form data, the current * scroll positions and innerHTML content of WYSIWYG editors * @param aWindow * Window reference * @param aContent * frame reference * @param aData * part of a tabData object to add the information to * @param aUpdateFormData * update all form data for this tab - * @param aFullData + * @param aIncludePrivateData * always return privacy sensitive data (use with care) * @param aIsPinned * the tab is pinned and should be treated differently for privacy */ _updateTextAndScrollDataForFrame: function ssi_updateTextAndScrollDataForFrame(aWindow, aContent, aData, - aUpdateFormData, aFullData, aIsPinned) { + aUpdateFormData, aIncludePrivateData, aIsPinned) { for (var i = 0; i < aContent.frames.length; i++) { if (aData.children && aData.children[i]) this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i], aData.children[i], aUpdateFormData, - aFullData, aIsPinned); + aIncludePrivateData, aIsPinned); } var isHTTPS = this._getURIFromString((aContent.parent || aContent). document.location.href).schemeIs("https"); let topURL = aContent.top.document.location.href; let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback"; - if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { - if (aFullData || aUpdateFormData) { + if (aIncludePrivateData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) { + if (aIncludePrivateData || aUpdateFormData) { let formData = DocumentUtils.getFormData(aContent.document); // We want to avoid saving data for about:sessionrestore as a string. // Since it's stored in the form as stringified JSON, stringifying further // causes an explosion of escape characters. cf. bug 467409 if (formData && isAboutSR) { formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]); } @@ -2422,38 +2472,16 @@ let SessionStoreInternal = { aHosts[aHost] = aIsPinned; } else if (aScheme == "file") { aHosts[aHost] = true; } }, /** - * store all hosts for a URL - * @param aWindow - * Window reference - */ - _updateCookieHosts: function ssi_updateCookieHosts(aWindow) { - var hosts = this._internalWindows[aWindow.__SSi].hosts = {}; - - // Since _updateCookiesHosts is only ever called for open windows during a - // session, we can call into _extractHostsForCookiesFromHostScheme directly - // using data that is attached to each browser. - for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) { - let tab = aWindow.gBrowser.tabs[i]; - let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || []; - for (let j = 0; j < hostSchemeData.length; j++) { - this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host, - hostSchemeData[j].scheme, - hosts, true, tab.pinned); - } - } - }, - - /** * Serialize cookie data * @param aWindows * JS object containing window data references * { id: winData, etc. } */ _updateCookies: function ssi_updateCookies(aWindows) { function addCookieToHash(aHash, aHost, aPath, aName, aCookie) { // lazily build up a 3-dimensional hash, with @@ -2680,20 +2708,39 @@ let SessionStoreInternal = { return { windows: [winData] }; }, _collectWindowData: function ssi_collectWindowData(aWindow) { if (!this._isWindowLoaded(aWindow)) return; + let tabbrowser = aWindow.gBrowser; + let tabs = tabbrowser.tabs; + let winData = this._windows[aWindow.__SSi]; + let tabsData = winData.tabs = []; + let hosts = this._internalWindows[aWindow.__SSi].hosts = {}; + // update the internal state data for this window - this._saveWindowHistory(aWindow); - this._updateTextAndScrollData(aWindow); - this._updateCookieHosts(aWindow); + for (let tab of tabs) { + tabsData.push(this._collectTabData(tab)); + + // Since we are only ever called for open + // windows during a session, we can call into + // _extractHostsForCookiesFromHostScheme directly using data + // that is attached to each browser. + let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || []; + for (let j = 0; j < hostSchemeData.length; j++) { + this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host, + hostSchemeData[j].scheme, + hosts, true, tab.pinned); + } + } + winData.selected = tabbrowser.mTabBox.selectedIndex + 1; + this._updateWindowFeatures(aWindow); // Make sure we keep __SS_lastSessionWindowID around for cases like entering // or leaving PB mode. if (aWindow.__SS_lastSessionWindowID) this._windows[aWindow.__SSi].__lastSessionWindowID = aWindow.__SS_lastSessionWindowID; @@ -2817,20 +2864,23 @@ let SessionStoreInternal = { winData.tabs[0].hidden = false; tabbrowser.showTab(tabs[0]); } // If overwriting tabs, we want to reset each tab's "restoring" state. Since // we're overwriting those tabs, they should no longer be restoring. The // tabs will be rebuilt and marked if they need to be restored after loading // state (in restoreHistoryPrecursor). + // We also want to invalidate any cached information on the tab state. if (aOverwriteTabs) { for (let i = 0; i < tabbrowser.tabs.length; i++) { + let tab = tabbrowser.tabs[i]; + TabStateCache.delete(tab); if (tabbrowser.browsers[i].__SS_restoreState) - this._resetTabRestoringState(tabbrowser.tabs[i]); + this._resetTabRestoringState(tab); } } // We want to set up a counter on the window that indicates how many tabs // in this window are unrestored. This will be used in restoreNextTab to // determine if gRestoreTabsProgressListener should be removed from the window. // If we aren't overwriting existing tabs, then we want to add to the existing // count in case there are still tabs restoring. @@ -2982,32 +3032,33 @@ let SessionStoreInternal = { * Counter for number of times delaying b/c browser or history aren't ready * @param aRestoreImmediately * Flag to indicate whether the given set of tabs aTabs should be * restored/loaded immediately even if restore_on_demand = true */ restoreHistoryPrecursor: function ssi_restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount, aRestoreImmediately = false) { + var tabbrowser = aWindow.gBrowser; // make sure that all browsers and their histories are available // - if one's not, resume this check in 100ms (repeat at most 10 times) for (var t = aIx; t < aTabs.length; t++) { try { if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) { throw new Error(); } } catch (ex) { // in case browser or history aren't ready yet if (aCount < 10) { var restoreHistoryFunc = function(self) { self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount + 1, aRestoreImmediately); - } + }; aWindow.setTimeout(restoreHistoryFunc, 100, this); return; } } } if (!this._isWindowLoaded(aWindow)) { // from now on, the data will come from the actual window @@ -3133,17 +3184,16 @@ let SessionStoreInternal = { // At this point we're essentially ready for consumers to read/write data // via the sessionstore API so we'll send the SSWindowStateReady event. this._setWindowStateReady(aWindow); return; // no more tabs to restore } var tab = aTabs.shift(); var tabData = aTabData.shift(); - var browser = aWindow.gBrowser.getBrowserForTab(tab); var history = browser.webNavigation.sessionHistory; if (history.count > 0) { history.PurgeHistory(history.count); } history.QueryInterface(Ci.nsISHistoryInternal); @@ -3698,27 +3748,27 @@ let SessionStoreInternal = { /** * save state delayed by N ms * marks window as dirty (i.e. data update can't be skipped) * @param aWindow * Window reference * @param aDelay * Milliseconds to delay */ - saveStateDelayed: function ssi_saveStateDelayed(aWindow, aDelay) { + saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) { if (aWindow) { this._dirtyWindows[aWindow.__SSi] = true; } if (!this._saveTimer) { // interval until the next disk operation is allowed var minimalDelay = this._lastSaveTime + this._interval - Date.now(); // if we have to wait, set a timer, otherwise saveState directly - aDelay = Math.max(minimalDelay, aDelay || 2000); + aDelay = Math.max(minimalDelay, aDelay); if (aDelay > 0) { this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT); } else { this.saveState(); } } @@ -3824,35 +3874,19 @@ let SessionStoreInternal = { Services.obs.notifyObservers(stateString, "sessionstore-state-write", ""); data = stateString.data; // Don't touch the file if an observer has deleted all state data. if (!data) { return; } - let promise; - // If "sessionstore.resume_from_crash" is true, attempt to backup the - // session file first, before writing to it. - if (this._resume_from_crash) { - // Note that we do not have race conditions here as _SessionFile - // guarantees that any I/O operation is completed before proceeding to - // the next I/O operation. - // Note backup happens only once, on initial save. - promise = this._backupSessionFileOnce; - } else { - promise = Promise.resolve(); - } - - // Attempt to write to the session file (potentially, depending on - // "sessionstore.resume_from_crash" preference, after successful backup). - promise = promise.then(function onSuccess() { - // Write (atomically) to a session file, using a tmp file. - return _SessionFile.write(data); - }); + // Write (atomically) to a session file, using a tmp file. + let promise = + _SessionFile.write(data, {backupOnFirstWrite: this._resume_from_crash}); // Once the session file is successfully updated, save the time stamp of the // last save and notify the observers. promise = promise.then(() => { this._lastSaveTime = Date.now(); Services.obs.notifyObservers(null, "sessionstore-state-write-complete", ""); }); @@ -3980,16 +4014,17 @@ let SessionStoreInternal = { */ _getTabForBrowser: function ssi_getTabForBrowser(aBrowser) { let window = aBrowser.ownerDocument.defaultView; for (let i = 0; i < window.gBrowser.tabs.length; i++) { let tab = window.gBrowser.tabs[i]; if (tab.linkedBrowser == aBrowser) return tab; } + return undefined; }, /** * Whether or not to resume session, if not recovering from a crash. * @returns bool */ _doResumeSession: function ssi_doResumeSession() { return this._prefBranch.getIntPref("startup.page") == 3 || @@ -4802,21 +4837,35 @@ function SessionStoreSHistoryListener(aT this.tab = aTab; } SessionStoreSHistoryListener.prototype = { QueryInterface: XPCOMUtils.generateQI([ Ci.nsISHistoryListener, Ci.nsISupportsWeakReference ]), browser: null, - OnHistoryNewEntry: function(aNewURI) { }, - OnHistoryGoBack: function(aBackURI) { return true; }, - OnHistoryGoForward: function(aForwardURI) { return true; }, - OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; }, - OnHistoryPurge: function(aNumEntries) { return true; }, +// The following events (with the exception of OnHistoryPurge) +// accompany either a "load" or a "pageshow" which will in turn cause +// invalidations. + OnHistoryNewEntry: function(aNewURI) { + + }, + OnHistoryGoBack: function(aBackURI) { + return true; + }, + OnHistoryGoForward: function(aForwardURI) { + return true; + }, + OnHistoryGotoIndex: function(aIndex, aGotoURI) { + return true; + }, + OnHistoryPurge: function(aNumEntries) { + TabStateCache.delete(this.tab); + return true; + }, OnHistoryReload: function(aReloadURI, aReloadFlags) { // On reload, we want to make sure that session history loads the right // URI. In order to do that, we will juet call restoreTab. That will remove // the history listener and load the right URI. SessionStoreInternal.restoreTab(this.tab); // Returning false will stop the load that docshell is attempting. return false; } @@ -4829,9 +4878,111 @@ String.prototype.hasRootDomain = functio return false; if (this == aDomain) return true; let prevChar = this[index - 1]; return (index == (this.length - aDomain.length)) && (prevChar == "." || prevChar == "/"); +}; + +function TabData(obj = null) { + if (obj) { + if (obj instanceof TabData) { + // FIXME: Can we get rid of this? + return obj; + } + for (let [key, value] in Iterator(obj)) { + this[key] = value; + } + } + return this; } + +/** + * A cache for tabs data. + * + * This cache implements a weak map from tabs (as XUL elements) + * to tab data (as instances of TabData). + * + * Note that we should never cache private data, as: + * - that data is used very seldom by SessionStore; + * - caching private data in addition to public data is memory consuming. + */ +let TabStateCache = { + _data: new WeakMap(), + + /** + * Add or replace an entry in the cache. + * + * @param {XULElement} aTab The key, which may be either a tab + * or the corresponding browser. The binding will disappear + * if the tab/browser is destroyed. + * @param {TabData} aValue The data associated to |aTab|. + */ + set: function(aTab, aValue) { + let key = this._normalizeToBrowser(aTab); + if (!(aValue instanceof TabData)) { + throw new TypeError("Attempting to cache a non TabData"); + } + this._data.set(key, aValue); + }, + + /** + * Return the tab data associated with a tab. + * + * @param {XULElement} aKey The tab or the associated browser. + * + * @return {TabData|undefined} The data if available, |undefined| + * otherwise. + */ + get: function(aKey) { + let key = this._normalizeToBrowser(aKey); + return this._data.get(key); + }, + + /** + * Delete the tab data associated with a tab. + * + * @param {XULElement} aKey The tab or the associated browser. + * + * Noop of there is no tab data associated with the tab. + */ + delete: function(aKey) { + let key = this._normalizeToBrowser(aKey); + this._data.delete(key); + }, + + /** + * Delete all tab data. + */ + clear: function() { + this._data.clear(); + }, + + /** + * Update in place a piece of data. + * + * @param {XULElement} aKey The tab or the associated browser. + * If the tab/browser is not present, do nothing. + * @param {string} aField The field to update. + * @param {*} aValue The new value to place in the field. + */ + update: function(aKey, aField, aValue) { + let key = this._normalizeToBrowser(aKey); + let data = this._data.get(key); + if (data) { + data[aField] = aValue; + } + }, + + _normalizeToBrowser: function(aKey) { + let nodeName = aKey.localName; + if (nodeName == "tab") { + return aKey.linkedBrowser; + } + if (nodeName == "browser") { + return aKey; + } + throw new TypeError("Key is neither a tab nor a browser: " + nodeName); + } +};
--- a/browser/components/sessionstore/src/SessionWorker.js +++ b/browser/components/sessionstore/src/SessionWorker.js @@ -52,16 +52,22 @@ self.onmessage = function (msg) { let Agent = { // The initial session string as read from disk. initialState: null, // Boolean that tells whether we already wrote // the loadState to disk once after startup. hasWrittenLoadStateOnce: false, + // Boolean that tells whether we already made a + // call to write(). We will only attempt to move + // sessionstore.js to sessionstore.bak on the + // first write. + hasWrittenState: false, + // The path to sessionstore.js path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), // The path to sessionstore.bak backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), /** * This method is only intended to be called by _SessionFile.syncRead() and @@ -102,17 +108,29 @@ let Agent = { // No sessionstore data files found. Return an empty string. return ""; }, /** * Write the session to disk. */ - write: function (stateString) { + write: function (stateString, options) { + if (!this.hasWrittenState) { + if (options && options.backupOnFirstWrite) { + try { + File.move(this.path, this.backupPath); + } catch (ex if isNoSuchFileEx(ex)) { + // Ignore exceptions about non-existent files. + } + } + + this.hasWrittenState = true; + } + let bytes = Encoder.encode(stateString); return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"}); }, /** * Writes the session state to disk again but changes session.state to * 'running' before doing so. This is intended to be called only once, shortly * after startup so that we detect crashes on startup correctly. @@ -135,29 +153,18 @@ let Agent = { try { state = JSON.parse(this.initialState); } finally { this.initialState = null; } state.session = state.session || {}; state.session.state = loadState; - return this.write(JSON.stringify(state)); - }, - - /** - * Moves sessionstore.js to sessionstore.bak. - */ - moveToBackupPath: function () { - try { - return File.move(this.path, this.backupPath); - } catch (ex if isNoSuchFileEx(ex)) { - // Ignore exceptions about non-existent files. - return true; - } + let bytes = Encoder.encode(JSON.stringify(state)); + return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"}); }, /** * Creates a copy of sessionstore.js. */ createBackupCopy: function (ext) { try { return File.copy(this.path, this.backupPath + ext);
--- a/browser/components/sessionstore/src/_SessionFile.jsm +++ b/browser/components/sessionstore/src/_SessionFile.jsm @@ -28,17 +28,17 @@ this.EXPORTED_SYMBOLS = ["_SessionFile"] const Cu = Components.utils; const Cc = Components.classes; const Ci = Components.interfaces; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); -Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Task", @@ -62,34 +62,28 @@ this._SessionFile = { Deprecated.warning( "syncRead is deprecated and will be removed in a future version", "https://bugzilla.mozilla.org/show_bug.cgi?id=532150") return SessionFileInternal.syncRead(); }, /** * Write the contents of the session file, asynchronously. */ - write: function (aData) { - return SessionFileInternal.write(aData); + write: function (aData, aOptions = {}) { + return SessionFileInternal.write(aData, aOptions); }, /** * Writes the initial state to disk again only to change the session's load * state. This must only be called once, it will throw an error otherwise. */ writeLoadStateOnceAfterStartup: function (aLoadState) { return SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState); }, /** * Create a backup copy, asynchronously. - */ - moveToBackupPath: function () { - return SessionFileInternal.moveToBackupPath(); - }, - /** - * Create a backup copy, asynchronously. * This is designed to perform backup on upgrade. */ createBackupCopy: function (ext) { return SessionFileInternal.createBackupCopy(ext); }, /** * Remove a backup copy, asynchronously. * This is designed to clean up a backup on upgrade. @@ -207,24 +201,24 @@ let SessionFileInternal = { SessionWorker.post("setInitialState", [text]); return text; }, read: function () { return SessionWorker.post("read").then(msg => msg.ok); }, - write: function (aData) { + write: function (aData, aOptions) { let refObj = {}; return TaskUtils.spawn(function task() { TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj); TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); try { - let promise = SessionWorker.post("write", [aData]); + let promise = SessionWorker.post("write", [aData, aOptions]); // At this point, we measure how long we stop the main thread TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); // Now wait for the result and measure how long we had to wait for the result yield promise; TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj); } catch (ex) { TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); @@ -234,20 +228,16 @@ let SessionFileInternal = { } }.bind(this)); }, writeLoadStateOnceAfterStartup: function (aLoadState) { return SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]); }, - moveToBackupPath: function () { - return SessionWorker.post("moveToBackupPath"); - }, - createBackupCopy: function (ext) { return SessionWorker.post("createBackupCopy", [ext]); }, removeBackupCopy: function (ext) { return SessionWorker.post("removeBackupCopy", [ext]); },
--- a/browser/components/sessionstore/src/nsSessionStartup.js +++ b/browser/components/sessionstore/src/nsSessionStartup.js @@ -34,17 +34,17 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); -Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +Cu.import("resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile", "resource:///modules/sessionstore/_SessionFile.jsm"); const STATE_RUNNING_STR = "running"; function debug(aMsg) { aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n"); @@ -157,25 +157,16 @@ SessionStartup.prototype = { this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION; else if (!lastSessionCrashed && doResumeSession) this._sessionType = Ci.nsISessionStartup.RESUME_SESSION; else if (this._initialState) this._sessionType = Ci.nsISessionStartup.DEFER_SESSION; else this._initialState = null; // reset the state - // wait for the first browser window to open - // Don't reset the initial window's default args (i.e. the home page(s)) - // if all stored tabs are pinned. - if (this.doRestore() && - (!this._initialState.windows || - !this._initialState.windows.every(function (win) - win.tabs.every(function (tab) tab.pinned)))) - Services.obs.addObserver(this, "domwindowopened", true); - Services.obs.addObserver(this, "sessionstore-windows-restored", true); if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) Services.obs.addObserver(this, "browser:purge-session-history", true); } finally { // We're ready. Notify everyone else. Services.obs.notifyObservers(null, "sessionstore-state-finalized", ""); @@ -199,99 +190,86 @@ SessionStartup.prototype = { break; case "quit-application": // no reason for initializing at this point (cf. bug 409115) Services.obs.removeObserver(this, "final-ui-startup"); Services.obs.removeObserver(this, "quit-application"); if (this._sessionType != Ci.nsISessionStartup.NO_SESSION) Services.obs.removeObserver(this, "browser:purge-session-history"); break; - case "domwindowopened": - var window = aSubject; - var self = this; - window.addEventListener("load", function() { - self._onWindowOpened(window); - window.removeEventListener("load", arguments.callee, false); - }, false); - break; case "sessionstore-windows-restored": Services.obs.removeObserver(this, "sessionstore-windows-restored"); // free _initialState after nsSessionStore is done with it this._initialState = null; break; case "browser:purge-session-history": Services.obs.removeObserver(this, "browser:purge-session-history"); // reset all state on sanitization this._sessionType = Ci.nsISessionStartup.NO_SESSION; break; } }, - /** - * Removes the default arguments from the first browser window - * (and removes the "domwindowopened" observer afterwards). - */ - _onWindowOpened: function sss_onWindowOpened(aWindow) { - var wType = aWindow.document.documentElement.getAttribute("windowtype"); - if (wType != "navigator:browser") - return; - - /** - * Note: this relies on the fact that nsBrowserContentHandler will return - * a different value the first time its getter is called after an update, - * due to its needHomePageOverride() logic. We don't want to remove the - * default arguments in the update case, since they include the "What's - * New" page. - * - * Since we're garanteed to be at least the second caller of defaultArgs - * (nsBrowserContentHandler calls it to determine which arguments to pass - * at startup), we know that if the window's arguments don't match the - * current defaultArguments, we're either in the update case, or we're - * launching a non-default browser window, so we shouldn't remove the - * window's arguments. - */ - var defaultArgs = Cc["@mozilla.org/browser/clh;1"]. - getService(Ci.nsIBrowserHandler).defaultArgs; - if (aWindow.arguments && aWindow.arguments[0] && - aWindow.arguments[0] == defaultArgs) - aWindow.arguments[0] = null; - - try { - Services.obs.removeObserver(this, "domwindowopened"); - } catch (e) { - // This might throw if we're removing the observer multiple times, - // but this is safe to ignore. - } - }, - /* ........ Public API ................*/ get onceInitialized() { return gOnceInitializedDeferred.promise; }, /** * Get the session state as a jsval */ get state() { this._ensureInitialized(); return this._initialState; }, /** - * Determine whether there is a pending session restore. + * Determines whether there is a pending session restore and makes sure that + * we're initialized before returning. If we're not yet this will read the + * session file synchronously. * @returns bool */ doRestore: function sss_doRestore() { this._ensureInitialized(); + return this._willRestore(); + }, + + /** + * Determines whether there is a pending session restore. + * @returns bool + */ + _willRestore: function () { return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION || this._sessionType == Ci.nsISessionStartup.RESUME_SESSION; }, /** + * Returns whether we will restore a session that ends up replacing the + * homepage. The browser uses this to not start loading the homepage if + * we're going to stop its load anyway shortly after. + * + * This is meant to be an optimization for the average case that loading the + * session file finishes before we may want to start loading the default + * homepage. Should this be called before the session file has been read it + * will just return false. + * + * @returns bool + */ + get willOverrideHomepage() { + if (this._initialState && this._willRestore()) { + let windows = this._initialState.windows || null; + // If there are valid windows with not only pinned tabs, signal that we + // will override the default homepage by restoring a session. + return windows && windows.some(w => w.tabs.some(t => !t.pinned)); + } + return false; + }, + + /** * Get the type of pending session store, if any. */ get sessionType() { this._ensureInitialized(); return this._sessionType; }, // Ensure that initialization is complete.
--- a/browser/components/sessionstore/test/Makefile.in +++ b/browser/components/sessionstore/test/Makefile.in @@ -22,16 +22,17 @@ MOCHITEST_BROWSER_FILES = \ browser_dying_cache.js \ browser_form_restore_events.js \ browser_form_restore_events_sample.html \ browser_formdata_format.js \ browser_formdata_format_sample.html \ browser_input.js \ browser_input_sample.html \ browser_pageshow.js \ + browser_sessionStorage.js \ browser_upgrade_backup.js \ browser_windowRestore_perwindowpb.js \ browser_248970_b_perwindowpb.js \ browser_248970_b_sample.html \ browser_339445.js \ browser_339445_sample.html \ browser_345898.js \ browser_346337.js \
--- a/browser/components/sessionstore/test/browser_625257.js +++ b/browser/components/sessionstore/test/browser_625257.js @@ -1,85 +1,86 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * 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/. */ +let Scope = {}; +Cu.import("resource://gre/modules/Task.jsm", Scope); +Cu.import("resource://gre/modules/Promise.jsm", Scope); +let {Task, Promise} = Scope; + + // This tests that a tab which is closed while loading is not lost. // Specifically, that session store does not rely on an invalid cache when // constructing data for a tab which is loading. -// The newly created tab which we load a URL into and try closing/undoing. -let tab; - // This test steps through the following parts: // 1. Tab has been created is loading URI_TO_LOAD. // 2. Before URI_TO_LOAD finishes loading, browser.currentURI has changed and // tab is scheduled to be removed. // 3. After the tab has been closed, undoCloseTab() has been called and the tab // should fully load. const URI_TO_LOAD = "about:mozilla"; +function waitForLoadStarted(aTab) { + let deferred = Promise.defer(); + waitForContentMessage(aTab.linkedBrowser, + "SessionStore:loadStart", + 1000, + deferred.resolve); + return deferred.promise; +} + +function waitForTabLoaded(aTab) { + let deferred = Promise.defer(); + whenBrowserLoaded(aTab.linkedBrowser, deferred.resolve); + return deferred.promise; +} + +function waitForTabClosed() { + let deferred = Promise.defer(); + let observer = function() { + gBrowser.tabContainer.removeEventListener("TabClose", observer, true); + deferred.resolve(); + }; + gBrowser.tabContainer.addEventListener("TabClose", observer, true); + return deferred.promise; +} + function test() { waitForExplicitFinish(); - gBrowser.addTabsProgressListener(tabsListener); - - tab = gBrowser.addTab(); - - tab.linkedBrowser.addEventListener("load", firstOnLoad, true); - - gBrowser.tabContainer.addEventListener("TabClose", onTabClose, true); -} - -function firstOnLoad(aEvent) { - tab.linkedBrowser.removeEventListener("load", firstOnLoad, true); + Task.spawn(function() { + try { + // Open a new tab + let tab = gBrowser.addTab("about:blank"); + yield waitForTabLoaded(tab); - let uri = aEvent.target.location; - is(uri, "about:blank", "first load should be for about:blank"); - - // Trigger a save state. - ss.getBrowserState(); + // Trigger a save state, to initialize any caches + ss.getBrowserState(); - is(gBrowser.tabs[1], tab, "newly created tab should exist by now"); - ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state"); + is(gBrowser.tabs[1], tab, "newly created tab should exist by now"); + ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state"); - tab.linkedBrowser.loadURI(URI_TO_LOAD); -} + // Start a load and interrupt it by closing the tab + tab.linkedBrowser.loadURI(URI_TO_LOAD); + let loaded = yield waitForLoadStarted(tab); + ok(loaded, "Load started"); -let tabsListener = { - onLocationChange: function onLocationChange(aBrowser) { - gBrowser.removeTabsProgressListener(tabsListener); - - is(aBrowser.currentURI.spec, URI_TO_LOAD, - "should occur after about:blank load and be loading next page"); - - // Since we are running in the context of tabs listeners, we do not - // want to disrupt other tabs listeners. - executeSoon(function() { + let tabClosing = waitForTabClosed(); gBrowser.removeTab(tab); - }); - } -}; + info("Now waiting for TabClose to close"); + yield tabClosing; -function onTabClose(aEvent) { - gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, true); + // Undo the tab, ensure that it proceeds with loading + tab = ss.undoCloseTab(window, 0); + yield waitForTabLoaded(tab); + is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD, "loading proceeded as expected"); - is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD, - "should only remove when loading page"); + gBrowser.removeTab(tab); - executeSoon(function() { - tab = ss.undoCloseTab(window, 0); - tab.linkedBrowser.addEventListener("load", secondOnLoad, true); + executeSoon(finish); + } catch (ex) { + ok(false, ex); + info(ex.stack); + } }); } - -function secondOnLoad(aEvent) { - let uri = aEvent.target.location; - is(uri, URI_TO_LOAD, "should load page from undoCloseTab"); - done(); -} - -function done() { - tab.linkedBrowser.removeEventListener("load", secondOnLoad, true); - gBrowser.removeTab(tab); - - executeSoon(finish); -}
--- a/browser/components/sessionstore/test/browser_833286_atomic_backup.js +++ b/browser/components/sessionstore/test/browser_833286_atomic_backup.js @@ -1,12 +1,14 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // This tests are for a sessionstore.js atomic backup. +// Each test will wait for a write to the Session Store +// before executing. let tmp = {}; Cu.import("resource://gre/modules/osfile.jsm", tmp); Cu.import("resource://gre/modules/Task.jsm", tmp); Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", tmp); const {OS, Task, _SessionFile} = tmp; @@ -18,17 +20,17 @@ const backupPath = OS.Path.join(OS.Const // A text decoder. let gDecoder = new TextDecoder(); // Global variables that contain sessionstore.js and sessionstore.bak data for // comparison between tests. let gSSData; let gSSBakData; -// waitForSaveStateComplete waits for a state write completion. +// Wait for a state write to complete and then execute a callback. function waitForSaveStateComplete(aSaveStateCallback) { let topic = "sessionstore-state-write-complete"; function observer() { Services.prefs.clearUserPref(PREF_SS_INTERVAL); Services.obs.removeObserver(observer, topic); executeSoon(function taskCallback() { Task.spawn(aSaveStateCallback); @@ -36,61 +38,56 @@ function waitForSaveStateComplete(aSaveS } Services.obs.addObserver(observer, topic, false); } // Register next test callback and trigger state saving change. function nextTest(testFunc) { waitForSaveStateComplete(testFunc); + + // We set the interval for session store state saves to be zero + // to cause a save ASAP. Services.prefs.setIntPref(PREF_SS_INTERVAL, 0); } registerCleanupFunction(function() { // Cleaning up after the test: removing the sessionstore.bak file. Task.spawn(function cleanupTask() { yield OS.File.remove(backupPath); }); }); function test() { waitForExplicitFinish(); - nextTest(testInitialWriteNoBackup); + nextTest(testAfterFirstWrite); } -function testInitialWriteNoBackup() { - // Ensure that sessionstore.js is created, but not sessionstore.bak. - let ssExists = yield OS.File.exists(path); - let ssBackupExists = yield OS.File.exists(backupPath); - ok(ssExists, "sessionstore.js should be created."); - ok(!ssBackupExists, "sessionstore.bak should not have been created, yet."); - - nextTest(testWriteNoBackup); -} - -function testWriteNoBackup() { - // Ensure sessionstore.bak is not created. +function testAfterFirstWrite() { + // Ensure sessionstore.bak is not created. We start with a clean + // profile so there was nothing to move to sessionstore.bak before + // initially writing sessionstore.js let ssExists = yield OS.File.exists(path); let ssBackupExists = yield OS.File.exists(backupPath); ok(ssExists, "sessionstore.js should exist."); ok(!ssBackupExists, "sessionstore.bak should not have been created, yet"); // Save sessionstore.js data to compare to the sessionstore.bak data in the // next test. let array = yield OS.File.read(path); gSSData = gDecoder.decode(array); - // Manually trigger _SessionFile.moveToBackupPath since the backup once - // promise is already resolved and backup would not be triggered again. - yield _SessionFile.moveToBackupPath(); + // Manually move to the backup since the first write has already happened + // and a backup would not be triggered again. + yield OS.File.move(path, backupPath); - nextTest(testWriteBackup); + nextTest(testReadBackup); } -function testWriteBackup() { +function testReadBackup() { // Ensure sessionstore.bak is finally created. let ssExists = yield OS.File.exists(path); let ssBackupExists = yield OS.File.exists(backupPath); ok(ssExists, "sessionstore.js exists."); ok(ssBackupExists, "sessionstore.bak should now be created."); // Read sessionstore.bak data. let array = yield OS.File.read(backupPath); @@ -122,20 +119,21 @@ function testWriteBackup() { ssDataRead = yield _SessionFile.read(); is(ssDataRead, gSSBakData, "_SessionFile.read read sessionstore.bak correctly."); // Read sessionstore.bak with _SessionFile.syncRead. ssDataRead = _SessionFile.syncRead(); is(ssDataRead, gSSBakData, "_SessionFile.syncRead read sessionstore.bak correctly."); - nextTest(testNoWriteBackup); + + nextTest(testBackupUnchanged); } -function testNoWriteBackup() { +function testBackupUnchanged() { // Ensure sessionstore.bak is backed up only once. // Read sessionstore.bak data. let array = yield OS.File.read(backupPath); let ssBakData = gDecoder.decode(array); // Ensure the sessionstore.bak did not change. is(ssBakData, gSSBakData, "sessionstore.bak is unchanged.");
--- a/browser/components/sessionstore/test/browser_capabilities.js +++ b/browser/components/sessionstore/test/browser_capabilities.js @@ -25,16 +25,21 @@ function runTests() { let state = JSON.parse(ss.getTabState(tab)); ok(!("disallow" in state), "everything allowed by default"); ok(flags.every(f => docShell[f]), "all flags set to true"); // Flip a couple of allow* flags. docShell.allowImages = false; docShell.allowMetaRedirects = false; + // Now reload the document to ensure that these capabilities + // are taken into account + browser.reload(); + yield whenBrowserLoaded(browser); + // Check that we correctly save disallowed features. let disallowedState = JSON.parse(ss.getTabState(tab)); let disallow = new Set(disallowedState.disallow.split(",")); ok(disallow.has("Images"), "images not allowed"); ok(disallow.has("MetaRedirects"), "meta redirects not allowed"); is(disallow.size, 2, "two capabilities disallowed"); // Reuse the tab to restore a new, clean state into it. @@ -47,17 +52,17 @@ function runTests() { ok(flags.every(f => docShell[f]), "all flags set to true"); // Restore the state with disallowed features. ss.setTabState(tab, JSON.stringify(disallowedState)); yield waitForLoad(browser); // Check that docShell flags are set. ok(!docShell.allowImages, "images not allowed"); - ok(!docShell.allowMetaRedirects, "meta redirects not allowed") + ok(!docShell.allowMetaRedirects, "meta redirects not allowed"); // Check that we correctly restored features as disabled. state = JSON.parse(ss.getTabState(tab)); disallow = new Set(state.disallow.split(",")); ok(disallow.has("Images"), "images not allowed anymore"); ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore"); is(disallow.size, 2, "two capabilities disallowed");
new file mode 100644 --- /dev/null +++ b/browser/components/sessionstore/test/browser_sessionStorage.js @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +let Scope = {}; +Cu.import("resource://gre/modules/Task.jsm", Scope); +Cu.import("resource://gre/modules/Promise.jsm", Scope); +let {Task, Promise} = Scope; + +function promiseBrowserLoaded(aBrowser) {