author | Margaret Leibovic <margaret.leibovic@gmail.com> |
Sun, 04 Aug 2013 15:14:01 -0700 | |
changeset 143496 | 2c24acf4816b28a62c3871555c6e3c3e33bdb736 |
parent 143495 | 4c3421eb1d4358150b151a9f3b74bddf1d8a0e7d (current diff) |
parent 141219 | 0a63cd911b4f8c065c3dd344f59adfe5e9b57849 (diff) |
child 143497 | 9972d89f09bbf393b238cb2bdd86cba47026801c |
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/CLOBBER +++ b/CLOBBER @@ -12,10 +12,9 @@ # O O # | | # O <-- Clobber O <-- Clobber # # Note: The description below will be part of the error message shown to users. # # Modifying this file will now automatically clobber the buildbot machines \o/ # -Bug 895173: Rename context menu id's to "home_" for home context menu. -Android resource changes +Removal of XPIDL for bug 893117 requires a clobber to make sure interfaces aren't generated.
--- a/accessible/public/msaa/Makefile.in +++ b/accessible/public/msaa/Makefile.in @@ -34,18 +34,16 @@ MIDL_GENERATED_FILES = \ ISimpleDOMDocument.h \ ISimpleDOMDocument_p.c \ ISimpleDOMDocument_i.c \ ISimpleDOMText.h \ ISimpleDOMText_p.c \ ISimpleDOMText_i.c \ $(NULL) -SRCDIR_CSRCS = $(addprefix $(srcdir)/,$(CSRCS)) - OS_LIBS = $(call EXPAND_LIBNAME,kernel32 rpcns4 rpcrt4 oleaut32) $(MIDL_GENERATED_FILES): done_gen done_gen: ISimpleDOMNode.idl \ ISimpleDOMDocument.idl \ ISimpleDOMText.idl
--- a/accessible/src/base/ARIAMap.cpp +++ b/accessible/src/base/ARIAMap.cpp @@ -614,17 +614,17 @@ static nsRoleMapEntry sWAIRoleMaps[] = &nsGkAtoms::treegrid, roles::TREE_TABLE, kUseMapRole, eNoValue, eNoAction, eNoLiveAttr, eSelect | eTable, kNoReqStates, - eARIAReadonly, + eARIAReadonlyOrEditable, eARIAMultiSelectable, eFocusableUntilDisabled }, { // treeitem &nsGkAtoms::treeitem, roles::OUTLINEITEM, kUseMapRole, eNoValue,
--- a/accessible/src/base/ARIAStateMap.cpp +++ b/accessible/src/base/ARIAStateMap.cpp @@ -361,34 +361,29 @@ MapEnumType(dom::Element* aElement, uint *aState |= aData.mDefaultState; } static void MapTokenType(dom::Element* aElement, uint64_t* aState, const TokenTypeData& aData) { - if (aElement->HasAttr(kNameSpaceID_None, aData.mAttrName)) { + if (nsAccUtils::HasDefinedARIAToken(aElement, aData.mAttrName)) { if ((aData.mType & eMixedType) && aElement->AttrValueIs(kNameSpaceID_None, aData.mAttrName, nsGkAtoms::mixed, eCaseMatters)) { *aState |= aData.mPermanentState | states::MIXED; return; } if (aElement->AttrValueIs(kNameSpaceID_None, aData.mAttrName, nsGkAtoms::_false, eCaseMatters)) { *aState |= aData.mPermanentState | aData.mFalseState; return; } - if (!aElement->AttrValueIs(kNameSpaceID_None, aData.mAttrName, - nsGkAtoms::_undefined, eCaseMatters) && - !aElement->AttrValueIs(kNameSpaceID_None, aData.mAttrName, - nsGkAtoms::_empty, eCaseMatters)) { - *aState |= aData.mPermanentState | aData.mTrueState; - } + *aState |= aData.mPermanentState | aData.mTrueState; return; } if (aData.mType & eDefinedIfAbsent) *aState |= aData.mPermanentState | aData.mFalseState; }
--- a/accessible/src/base/ARIAStateMap.h +++ b/accessible/src/base/ARIAStateMap.h @@ -2,17 +2,17 @@ /* 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/. */ #ifndef _mozilla_a11y_aria_ARIAStateMap_h_ #define _mozilla_a11y_aria_ARIAStateMap_h_ -#include "mozilla/StandardInteger.h" +#include <stdint.h> namespace mozilla { namespace dom { class Element; } namespace a11y {
--- a/accessible/src/base/EventQueue.cpp +++ b/accessible/src/base/EventQueue.cpp @@ -298,17 +298,17 @@ EventQueue::CoalesceSelChangeEvents(AccS aTailEvent->mPackedEvent = aThisEvent; return; } if (aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd && aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { aTailEvent->mEventRule = AccEvent::eDoNotEmit; aThisEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; - aThisEvent->mPackedEvent = aThisEvent; + aThisEvent->mPackedEvent = aTailEvent; return; } } // Unpack the packed selection change event because we've got one // more selection add/remove. if (aThisEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { if (aThisEvent->mPackedEvent) { @@ -467,16 +467,39 @@ EventQueue::ProcessEventQueue() hyperText->GetSelectionCount(&selectionCount); if (selectionCount) nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED, hyperText); } continue; } + // Fire selected state change events in support to selection events. + if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_ADD) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + true, event->mIsFromUserInput); + + } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION_REMOVE) { + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + false, event->mIsFromUserInput); + + } else if (event->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { + AccSelChangeEvent* selChangeEvent = downcast_accEvent(event); + nsEventShell::FireEvent(event->mAccessible, states::SELECTED, + (selChangeEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd), + event->mIsFromUserInput); + + if (selChangeEvent->mPackedEvent) { + nsEventShell::FireEvent(selChangeEvent->mPackedEvent->mAccessible, + states::SELECTED, + (selChangeEvent->mPackedEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd), + selChangeEvent->mPackedEvent->mIsFromUserInput); + } + } + nsEventShell::FireEvent(event); // Fire text change events. AccMutationEvent* mutationEvent = downcast_accEvent(event); if (mutationEvent) { if (mutationEvent->mTextChangeEvent) nsEventShell::FireEvent(mutationEvent->mTextChangeEvent); }
--- a/accessible/src/base/Filters.h +++ b/accessible/src/base/Filters.h @@ -1,16 +1,16 @@ /* 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/. */ #ifndef mozilla_a11y_Filters_h__ #define mozilla_a11y_Filters_h__ -#include "mozilla/StandardInteger.h" +#include <stdint.h> /** * Predefined filters used for nsAccIterator and nsAccCollector. */ namespace mozilla { namespace a11y { class Accessible;
--- a/accessible/src/base/NotificationController.cpp +++ b/accessible/src/base/NotificationController.cpp @@ -43,16 +43,18 @@ NotificationController::~NotificationCon } //////////////////////////////////////////////////////////////////////////////// // NotificationCollector: AddRef/Release and cycle collection NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(NotificationController) NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(NotificationController) +NS_IMPL_CYCLE_COLLECTION_CLASS(NotificationController) + NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(NotificationController) if (tmp->mDocument) tmp->Shutdown(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(NotificationController) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHangingChildDocuments) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContentInsertions)
--- a/accessible/src/base/RoleAsserts.cpp +++ b/accessible/src/base/RoleAsserts.cpp @@ -7,13 +7,13 @@ #include "nsIAccessibleRole.h" #include "Role.h" #include "mozilla/Assertions.h" using namespace mozilla::a11y; #define ROLE(geckoRole, stringRole, atkRole, macRole, msaaRole, ia2Role, nameRule) \ - MOZ_STATIC_ASSERT(static_cast<uint32_t>(roles::geckoRole) \ - == static_cast<uint32_t>(nsIAccessibleRole::ROLE_ ## geckoRole), \ - "internal and xpcom roles differ!"); + static_assert(static_cast<uint32_t>(roles::geckoRole) \ + == static_cast<uint32_t>(nsIAccessibleRole::ROLE_ ## geckoRole), \ + "internal and xpcom roles differ!"); #include "RoleMap.h" #undef ROLE
--- a/accessible/src/base/States.h +++ b/accessible/src/base/States.h @@ -2,17 +2,17 @@ /* vim: set expandtab shiftwidth=2 tabstop=2: */ /* 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/. */ #ifndef _states_h_ #define _states_h_ -#include "mozilla/StandardInteger.h" +#include <stdint.h> namespace mozilla { namespace a11y { namespace states { /** * The object is disabled, opposite to enabled and sensitive. */
--- a/accessible/src/base/nsCoreUtils.cpp +++ b/accessible/src/base/nsCoreUtils.cpp @@ -36,16 +36,18 @@ #include "nsComponentManagerUtils.h" #include "nsIInterfaceRequestorUtils.h" #include "mozilla/dom/Element.h" #include "nsITreeBoxObject.h" #include "nsITreeColumns.h" +using namespace mozilla; + //////////////////////////////////////////////////////////////////////////////// // nsCoreUtils //////////////////////////////////////////////////////////////////////////////// bool nsCoreUtils::HasClickListener(nsIContent *aContent) { NS_ENSURE_TRUE(aContent, false); @@ -125,17 +127,17 @@ nsCoreUtils::DispatchClickEvent(nsITreeB 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); - event.refPoint = nsIntPoint(aX, aY); + event.refPoint = LayoutDeviceIntPoint(aX, aY); event.clickCount = 1; event.button = nsMouseEvent::eLeftButton; event.time = PR_IntervalNow(); event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN; nsEventStatus status = nsEventStatus_eIgnore; aPresShell->HandleEventWithTarget(&event, aFrame, aContent, &status);
--- a/accessible/src/base/nsEventShell.h +++ b/accessible/src/base/nsEventShell.h @@ -31,16 +31,30 @@ public: * @param aEventType [in] the event type * @param aAccessible [in] the event target */ static void FireEvent(uint32_t aEventType, mozilla::a11y::Accessible* aAccessible, mozilla::a11y::EIsFromUserInput aIsFromUserInput = mozilla::a11y::eAutoDetect); /** + * Fire state change event. + */ + static void FireEvent(mozilla::a11y::Accessible* aTarget, uint64_t aState, + bool aIsEnabled, bool aIsFromUserInput) + { + nsRefPtr<mozilla::a11y::AccStateChangeEvent> stateChangeEvent = + new mozilla::a11y::AccStateChangeEvent(aTarget, aState, aIsEnabled, + (aIsFromUserInput ? + mozilla::a11y::eFromUserInput : + mozilla::a11y::eNoUserInput)); + FireEvent(stateChangeEvent); + } + + /** * Append 'event-from-input' object attribute if the accessible event has * been fired just now for the given node. * * @param aNode [in] the DOM node * @param aAttributes [in, out] the attributes */ static void GetEventAttributes(nsINode *aNode, nsIPersistentProperties *aAttributes);
--- a/accessible/src/generic/Accessible.cpp +++ b/accessible/src/generic/Accessible.cpp @@ -835,17 +835,17 @@ Accessible::ChildAtPoint(int32_t aX, int nsIWidget* rootWidget = rootFrame->GetView()->GetNearestWidget(nullptr); NS_ENSURE_TRUE(rootWidget, nullptr); nsIntRect rootRect; rootWidget->GetScreenBounds(rootRect); nsMouseEvent dummyEvent(true, NS_MOUSE_MOVE, rootWidget, nsMouseEvent::eSynthesized); - dummyEvent.refPoint = nsIntPoint(aX - rootRect.x, aY - rootRect.y); + dummyEvent.refPoint = LayoutDeviceIntPoint(aX - rootRect.x, aY - rootRect.y); nsIFrame* popupFrame = nsLayoutUtils:: GetPopupFrameForEventCoordinates(accDocument->PresContext()->GetRootPresContext(), &dummyEvent); if (popupFrame) { // If 'this' accessible is not inside the popup then ignore the popup when // searching an accessible at point. DocAccessible* popupDoc = @@ -3348,24 +3348,24 @@ Accessible::GetLevelInternal() } return level; } void Accessible::StaticAsserts() const { - MOZ_STATIC_ASSERT(eLastChildrenFlag <= (2 << kChildrenFlagsBits) - 1, - "Accessible::mChildrenFlags was oversized by eLastChildrenFlag!"); - MOZ_STATIC_ASSERT(eLastStateFlag <= (2 << kStateFlagsBits) - 1, - "Accessible::mStateFlags was oversized by eLastStateFlag!"); - MOZ_STATIC_ASSERT(eLastAccType <= (2 << kTypeBits) - 1, - "Accessible::mType was oversized by eLastAccType!"); - MOZ_STATIC_ASSERT(eLastAccGenericType <= (2 << kGenericTypesBits) - 1, - "Accessible::mGenericType was oversized by eLastAccGenericType!"); + static_assert(eLastChildrenFlag <= (2 << kChildrenFlagsBits) - 1, + "Accessible::mChildrenFlags was oversized by eLastChildrenFlag!"); + static_assert(eLastStateFlag <= (2 << kStateFlagsBits) - 1, + "Accessible::mStateFlags was oversized by eLastStateFlag!"); + static_assert(eLastAccType <= (2 << kTypeBits) - 1, + "Accessible::mType was oversized by eLastAccType!"); + static_assert(eLastAccGenericType <= (2 << kGenericTypesBits) - 1, + "Accessible::mGenericType was oversized by eLastAccGenericType!"); } //////////////////////////////////////////////////////////////////////////////// // KeyBinding class void KeyBinding::ToPlatformFormat(nsAString& aValue) const
--- a/accessible/src/generic/Accessible.h +++ b/accessible/src/generic/Accessible.h @@ -718,16 +718,25 @@ public: bool HasOwnContent() const { return mContent && !(mStateFlags & eSharedNode); } /** * Return true if the accessible has a numeric value. */ bool HasNumericValue() const; + /** + * Return true if the accessible state change is processed by handling proper + * DOM UI event, if otherwise then false. For example, HTMLCheckboxAccessible + * process nsIDocumentObserver::ContentStateChanged instead + * 'CheckboxStateChange' event. + */ + bool NeedsDOMUIEvent() const + { return !(mStateFlags & eIgnoreDOMUIEvent); } + protected: /** * Return the accessible name provided by native markup. It doesn't take * into account ARIA markup used to specify the name. */ virtual mozilla::a11y::ENameValueFlag NativeName(nsString& aName); @@ -784,16 +793,17 @@ protected: * @note keep these flags in sync with ChildrenFlags */ enum StateFlags { eIsDefunct = 1 << 0, // accessible is defunct eIsNotInDocument = 1 << 1, // accessible is not in document eSharedNode = 1 << 2, // accessible shares DOM node from another accessible eNotNodeMapEntry = 1 << 3, // accessible shouldn't be in document node map eHasNumericValue = 1 << 4, // accessible has a numeric value + eIgnoreDOMUIEvent = 1 << 5, // don't process DOM UI events for a11y events eLastStateFlag = eHasNumericValue }; protected: ////////////////////////////////////////////////////////////////////////////// // Miscellaneous helpers
--- a/accessible/src/generic/DocAccessible.cpp +++ b/accessible/src/generic/DocAccessible.cpp @@ -98,16 +98,18 @@ DocAccessible::~DocAccessible() { NS_ASSERTION(!mPresShell, "LastRelease was never called!?!"); } //////////////////////////////////////////////////////////////////////////////// // nsISupports +NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, Accessible) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments) tmp->mDependentIDsHash.EnumerateRead(CycleCollectorTraverseDepIDsEntry, &cb); NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END @@ -1151,17 +1153,23 @@ DocAccessible::ContentStateChanged(nsIDo Accessible* widget = accessible->ContainerWidget(); if (widget && widget->IsSelect()) { AccSelChangeEvent::SelChangeType selChangeType = aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED) ? AccSelChangeEvent::eSelectionAdd : AccSelChangeEvent::eSelectionRemove; nsRefPtr<AccEvent> event = new AccSelChangeEvent(widget, accessible, selChangeType); FireDelayedEvent(event); + return; } + + nsRefPtr<AccEvent> event = + new AccStateChangeEvent(accessible, states::CHECKED, + aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED)); + FireDelayedEvent(event); } if (aStateMask.HasState(NS_EVENT_STATE_INVALID)) { nsRefPtr<AccEvent> event = new AccStateChangeEvent(accessible, states::INVALID, true); FireDelayedEvent(event); } @@ -1825,20 +1833,26 @@ DocAccessible::UpdateTree(Accessible* aC } uint32_t DocAccessible::UpdateTreeInternal(Accessible* aChild, bool aIsInsert, AccReorderEvent* aReorderEvent) { uint32_t updateFlags = eAccessible; + // If a focused node has been shown then it could mean its frame was recreated + // while the node stays focused and we need to fire focus event on + // the accessible we just created. If the queue contains a focus event for + // this node already then it will be suppressed by this one. + Accessible* focusedAcc = nullptr; + nsINode* node = aChild->GetNode(); if (aIsInsert) { // Create accessible tree for shown accessible. - CacheChildrenInSubtree(aChild); + CacheChildrenInSubtree(aChild, &focusedAcc); } else { // Fire menupopup end event before hide event if a menu goes away. // XXX: We don't look into children of hidden subtree to find hiding // menupopup (as we did prior bug 570275) because we don't do that when // menu is showing (and that's impossible until bug 606924 is fixed). // Nevertheless we should do this at least because layout coalesces @@ -1865,57 +1879,59 @@ DocAccessible::UpdateTreeInternal(Access // Fire EVENT_MENUPOPUP_START if ARIA menu appears. FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, aChild); } else if (ariaRole == roles::ALERT) { // Fire EVENT_ALERT if ARIA alert appears. updateFlags = eAlertAccessible; FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, aChild); } - - // If focused node has been shown then it means its frame was recreated - // while it's focused. Fire focus event on new focused accessible. If - // the queue contains focus event for this node then it's suppressed by - // this one. - // XXX: do we really want to send focus to focused DOM node not taking into - // account active item? - if (FocusMgr()->IsFocused(aChild)) - FocusMgr()->DispatchFocusEvent(this, aChild); - } else { // Update the tree for content removal. // The accessible parent may differ from container accessible if // the parent doesn't have own DOM node like list accessible for HTML // selects. Accessible* parent = aChild->Parent(); NS_ASSERTION(parent, "No accessible parent?!"); if (parent) parent->RemoveChild(aChild); UncacheChildrenInSubtree(aChild); } + // XXX: do we really want to send focus to focused DOM node not taking into + // account active item? + if (focusedAcc) + FocusMgr()->DispatchFocusEvent(this, focusedAcc); + return updateFlags; } void -DocAccessible::CacheChildrenInSubtree(Accessible* aRoot) +DocAccessible::CacheChildrenInSubtree(Accessible* aRoot, + Accessible** aFocusedAcc) { + // If the accessible is focused then report a focus event after all related + // mutation events. + if (aFocusedAcc && !*aFocusedAcc && + FocusMgr()->HasDOMFocus(aRoot->GetContent())) + *aFocusedAcc = aRoot; + aRoot->EnsureChildren(); // Make sure we create accessible tree defined in DOM only, i.e. if accessible // provides specific tree (like XUL trees) then tree creation is handled by // this accessible. uint32_t count = aRoot->ContentChildCount(); for (uint32_t idx = 0; idx < count; idx++) { Accessible* child = aRoot->ContentChildAt(idx); NS_ASSERTION(child, "Illicit tree change while tree is created!"); // Don't cross document boundaries. if (child && child->IsContent()) - CacheChildrenInSubtree(child); + CacheChildrenInSubtree(child, aFocusedAcc); } // Fire document load complete on ARIA documents. // XXX: we should delay an event if the ARIA document has aria-busy. if (aRoot->HasARIARole() && !aRoot->IsDoc()) { a11y::role role = aRoot->ARIARole(); if (role == roles::DIALOG || role == roles::DOCUMENT) FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot);
--- a/accessible/src/generic/DocAccessible.h +++ b/accessible/src/generic/DocAccessible.h @@ -444,18 +444,23 @@ protected: eAlertAccessible = 2 }; uint32_t UpdateTreeInternal(Accessible* aChild, bool aIsInsert, AccReorderEvent* aReorderEvent); /** * Create accessible tree. + * + * @param aRoot [in] a root of subtree to create + * @param aFocusedAcc [in, optional] a focused accessible under created + * subtree if any */ - void CacheChildrenInSubtree(Accessible* aRoot); + void CacheChildrenInSubtree(Accessible* aRoot, + Accessible** aFocusedAcc = nullptr); /** * Remove accessibles in subtree from node to accessible map. */ void UncacheChildrenInSubtree(Accessible* aRoot); /** * Shutdown any cached accessible in the subtree.
--- a/accessible/src/generic/FormControlAccessible.h +++ b/accessible/src/generic/FormControlAccessible.h @@ -16,17 +16,19 @@ namespace a11y { */ template<int Max> class ProgressMeterAccessible : public LeafAccessible { public: ProgressMeterAccessible(nsIContent* aContent, DocAccessible* aDoc) : LeafAccessible(aContent, aDoc) { - mStateFlags |= eHasNumericValue; + // Ignore 'ValueChange' DOM event in lieu of @value attribute change + // notifications. + mStateFlags |= eHasNumericValue | eIgnoreDOMUIEvent; mType = eProgressType; } NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSIACCESSIBLEVALUE // Accessible virtual void Value(nsString& aValue);
--- a/accessible/src/generic/RootAccessible.cpp +++ b/accessible/src/generic/RootAccessible.cpp @@ -305,47 +305,44 @@ RootAccessible::ProcessDOMEvent(nsIDOMEv HandleTreeInvalidatedEvent(aDOMEvent, treeAcc); return; } } #endif if (eventType.EqualsLiteral("RadioStateChange")) { uint64_t state = accessible->State(); - - // radiogroup in prefWindow is exposed as a list, - // and panebutton is exposed as XULListitem in A11y. - // XULListitemAccessible::GetStateInternal uses STATE_SELECTED in this case, - // so we need to check states::SELECTED also. bool isEnabled = (state & (states::CHECKED | states::SELECTED)) != 0; - nsRefPtr<AccEvent> accEvent = - new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); - nsEventShell::FireEvent(accEvent); + if (accessible->NeedsDOMUIEvent()) { + nsRefPtr<AccEvent> accEvent = + new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); + nsEventShell::FireEvent(accEvent); + } if (isEnabled) { FocusMgr()->ActiveItemChanged(accessible); #ifdef A11Y_LOG if (logging::IsEnabled(logging::eFocus)) logging::ActiveItemChangeCausedBy("RadioStateChange", accessible); #endif } return; } if (eventType.EqualsLiteral("CheckboxStateChange")) { - uint64_t state = accessible->State(); - - bool isEnabled = !!(state & states::CHECKED); + if (accessible->NeedsDOMUIEvent()) { + uint64_t state = accessible->State(); + bool isEnabled = !!(state & states::CHECKED); - nsRefPtr<AccEvent> accEvent = - new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); - - nsEventShell::FireEvent(accEvent); + nsRefPtr<AccEvent> accEvent = + new AccStateChangeEvent(accessible, states::CHECKED, isEnabled); + nsEventShell::FireEvent(accEvent); + } return; } Accessible* treeItemAcc = nullptr; #ifdef MOZ_XUL // If it's a tree element, need the currently selected item. if (treeAcc) { treeItemAcc = accessible->CurrentItem(); @@ -450,24 +447,20 @@ RootAccessible::ProcessDOMEvent(nsIDOMEv accessible, eFromUserInput); FocusMgr()->ActiveItemChanged(nullptr); #ifdef A11Y_LOG if (logging::IsEnabled(logging::eFocus)) logging::ActiveItemChangeCausedBy("DOMMenuBarInactive", accessible); #endif } - else if (eventType.EqualsLiteral("ValueChange")) { - - //We don't process 'ValueChange' events for progress meters since we listen - //@value attribute change for them. - if (!accessible->IsProgress()) { - targetDocument->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, - accessible); - } + else if (accessible->NeedsDOMUIEvent() && + eventType.EqualsLiteral("ValueChange")) { + targetDocument->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, + accessible); } #ifdef DEBUG_DRAGDROPSTART else if (eventType.EqualsLiteral("mouseover")) { nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_DRAGDROP_START, accessible); } #endif }
--- a/accessible/src/generic/TableCellAccessible.h +++ b/accessible/src/generic/TableCellAccessible.h @@ -3,17 +3,17 @@ /* 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/. */ #ifndef mozilla_a11y_TableCellAccessible_h__ #define mozilla_a11y_TableCellAccessible_h__ #include "nsTArray.h" -#include "mozilla/StandardInteger.h" +#include <stdint.h> namespace mozilla { namespace a11y { class Accessible; class TableAccessible; /**
--- a/accessible/src/html/HTMLFormControlAccessible.cpp +++ b/accessible/src/html/HTMLFormControlAccessible.cpp @@ -34,22 +34,16 @@ using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::a11y; //////////////////////////////////////////////////////////////////////////////// // HTMLCheckboxAccessible //////////////////////////////////////////////////////////////////////////////// -HTMLCheckboxAccessible:: - HTMLCheckboxAccessible(nsIContent* aContent, DocAccessible* aDoc) : - LeafAccessible(aContent, aDoc) -{ -} - role HTMLCheckboxAccessible::NativeRole() { return roles::CHECKBUTTON; } uint8_t HTMLCheckboxAccessible::ActionCount() @@ -114,22 +108,16 @@ HTMLCheckboxAccessible::IsWidget() const return true; } //////////////////////////////////////////////////////////////////////////////// // HTMLRadioButtonAccessible //////////////////////////////////////////////////////////////////////////////// -HTMLRadioButtonAccessible:: - HTMLRadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc) : - RadioButtonAccessible(aContent, aDoc) -{ -} - uint64_t HTMLRadioButtonAccessible::NativeState() { uint64_t state = AccessibleWrap::NativeState(); state |= states::CHECKABLE; HTMLInputElement* input = HTMLInputElement::FromContent(mContent);
--- a/accessible/src/html/HTMLFormControlAccessible.h +++ b/accessible/src/html/HTMLFormControlAccessible.h @@ -21,17 +21,23 @@ typedef ProgressMeterAccessible<1> HTMLP * Accessible for HTML input@type="checkbox". */ class HTMLCheckboxAccessible : public LeafAccessible { public: enum { eAction_Click = 0 }; - HTMLCheckboxAccessible(nsIContent* aContent, DocAccessible* aDoc); + HTMLCheckboxAccessible(nsIContent* aContent, DocAccessible* aDoc) : + LeafAccessible(aContent, aDoc) + { + // Ignore "CheckboxStateChange" DOM event in lieu of document observer + // state change notification. + mStateFlags |= eIgnoreDOMUIEvent; + } // nsIAccessible NS_IMETHOD GetActionName(uint8_t aIndex, nsAString& aName); NS_IMETHOD DoAction(uint8_t index); // Accessible virtual mozilla::a11y::role NativeRole(); virtual uint64_t NativeState(); @@ -46,17 +52,23 @@ public: /** * Accessible for HTML input@type="radio" element. */ class HTMLRadioButtonAccessible : public RadioButtonAccessible { public: - HTMLRadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc); + HTMLRadioButtonAccessible(nsIContent* aContent, DocAccessible* aDoc) : + RadioButtonAccessible(aContent, aDoc) + { + // Ignore "RadioStateChange" DOM event in lieu of document observer + // state change notification. + mStateFlags |= eIgnoreDOMUIEvent; + } // Accessible virtual uint64_t NativeState(); virtual void GetPositionAndSizeInternal(int32_t *aPosInSet, int32_t *aSetSize); };
--- a/accessible/src/windows/msaa/AccessibleWrap.cpp +++ b/accessible/src/windows/msaa/AccessibleWrap.cpp @@ -1612,18 +1612,18 @@ AccessibleWrap::HandleAccEvent(AccEvent* // when running in metro mode. This confuses input focus tracking // in metro's UIA implementation. if (XRE_GetWindowsEnvironment() == WindowsEnvironmentType_Metro) { return NS_OK; } uint32_t eventType = aEvent->GetEventType(); - MOZ_STATIC_ASSERT(sizeof(gWinEventMap)/sizeof(gWinEventMap[0]) == nsIAccessibleEvent::EVENT_LAST_ENTRY, - "MSAA event map skewed"); + static_assert(sizeof(gWinEventMap)/sizeof(gWinEventMap[0]) == nsIAccessibleEvent::EVENT_LAST_ENTRY, + "MSAA event map skewed"); NS_ENSURE_TRUE(eventType > 0 && eventType < ArrayLength(gWinEventMap), NS_ERROR_FAILURE); uint32_t winEvent = gWinEventMap[eventType]; if (!winEvent) return NS_OK; // Means we're not active.
--- a/accessible/src/windows/msaa/Compatibility.h +++ b/accessible/src/windows/msaa/Compatibility.h @@ -2,17 +2,17 @@ /* 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/. */ #ifndef COMPATIBILITY_MANAGER_H #define COMPATIBILITY_MANAGER_H -#include "mozilla/StandardInteger.h" +#include <stdint.h> namespace mozilla { namespace a11y { /** * Used to get compatibility modes. Note, modes are computed at accessibility * start up time and aren't changed during lifetime. */
--- a/accessible/tests/mochitest/events.js +++ b/accessible/tests/mochitest/events.js @@ -503,69 +503,74 @@ function eventQueue(aEventType) ok(false, "Unique type " + eventQueue.getEventTypeAsString(checker) + " event was handled."); } } } } - var matchedChecker = null; + var hasMatchedCheckers = false; for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { var eventSeq = this.mScenarios[scnIdx]; // Check if handled event matches expected sync event. var nextChecker = this.getNextExpectedEvent(eventSeq); if (nextChecker) { if (eventQueue.compareEvents(nextChecker, aEvent)) { - matchedChecker = nextChecker; - matchedChecker.wasCaught++; - break; + this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); + hasMatchedCheckers = true; + continue; } } // Check if handled event matches any expected async events. for (idx = 0; idx < eventSeq.length; idx++) { if (!eventSeq[idx].unexpected && eventSeq[idx].async) { if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { - matchedChecker = eventSeq[idx]; - matchedChecker.wasCaught++; + this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); + hasMatchedCheckers = true; break; } } } } - // Call 'check' functions on invoker's side. - if (matchedChecker) { - if ("check" in matchedChecker) - matchedChecker.check(aEvent); - + if (hasMatchedCheckers) { var invoker = this.getInvoker(); if ("check" in invoker) invoker.check(aEvent); } - // Dump handled event. - eventQueue.logEvent(aEvent, matchedChecker, this.areExpectedEventsLeft(), - this.mNextInvokerStatus); - // If we don't have more events to wait then schedule next invoker. - if (!this.areExpectedEventsLeft() && + if (this.hasMatchedScenario() && (this.mNextInvokerStatus == kInvokerNotScheduled)) { this.processNextInvokerInTimeout(); return; } // If we have scheduled a next invoker then cancel in case of match. - if ((this.mNextInvokerStatus == kInvokerPending) && matchedChecker) + if ((this.mNextInvokerStatus == kInvokerPending) && hasMatchedCheckers) this.mNextInvokerStatus = kInvokerCanceled; } // Helpers + this.processMatchedChecker = + function eventQueue_function(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx) + { + aMatchedChecker.wasCaught++; + + if ("check" in aMatchedChecker) + aMatchedChecker.check(aEvent); + + eventQueue.logEvent(aEvent, aMatchedChecker, aScenarioIdx, aEventIdx, + this.areExpectedEventsLeft(), + this.mNextInvokerStatus); + } + this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent(aEventSeq) { if (!("idx" in aEventSeq)) aEventSeq.idx = 0; while (aEventSeq.idx < aEventSeq.length && (aEventSeq[aEventSeq.idx].unexpected || @@ -630,16 +635,26 @@ function eventQueue(aEventType) break; } if (idx == eventSeq.length) return true; } return false; } + + this.hasMatchedScenario = + function eventQueue_hasMatchedScenario() + { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + if (!this.areExpectedEventsLeft(this.mScenarios[scnIdx])) + return true; + } + return false; + } this.getInvoker = function eventQueue_getInvoker() { return this.mInvokers[this.mIndex]; } this.getNextInvoker = function eventQueue_getNextInvoker() { @@ -853,16 +868,17 @@ eventQueue.isSameEvent = function eventQ // target, thus we should filter text change and state change events since // they may occur on the same element because of complex changes. return this.compareEvents(aChecker, aEvent) && !(aEvent instanceof nsIAccessibleTextChangeEvent) && !(aEvent instanceof nsIAccessibleStateChangeEvent); } eventQueue.logEvent = function eventQueue_logEvent(aOrigEvent, aMatchedChecker, + aScenarioIdx, aEventIdx, aAreExpectedEventsLeft, aInvokerStatus) { if (!gLogger.isEnabled()) // debug stuff return; // Dump DOM event information. Skip a11y event since it is dumped by // gA11yEventObserver. @@ -892,17 +908,18 @@ eventQueue.logEvent = function eventQueu if (!aMatchedChecker) return; var msg = "EQ: "; var emphText = "matched "; var currType = eventQueue.getEventTypeAsString(aMatchedChecker); var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); - var consoleMsg = "*****\nEQ matched: " + currType + "\n*****"; + var consoleMsg = "*****\nScenario " + aScenarioIdx + + ", event " + aEventIdx + " matched: " + currType + "\n*****"; gLogger.logToConsole(consoleMsg); msg += " event, type: " + currType + ", target: " + currTargetDescr; gLogger.logToDOM(msg, true, emphText); } @@ -1722,17 +1739,18 @@ function stateChangeChecker(aState, aIsE var unxpdExtraState = aIsEnabled ? 0 : (aIsExtraState ? aState : 0); testStates(event.accessible, state, extraState, unxpdState, unxpdExtraState); } this.match = function stateChangeChecker_match(aEvent) { if (aEvent instanceof nsIAccessibleStateChangeEvent) { var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); - return aEvent.accessible = this.target && scEvent.state == aState; + return (aEvent.accessible == getAccessible(this.target)) && + (scEvent.state == aState); } return false; } } function asyncStateChangeChecker(aState, aIsExtraState, aIsEnabled, aTargetOrFunc, aTargetFuncArg) { @@ -1767,16 +1785,69 @@ function expandedStateChecker(aIsEnabled "Wrong state of statechange event state"); testStates(event.accessible, (aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED)); } } //////////////////////////////////////////////////////////////////////////////// +// Event sequances (array of predefined checkers) + +/** + * Event seq for single selection change. + */ +function selChangeSeq(aUnselectedID, aSelectedID) +{ + if (!aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID) + ]; + } + + // Return two possible scenarios: depending on widget type when selection is + // moved the the order of items that get selected and unselected may vary. + return [ + [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID) + ], + [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID) + ] + ]; +} + +/** + * Event seq for item removed form the selection. + */ +function selRemoveSeq(aUnselectedID) +{ + return [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID) + ]; +} + +/** + * Event seq for item added to the selection. + */ +function selAddSeq(aSelectedID) +{ + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION_ADD, aSelectedID) + ]; +} + +//////////////////////////////////////////////////////////////////////////////// // Private implementation details. //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // General var gA11yEventListeners = {}; @@ -2038,23 +2109,30 @@ function sequenceItem(aProcessor, aEvent } //////////////////////////////////////////////////////////////////////////////// // Event queue invokers /** * Invoker base class for prepare an action. */ -function synthAction(aNodeOrID, aCheckerOrEventSeq) +function synthAction(aNodeOrID, aEventsObj) { this.DOMNode = getNode(aNodeOrID); - if (aCheckerOrEventSeq) { - if (aCheckerOrEventSeq instanceof Array) { - this.eventSeq = aCheckerOrEventSeq; + if (aEventsObj) { + var scenarios = null; + if (aEventsObj instanceof Array) { + if (aEventsObj[0] instanceof Array) + scenarios = aEventsObj; // scenarios + else + scenarios = [ aEventsObj ]; // event sequance } else { - this.eventSeq = [ aCheckerOrEventSeq ]; + scenarios = [ [ aEventsObj ] ]; // a single checker object } + + for (var i = 0; i < scenarios.length; i++) + defineScenario(this, scenarios[i]); } this.getID = function synthAction_getID() { return prettyName(aNodeOrID) + " action"; } }
--- a/accessible/tests/mochitest/events/test_selection.html +++ b/accessible/tests/mochitest/events/test_selection.html @@ -33,56 +33,61 @@ function doTests() { gQueue = new eventQueue(); // open combobox gQueue.push(new synthClick("combobox", new invokerChecker(EVENT_FOCUS, "cb1_item1"))); gQueue.push(new synthDownKey("cb1_item1", - new invokerChecker(EVENT_SELECTION, "cb1_item2"))); + selChangeSeq("cb1_item1", "cb1_item2"))); // closed combobox gQueue.push(new synthEscapeKey("combobox", new invokerChecker(EVENT_FOCUS, "combobox"))); gQueue.push(new synthDownKey("cb1_item2", - new invokerChecker(EVENT_SELECTION, "cb1_item3"))); + selChangeSeq("cb1_item2", "cb1_item3"))); // listbox gQueue.push(new synthClick("lb1_item1", new invokerChecker(EVENT_SELECTION, "lb1_item1"))); gQueue.push(new synthDownKey("lb1_item1", - new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + selChangeSeq("lb1_item1", "lb1_item2"))); // multiselectable listbox gQueue.push(new synthClick("lb2_item1", - new invokerChecker(EVENT_SELECTION, "lb2_item1"))); + selChangeSeq(null, "lb2_item1"))); gQueue.push(new synthDownKey("lb2_item1", - new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"), + selAddSeq("lb2_item2"), { shiftKey: true })); gQueue.push(new synthUpKey("lb2_item2", - new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"), + selRemoveSeq("lb2_item2"), { shiftKey: true })); gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, - new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1"))); + selRemoveSeq("lb2_item1"))); gQueue.invoke(); // Will call SimpleTest.finish(); } SimpleTest.waitForExplicitFinish(); addA11yLoadEvent(doTests); </script> </head> <body> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302" title="Incorrect selection events in HTML, XUL and ARIA"> - Mozilla Bug 414302 + Bug 414302 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=810268" + title="There's no way to know unselected item when selection in single selection was changed"> + Bug 810268 </a> <p id="display"></p> <div id="content" style="display: none"></div> <pre id="test"> </pre> <select id="combobox">
--- a/accessible/tests/mochitest/events/test_selection_aria.html +++ b/accessible/tests/mochitest/events/test_selection_aria.html @@ -31,17 +31,17 @@ this.eventSeq = [ new invokerChecker(EVENT_SELECTION, aItemID) ]; this.invoke = function selectItem_invoke() { var itemNode = this.selectNode.querySelector("*[aria-selected='true']"); if (itemNode) - itemNode.removeAttribute("aria-selected", "true"); + itemNode.removeAttribute("aria-selected"); this.itemNode.setAttribute("aria-selected", "true"); } this.getID = function selectItem_getID() { return "select item " + prettyName(aItemID); }
--- a/accessible/tests/mochitest/events/test_statechange.html +++ b/accessible/tests/mochitest/events/test_statechange.html @@ -67,16 +67,36 @@ testStates(aNodeOrID, STATE_INVALID); }; this.getID = function invalidInput_getID() { return prettyName(aNodeOrID) + " became invalid"; }; } + function changeCheckInput(aID, aIsChecked) + { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new stateChangeChecker(STATE_CHECKED, false, aIsChecked, this.DOMNode) + ]; + + this.invoke = function changeCheckInput_invoke() + { + this.DOMNode.checked = aIsChecked; + } + + this.getID = function changeCheckInput_getID() + { + return "change checked state to '" + aIsChecked + "' for " + + prettyName(aID); + } + } + function stateChangeOnFileInput(aID, aAttr, aValue, aState, aIsExtraState, aIsEnabled) { this.fileControlNode = getNode(aID); this.fileControl = getAccessible(this.fileControlNode); this.browseButton = this.fileControl.firstChild; // No state change events on the label. @@ -149,16 +169,22 @@ // Test delayed editable state change var doc = document.getElementById("iframe").contentDocument; gQueue.push(new makeEditableDoc(doc)); // invalid state change gQueue.push(new invalidInput("email")); + // checked state change + gQueue.push(new changeCheckInput("checkbox", true)); + gQueue.push(new changeCheckInput("checkbox", false)); + gQueue.push(new changeCheckInput("radio", true)); + gQueue.push(new changeCheckInput("radio", false)); + // file input inherited state changes gQueue.push(new stateChangeOnFileInput("file", "aria-busy", "true", STATE_BUSY, false, true)); gQueue.push(new stateChangeOnFileInput("file", "aria-required", "true", STATE_REQUIRED, false, true)); gQueue.push(new stateChangeOnFileInput("file", "aria-invalid", "true", STATE_INVALID, false, true)); @@ -175,39 +201,47 @@ </script> </head> <body> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=564471" title="Make state change events async"> - Mozilla Bug 564471 - </a><br> + Bug 564471 + </a> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555728" title="Fire a11y event based on HTML5 constraint validation"> - Mozilla Bug 555728 + Bug 555728 </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 + Bug 699017 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=788389" + title="Fire statechange event whenever checked state is changed not depending on focused state"> + Bug 788389 </a> <p id="display"></p> <div id="content" style="display: none"></div> <pre id="test"> </pre> <div id="testContainer"> <iframe id="iframe"></iframe> </div> <input id="email" type='email'> + <input id="checkbox" type="checkbox"> + <input id="radio" type="radio"> + <input id="file" type="file"> <div id="div"></div> <div id="eventdump"></div> </body> </html>
--- a/accessible/tests/mochitest/states/test_aria.html +++ b/accessible/tests/mochitest/states/test_aria.html @@ -136,16 +136,47 @@ STATE_READONLY, 0); testStates("aria_grid_readonly_rowheader_inherited", STATE_READONLY, 0, 0, EXT_STATE_EDITABLE); testStates("aria_grid_readonly_cell_editable", 0, EXT_STATE_EDITABLE, STATE_READONLY, 0); testStates("aria_grid_readonly_cell_inherited", STATE_READONLY, 0, 0, EXT_STATE_EDITABLE); + // readonly/editable on treegrid and gridcell + testStates("aria_treegrid_default", 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0); + testStates("aria_treegrid_default_colheader_readonly", STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE); + testStates("aria_treegrid_default_colheader_inherited", 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0); + testStates("aria_treegrid_default_rowheader_readonly", STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE); + testStates("aria_treegrid_default_rowheader_inherited", 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0); + testStates("aria_treegrid_default_cell_readonly", STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE); + testStates("aria_treegrid_default_cell_inherited", 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0); + + testStates("aria_treegrid_readonly", STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE); + testStates("aria_treegrid_readonly_colheader_editable", 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0); + testStates("aria_treegrid_readonly_colheader_inherited", STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE); + testStates("aria_treegrid_readonly_rowheader_editable", 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0); + testStates("aria_treegrid_readonly_rowheader_inherited", STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE); + testStates("aria_treegrid_readonly_cell_editable", 0, EXT_STATE_EDITABLE, + STATE_READONLY, 0); + testStates("aria_treegrid_readonly_cell_inherited", STATE_READONLY, 0, + 0, EXT_STATE_EDITABLE); + // aria-selectable testStates("aria_selectable_listitem", STATE_SELECTABLE | STATE_SELECTED); // active state caused by aria-activedescendant testStates("as_item1", 0, EXT_STATE_ACTIVE); testStates("as_item2", 0, 0, 0, EXT_STATE_ACTIVE); // universal ARIA properties inherited from file input control @@ -268,16 +299,21 @@ Mozilla Bug 740851 </a> <a target="_blank" 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=892091" + title="ARIA treegrid should be editable by default"> + Bug 892091 + </a> + <a target="_blank" 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"> @@ -354,16 +390,58 @@ <div role="row"> <div id="aria_grid_readonly_cell_editable" role="gridcell" aria-readonly="false">gridcell1</div> <div id="aria_grid_readonly_cell_inherited" role="gridcell">gridcell2</div> </div> </div> + <div id="aria_treegrid_default" role="grid"> + <div role="row"> + <div id="aria_treegrid_default_colheader_readonly" + role="columnheader" aria-readonly="true">colheader1</div> + <div id="aria_treegrid_default_colheader_inherited" + role="columnheader">colheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_default_rowheader_readonly" + role="rowheader" aria-readonly="true">rowheader1</div> + <div id="aria_treegrid_default_rowheader_inherited" + role="rowheader">rowheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_default_cell_readonly" + role="gridcell" aria-readonly="true">gridcell1</div> + <div id="aria_treegrid_default_cell_inherited" + role="gridcell">gridcell2</div> + </div> + </div> + + <div id="aria_treegrid_readonly" role="grid" aria-readonly="true"> + <div role="row"> + <div id="aria_treegrid_readonly_colheader_editable" + role="columnheader" aria-readonly="false">colheader1</div> + <div id="aria_treegrid_readonly_colheader_inherited" + role="columnheader">colheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_readonly_rowheader_editable" + role="rowheader" aria-readonly="false">rowheader1</div> + <div id="aria_treegrid_readonly_rowheader_inherited" + role="rowheader">rowheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_readonly_cell_editable" + role="gridcell" aria-readonly="false">gridcell1</div> + <div id="aria_treegrid_readonly_cell_inherited" + role="gridcell">gridcell2</div> + </div> + </div> + <div role="listbox"> <div id="aria_selectable_listitem" role="option" aria-selected="true">Item1</div> </div> <!-- Test that aria-disabled state gets propagated to all descendants --> <div id="group" role="group" aria-disabled="true"> <button>hi</button> <div tabindex="0" role="listbox" aria-activedescendant="item1">
--- a/accessible/tests/mochitest/test_aria_token_attrs.html +++ b/accessible/tests/mochitest/test_aria_token_attrs.html @@ -32,18 +32,18 @@ https://bugzilla.mozilla.org/show_bug.cg testStates("button_pressed_false", STATE_CHECKABLE, 0, STATE_PRESSED); testStates("button_pressed_empty", 0, 0, STATE_PRESSED | STATE_CHECKABLE); testStates("button_pressed_undefined", 0, 0, STATE_PRESSED | STATE_CHECKABLE); testStates("button_pressed_absent", 0, 0, STATE_PRESSED | STATE_CHECKABLE); // test (checkbox) checkable and checked states testStates("checkbox_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); testStates("checkbox_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); - testStates("checkbox_checked_empty", 0 , 0, STATE_CHECKABLE | STATE_CHECKED); - testStates("checkbox_checked_undefined", 0, 0, STATE_CHECKABLE | STATE_CHECKED); + testStates("checkbox_checked_empty", STATE_CHECKABLE , 0, STATE_CHECKED); + testStates("checkbox_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED); testStates("checkbox_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED); // test native checkbox checked state and aria-checked state (if conflict, native wins) testStates("native_checkbox_nativechecked_ariatrue", (STATE_CHECKABLE | STATE_CHECKED)); testStates("native_checkbox_nativechecked_ariafalse", (STATE_CHECKABLE | STATE_CHECKED)); testStates("native_checkbox_nativechecked_ariaempty", (STATE_CHECKABLE | STATE_CHECKED)); testStates("native_checkbox_nativechecked_ariaundefined", (STATE_CHECKABLE | STATE_CHECKED)); testStates("native_checkbox_nativechecked_ariaabsent", (STATE_CHECKABLE | STATE_CHECKED)); @@ -101,39 +101,39 @@ https://bugzilla.mozilla.org/show_bug.cg testStates("menuitem_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); testStates("menuitem_checked_empty", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); testStates("menuitem_checked_undefined", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); testStates("menuitem_checked_absent", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); // test (menuitemradio) checkable and checked states testStates("menuitemradio_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); testStates("menuitemradio_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); - testStates("menuitemradio_checked_empty", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); - testStates("menuitemradio_checked_undefined", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); + testStates("menuitemradio_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("menuitemradio_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED); testStates("menuitemradio_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED); // test (radio) checkable and checked states testStates("radio_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); testStates("radio_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); - testStates("radio_checked_empty", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); - testStates("radio_checked_undefined", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); + testStates("radio_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("radio_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED); testStates("radio_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED); // test (textbox) multiline states testStates("textbox_multiline_true", 0, EXT_STATE_MULTI_LINE); testStates("textbox_multiline_false", 0, EXT_STATE_SINGLE_LINE); - testStates("textbox_multiline_empty", 0, 0, 0, EXT_STATE_SINGLE_LINE | EXT_STATE_MULTI_LINE); - testStates("textbox_multiline_undefined", 0, 0, 0, EXT_STATE_SINGLE_LINE | EXT_STATE_MULTI_LINE); + testStates("textbox_multiline_empty", 0, EXT_STATE_SINGLE_LINE); + testStates("textbox_multiline_undefined", 0, EXT_STATE_SINGLE_LINE); testStates("textbox_multiline_absent", 0, EXT_STATE_SINGLE_LINE); // test (textbox) readonly states testStates("textbox_readonly_true", STATE_READONLY); testStates("textbox_readonly_false", 0, EXT_STATE_EDITABLE, STATE_READONLY); - testStates("textbox_readonly_empty", 0, 0, STATE_READONLY, EXT_STATE_EDITABLE); - testStates("textbox_readonly_undefined", 0, 0, STATE_READONLY, EXT_STATE_EDITABLE); + testStates("textbox_readonly_empty", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("textbox_readonly_undefined", 0, EXT_STATE_EDITABLE, STATE_READONLY); testStates("textbox_readonly_absent", 0, EXT_STATE_EDITABLE, STATE_READONLY); // test native textbox readonly state and aria-readonly state (if conflict, native wins) testStates("native_textbox_nativereadonly_ariatrue", STATE_READONLY); testStates("native_textbox_nativereadonly_ariafalse", STATE_READONLY); testStates("native_textbox_nativereadonly_ariaempty", STATE_READONLY); testStates("native_textbox_nativereadonly_ariaundefined", STATE_READONLY); testStates("native_textbox_nativereadonly_ariaabsent", STATE_READONLY); @@ -142,18 +142,18 @@ https://bugzilla.mozilla.org/show_bug.cg testStates("native_textbox_nativeeditable_ariafalse", 0, 0, STATE_READONLY); testStates("native_textbox_nativeeditable_ariaempty", 0, 0, STATE_READONLY); testStates("native_textbox_nativeeditable_ariaundefined", 0, 0, STATE_READONLY); testStates("native_textbox_nativeeditable_ariaabsent", 0, 0, STATE_READONLY); // test (treeitem) selectable and selected states testStates("treeitem_selected_true", (STATE_SELECTABLE | STATE_SELECTED)); testStates("treeitem_selected_false", STATE_SELECTABLE, 0, STATE_SELECTED); - testStates("treeitem_selected_empty", 0, 0, (STATE_SELECTABLE | STATE_SELECTED)); - testStates("treeitem_selected_undefined", 0, 0, (STATE_SELECTABLE | STATE_SELECTED)); + testStates("treeitem_selected_empty", STATE_SELECTABLE, 0, STATE_SELECTED); + testStates("treeitem_selected_undefined", STATE_SELECTABLE, 0, STATE_SELECTED); testStates("treeitem_selected_absent", STATE_SELECTABLE, 0, STATE_SELECTED); // test (treeitem) haspopup states testStates("treeitem_haspopup_true", STATE_HASPOPUP); testStates("treeitem_haspopup_false", 0, 0, STATE_HASPOPUP); testStates("treeitem_haspopup_empty", 0, 0, STATE_HASPOPUP); testStates("treeitem_haspopup_undefined", 0, 0, STATE_HASPOPUP); testStates("treeitem_haspopup_absent", 0, 0, STATE_HASPOPUP);
--- a/addon-sdk/source/app-extension/bootstrap.js +++ b/addon-sdk/source/app-extension/bootstrap.js @@ -215,18 +215,16 @@ function startup(data, reasonCode) { loadReason: reason, prefixURI: prefixURI, // Add-on URI. rootURI: rootURI, // options used by system module. // File to write 'OK' or 'FAIL' (exit code emulation). resultFile: options.resultFile, - // File to write stdout. - logFile: options.logFile, // Arguments passed as --static-args staticArgs: options.staticArgs, // Arguments related to test runner. modules: { '@test/options': { allTestModules: options.allTestModules, iterations: options.iterations,
--- a/addon-sdk/source/app-extension/install.rdf +++ b/addon-sdk/source/app-extension/install.rdf @@ -12,18 +12,18 @@ <em:type>2</em:type> <em:bootstrap>true</em:bootstrap> <em:unpack>false</em:unpack> <!-- Firefox --> <em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> - <em:minVersion>19.0</em:minVersion> - <em:maxVersion>22.0a1</em:maxVersion> + <em:minVersion>21.0</em:minVersion> + <em:maxVersion>25.0a1</em:maxVersion> </Description> </em:targetApplication> <!-- Front End MetaData --> <em:name>Test App</em:name> <em:description>Harness for tests.</em:description> <em:creator>Mozilla Corporation</em:creator> <em:homepageURL></em:homepageURL>
--- a/addon-sdk/source/doc/dev-guide-source/cfx-tool.md +++ b/addon-sdk/source/doc/dev-guide-source/cfx-tool.md @@ -635,25 +635,24 @@ of `updateURL`. Note that as the [add-on documentation](https://developer.mozilla.org/en/extension_versioning,_update_and_compatibility#Securing_Updates) explains, you should make sure the update procedure for your add-on is secure, and this usually involves using HTTPS for the links. So if we run the following command: <pre> - cfx xpi --update-link https://example.com/addon/latest - --update-url https://example.com/addon/update_rdf + cfx xpi --update-link https://example.com/addon/latest/pluginName.xpi --update-url https://example.com/addon/update_rdf/pluginName.update.rdf </pre> `cfx` will create two files: * an XPI file which embeds -`https://example.com/addon/update_rdf` as the value of `updateURL` -* an RDF file which embeds `https://example.com/addon/latest` as the value of +`https://example.com/addon/update_rdf/pluginName.update.rdf` as the value of `updateURL` +* an RDF file which embeds `https://example.com/addon/latest/pluginName.xpi` as the value of `updateLink`. ### Supported Options ### As with `cfx run` you can point `cfx` at a different `package.json` file using the `--pkgdir` option. You can also embed arguments in the XPI using the `--static-args` option: if you do this the arguments will be passed to your add-on whenever it is run.
--- a/addon-sdk/source/doc/module-source/sdk/self.md +++ b/addon-sdk/source/doc/module-source/sdk/self.md @@ -8,16 +8,23 @@ The `self` module provides access to dat as a whole. It also provides access to the [Program ID](dev-guide/guides/program-id.html), a value which is unique for each add-on. Note that the `self` module is completely different from the global `self` object accessible to content scripts, which is used by a content script to [communicate with the add-on code](dev-guide/guides/content-scripts/using-port.html). +<api name="uri"> +@property {string} +This property represents an add-on associated unique URI string. +This URI can be used for APIs which require a valid URI string, such as the +[passwords](modules/sdk/passwords.html) module. +</api> + <api name="id"> @property {string} This property is a printable string that is unique for each add-on. It comes from the `id` property set in the `package.json` file in the main package (i.e. the package in which you run `cfx xpi`). While not generally of use to add-on code directly, it can be used by internal API code to index local storage and other resources that are associated with a particular add-on. Eventually, this ID will be unspoofable (see
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/doc/module-source/sdk/test/utils.md @@ -0,0 +1,90 @@ +<!-- 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/. --> + +The `test/utils` module provides additional helper methods to be used in +the CommonJS Unit Testing test suite. + +## Before and After + +Helper functions `before()` and `after()` are available for running a function +before or after each test in a suite. They're useful when you need to +guarantee a particular state before running a test, and to clean up +after your test. + + let { before, after } = require('sdk/test/utils'); + let { search } = require('sdk/places/bookmarks'); + + exports.testCountBookmarks = function (assert, done) { + search().on('end', function (results) { + assert.equal(results, 0, 'should be no bookmarks'); + done(); + }); + }; + + before(exports, function (name, assert) { + removeAllBookmarks(); + }); + + require('sdk/test').run(exports); + +Both `before` and `after` may be asynchronous. To make them asynchronous, +pass a third argument `done`, which is a function to call when you have +finished: + + let { before, after } = require('sdk/test/utils'); + let { search } = require('sdk/places/bookmarks'); + + exports.testCountBookmarks = function (assert, done) { + search().on('end', function (results) { + assert.equal(results, 0, 'should be no bookmarks'); + done(); + }); + }; + + before(exports, function (name, assert, done) { + removeAllBookmarksAsync(function () { + done(); + }); + }); + + require('sdk/test').run(exports); + +<api name="before"> +@function + Runs `beforeFn` before each test in the file. May be asynchronous + if `beforeFn` accepts a third argument, which is a callback. + + @param exports {Object} + A test file's `exports` object + @param beforeFn {Function} + The function to be called before each test. It has two arguments, + or three if it is asynchronous: + + * the first argument is the test's name as a `String`. + * the second argument is the `assert` object for the test. + * the third, optional, argument is a callback. If the callback is + defined, then the `beforeFn` is considered asynchronous, and the + callback must be invoked before the test runs. + +</api> + +<api name="after"> +@function + Runs `afterFn` after each test in the file. May be asynchronous + if `afterFn` accepts a third argument, which is a callback. + + @param exports {Object} + A test file's `exports` object + @param afterFn {Function} + The function to be called after each test. It has two arguments, + or three if it is asynchronous: + + * the first argument is the test's name as a `String`. + * the second argument is the `assert` object for the test. + * the third, optional, argument is a callback. If the callback is + defined, then the `afterFn` is considered asynchronous, and the + callback must be invoked before the next test runs. + +</api> +
--- a/addon-sdk/source/doc/module-source/sdk/util/array.md +++ b/addon-sdk/source/doc/module-source/sdk/util/array.md @@ -1,13 +1,13 @@ <!-- 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/. --> -The `util/array` module provides simple helper functions for working with +The `util/array` module provides simple helper functions for working with arrays. <api name="has"> @function Returns `true` if the given [`Array`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) contains the element or `false` otherwise. A simplified version of `array.indexOf(element) >= 0`. let { has } = require('sdk/util/array'); @@ -24,17 +24,17 @@ A simplified version of `array.indexOf(e The element to search for in the array. @returns {boolean} A boolean indicating whether or not the element was found in the array. </api> <api name="hasAny"> @function -Returns `true` if the given [`Array`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) contains any of the elements in the +Returns `true` if the given [`Array`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) contains any of the elements in the `elements` array, or `false` otherwise. let { hasAny } = require('sdk/util/array'); let a = ['rock', 'roll', 100]; hasAny(a, ['rock', 'bright', 'light']); // true hasAny(a, ['rush', 'coil', 'jet']); // false @@ -79,17 +79,17 @@ does not add the element and returns `fa If the given [array](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array) contains the given element, this function removes the element from the array and returns `true`. Otherwise, this function does not alter the array and returns `false`. let { remove } = require('sdk/util/array'); let a = ['alice', 'bob', 'carol']; remove(a, 'dave'); // false - remove(a, 'bob'); // true + remove(a, 'bob'); // true remove(a, 'bob'); // false console.log(a); // ['alice', 'carol'] @param array {array} The array to remove the element from. @param element {*} @@ -149,8 +149,29 @@ Iterates over an [iterator](https://deve @param iterator {iterator} The [`Iterator`](https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Iterators_and_Generators#Iterators) object over which to iterate and place results into an array. @returns {array} The iterator's results in an array. </api> +<api name="find"> +@function +Iterates over given `array` and applies given `predicate` function until +`predicate(element)` is `true`. If such element is found it's retured back +otherwise third optional `fallback` argument is returned back. If fallback +is not provided returns `undefined`. + + let { find } = require('sdk/util/array'); + let isOdd = (x) => x % 2; + find([2, 4, 5, 7, 8, 9], isOdd); // => 5 + find([2, 4, 6, 8], isOdd); // => undefiend + find([2, 4, 6, 8], isOdd, null); // => null + + fromIterator(i) // ['otoro', 'unagi', 'keon'] + +@param iterator {iterator} + The [`Iterator`](https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Iterators_and_Generators#Iterators) object over which to iterate and place results into an array. + +@returns {array} + The iterator's results in an array. +</api>
--- a/addon-sdk/source/lib/sdk/addon/installer.js +++ b/addon-sdk/source/lib/sdk/addon/installer.js @@ -1,14 +1,18 @@ /* 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/. */ module.metadata = { - "stability": "experimental" + "stability": "experimental", + "engines": { + // TODO Fennec Support in bug 894515 + "Firefox": "*" + } }; const { Cc, Ci, Cu } = require("chrome"); const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm"); const { defer } = require("../core/promise"); const { setTimeout } = require("../timers"); /**
--- a/addon-sdk/source/lib/sdk/clipboard.js +++ b/addon-sdk/source/lib/sdk/clipboard.js @@ -2,17 +2,21 @@ /* vim:set ts=2 sw=2 sts=2 et: */ /* 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/. */ "use strict"; module.metadata = { - "stability": "stable" + "stability": "stable", + "engines": { + // TODO Fennec Support 789757 + "Firefox": "*" + } }; const { Cc, Ci } = require("chrome"); const { DataURL } = require("./url"); const errors = require("./deprecated/errors"); const apiUtils = require("./deprecated/api-utils"); /* While these data flavors resemble Internet media types, they do
--- a/addon-sdk/source/lib/sdk/console/plain-text.js +++ b/addon-sdk/source/lib/sdk/console/plain-text.js @@ -3,134 +3,77 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; module.metadata = { "stability": "unstable" }; -const { Cc, Ci } = require("chrome"); +const { Cc, Ci, Cu, Cr } = require("chrome"); const self = require("../self"); -const traceback = require("./traceback") const prefs = require("../preferences/service"); const { merge } = require("../util/object"); -const { partial } = require("../lang/functional"); +const { ConsoleAPI } = Cu.import("resource://gre/modules/devtools/Console.jsm"); -const LEVELS = { - "all": Number.MIN_VALUE, - "debug": 20000, - "info": 30000, - "warn": 40000, - "error": 50000, - "off": Number.MAX_VALUE, -}; const DEFAULT_LOG_LEVEL = "error"; const ADDON_LOG_LEVEL_PREF = "extensions." + self.id + ".sdk.console.logLevel"; const SDK_LOG_LEVEL_PREF = "extensions.sdk.console.logLevel"; -let logLevel; - +let logLevel = DEFAULT_LOG_LEVEL; function setLogLevel() { - logLevel = prefs.get(ADDON_LOG_LEVEL_PREF, prefs.get(SDK_LOG_LEVEL_PREF, - DEFAULT_LOG_LEVEL)); + logLevel = prefs.get(ADDON_LOG_LEVEL_PREF, + prefs.get(SDK_LOG_LEVEL_PREF, + DEFAULT_LOG_LEVEL)); } setLogLevel(); let logLevelObserver = { + QueryInterface: function(iid) { + if (!iid.equals(Ci.nsIObserver) && + !iid.equals(Ci.nsISupportsWeakReference) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, observe: function(subject, topic, data) { setLogLevel(); } }; let branch = Cc["@mozilla.org/preferences-service;1"]. getService(Ci.nsIPrefService). getBranch(null); -branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, false); -branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, false); - -function stringify(arg) { - try { - return String(arg); - } - catch(ex) { - return "<toString() error>"; - } -} - -function stringifyArgs(args) { - return Array.map(args, stringify).join(" "); -} - -function message(print, level) { - if (LEVELS[level] < LEVELS[logLevel]) - return; - - let args = Array.slice(arguments, 2); - - print(level + ": " + self.name + ": " + stringifyArgs(args) + "\n", level); -} - -function errorMessage(print, e) { - // Some platform exception doesn't have name nor message but - // can be stringified to a meaningfull message - var fullString = ("An exception occurred.\n" + - (e.name ? e.name + ": " : "") + - (e.message ? e.message : e.toString()) + "\n" + - (e.fileName ? traceback.sourceURI(e.fileName) + " " + - e.lineNumber + "\n" - : "") + - traceback.format(e)); - - message(print, "error", fullString); -} - -function traceMessage(print) { - var stack = traceback.get(); - stack.splice(-1, 1); - - message(print, "info", traceback.format(stack)); -} +branch.addObserver(ADDON_LOG_LEVEL_PREF, logLevelObserver, true); +branch.addObserver(SDK_LOG_LEVEL_PREF, logLevelObserver, true); function PlainTextConsole(print) { - if (!print) - print = dump; - - if (print === dump) { - // If we're just using dump(), auto-enable preferences so - // that the developer actually sees the console output. - var prefs = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefBranch); - prefs.setBoolPref("browser.dom.window.dump.enabled", true); - } - merge(this, { - log: partial(message, print, "info"), - info: partial(message, print, "info"), - warn: partial(message, print, "warn"), - error: partial(message, print, "error"), - debug: partial(message, print, "debug"), - exception: partial(errorMessage, print), - trace: partial(traceMessage, print), + let consoleOptions = { + prefix: self.name + ": ", + maxLogLevel: logLevel, + dump: print + }; + let console = new ConsoleAPI(consoleOptions); - dir: function dir() {}, - group: function group() {}, - groupCollapsed: function groupCollapsed() {}, - groupEnd: function groupEnd() {}, - time: function time() {}, - timeEnd: function timeEnd() {} + // As we freeze the console object, we can't modify this property afterward + Object.defineProperty(console, "maxLogLevel", { + get: function() { + return logLevel; + } }); // We defined the `__exposedProps__` in our console chrome object. // Although it seems redundant, because we use `createObjectIn` too, in // worker.js, we are following what `ConsoleAPI` does. See: // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#132 // // Meanwhile we're investigating with the platform team if `__exposedProps__` // are needed, or are just a left-over. - this.__exposedProps__ = Object.keys(this).reduce(function(exposed, prop) { + console.__exposedProps__ = Object.keys(ConsoleAPI.prototype).reduce(function(exposed, prop) { exposed[prop] = "r"; return exposed; }, {}); - Object.freeze(this); + Object.freeze(console); + return console; }; exports.PlainTextConsole = PlainTextConsole;
--- a/addon-sdk/source/lib/sdk/context-menu.js +++ b/addon-sdk/source/lib/sdk/context-menu.js @@ -1,15 +1,19 @@ /* 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/. */ "use strict"; module.metadata = { - "stability": "stable" + "stability": "stable", + "engines": { + // TODO Fennec support Bug 788334 + "Firefox": "*" + } }; const { Class, mix } = require("./core/heritage"); const { addCollectionProperty } = require("./util/collection"); const { ns } = require("./core/namespace"); const { validateOptions, getTypeOf } = require("./deprecated/api-utils"); const { URL, isValidURI } = require("./url"); const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils");
--- a/addon-sdk/source/lib/sdk/indexed-db.js +++ b/addon-sdk/source/lib/sdk/indexed-db.js @@ -48,15 +48,8 @@ let principal = Cc["@mozilla.org/scripts exports.indexedDB = Object.freeze({ open: indexedDB.openForPrincipal.bind(indexedDB, principal), deleteDatabase: indexedDB.deleteForPrincipal.bind(indexedDB, principal), cmp: indexedDB.cmp }); exports.IDBKeyRange = IDBKeyRange; exports.DOMException = Ci.nsIDOMDOMException; -exports.IDBCursor = Ci.nsIIDBCursor; -exports.IDBTransaction = Ci.nsIIDBTransaction; -exports.IDBOpenDBRequest = Ci.nsIIDBOpenDBRequest; -exports.IDBDatabase = Ci.nsIIDBDatabase; -exports.IDBIndex = Ci.nsIIDBIndex; -exports.IDBObjectStore = Ci.nsIIDBObjectStore; -exports.IDBRequest = Ci.nsIIDBRequest;
--- a/addon-sdk/source/lib/sdk/io/buffer.js +++ b/addon-sdk/source/lib/sdk/io/buffer.js @@ -4,79 +4,257 @@ */ "use strict"; module.metadata = { "stability": "experimental" }; -const { Cc, Ci, CC } = require("chrome"); -const { Class } = require("../core/heritage"); +const { Cu } = require("chrome"); +const { TextEncoder, TextDecoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}); -const Transcoder = CC("@mozilla.org/intl/scriptableunicodeconverter", - "nsIScriptableUnicodeConverter"); +exports.TextEncoder = TextEncoder; +exports.TextDecoder = TextDecoder; + -var Buffer = Class({ - initialize: function initialize(subject, encoding) { - subject = subject ? subject.valueOf() : 0; - let length = typeof subject === "number" ? subject : 0; - this.encoding = encoding || "utf-8"; - this.valueOf(Array.isArray(subject) ? subject : new Array(length)); +function Buffer(subject, encoding) { + var type = typeof(subject); + switch (type) { + case "number": + // Create typed array of the given size if number. + return Uint8Array(subject > 0 ? Math.floor(subject) : 0); + case "string": + // If string encode it and use buffer for the returned Uint8Array + // to create a local patched version that acts like node buffer. + encoding = encoding || "utf8"; + return Uint8Array(TextEncoder(encoding).encode(subject).buffer); + case "object": + // If array or alike just make a copy with a local patched prototype. + return Uint8Array(subject); + default: + throw new TypeError("must start with number, buffer, array or string"); + } +} +exports.Buffer = Buffer; + +// Tests if `value` is a Buffer. +Buffer.isBuffer = value => value instanceof Buffer + +// Returns true if the encoding is a valid encoding argument & false otherwise +Buffer.isEncoding = encoding => !!ENCODINGS[String(encoding).toLowerCase()] + +// Gives the actual byte length of a string. encoding defaults to 'utf8'. +// This is not the same as String.prototype.length since that returns the +// number of characters in a string. +Buffer.byteLength = (value, encoding = "utf8") => + TextEncoder(encoding).encode(value).byteLength - if (typeof subject === "string") this.write(subject); - }, - get length() { - return this.valueOf().length; - }, - get: function get(index) { - return this.valueOf()[index]; - }, - set: function set(index, value) { - return this.valueOf()[index] = value; - }, - valueOf: function valueOf(value) { - Object.defineProperty(this, "valueOf", { - value: Array.prototype.valueOf.bind(value), - configurable: false, - writable: false, - enumerable: false - }); +// Direct copy of the nodejs's buffer implementation: +// https://github.com/joyent/node/blob/b255f4c10a80343f9ce1cee56d0288361429e214/lib/buffer.js#L146-L177 +Buffer.concat = function(list, length) { + if (!Array.isArray(list)) + throw new TypeError('Usage: Buffer.concat(list[, length])'); + + if (typeof length === 'undefined') { + length = 0; + for (var i = 0; i < list.length; i++) + length += list[i].length; + } else { + length = ~~length; + } + + if (length < 0) + length = 0; + + if (list.length === 0) + return new Buffer(0); + else if (list.length === 1) + return list[0]; + + if (length < 0) + throw new RangeError('length is not a positive number'); + + var buffer = new Buffer(length); + var pos = 0; + for (var i = 0; i < list.length; i++) { + var buf = list[i]; + buf.copy(buffer, pos); + pos += buf.length; + } + + return buffer; +}; + +// Node buffer is very much like Uint8Array although it has bunch of methods +// that typically can be used in combination with `DataView` while preserving +// access by index. Since in SDK echo module has it's own set of bult-ins it +// ok to patch ours to make it nodejs Buffer compatible. +Buffer.prototype = Uint8Array.prototype; +Object.defineProperties(Buffer.prototype, { + view: { + get: function() this._view || (this._view = DataView(this.buffer)) }, - toString: function toString(encoding, start, end) { - let bytes = this.valueOf().slice(start || 0, end || this.length); - let transcoder = Transcoder(); - transcoder.charset = String(encoding || this.encoding).toUpperCase(); - return transcoder.convertFromByteArray(bytes, this.length); + toString: { + value: function(encoding, start, end) { + encoding = !!encoding ? (encoding + '').toLowerCase() : "utf8"; + start = Math.max(0, ~~start); + end = Math.min(this.length, end === void(0) ? this.length : ~~end); + return TextDecoder(encoding).decode(this.subarray(start, end)); + } + }, + toJSON: { + value: function() ({ type: "Buffer", data: Array.slice(this, 0) }) }, - toJSON: function toJSON() { - return this.toString() + get: { + value: function(offset) this[offset] + }, + set: { + value: function(offset, value) this[offset] = value + }, + copy: { + value: function(target, offset, start, end) + Uint8Array.set(target, this.subarray(start, end), offset) + }, + slice: { + value: Buffer.prototype.subarray }, - write: function write(string, offset, encoding) { - offset = Math.max(offset || 0, 0); - let value = this.valueOf(); - let transcoder = Transcoder(); - transcoder.charset = String(encoding || this.encoding).toUpperCase(); - let bytes = transcoder.convertToByteArray(string, {}); - value.splice.apply(value, [ - offset, - Math.min(value.length - offset, bytes.length, bytes) - ].concat(bytes)); - return bytes; + write: { + value: function(string, offset, length, encoding = "utf8") { + if (typeof(offset) === "string") + ([offset, length, encoding]) = [0, null, offset]; + else if (typeof(length) === "string") + ([length, encoding]) = [null, length]; + + offset = ~~offset; + length = length || this.length - offset; + let buffer = TextEncoder(encoding).encode(string); + let result = Math.min(buffer.length, length); + if (buffer.length !== length) + buffer = buffer.subarray(0, length); + Uint8Array.set(this, buffer, offset); + return result; + } }, - slice: function slice(start, end) { - return new Buffer(this.valueOf().slice(start, end)); - }, - copy: function copy(target, offset, start, end) { - offset = Math.max(offset || 0, 0); - target = target.valueOf(); - let bytes = this.valueOf(); - bytes.slice(Math.max(start || 0, 0), end); - target.splice.apply(target, [ - offset, - Math.min(target.length - offset, bytes.length), - ].concat(bytes)); + fill: { + value: function fill(value, start, end) { + value = value || 0; + start = start || 0; + end = end || this.length; + + if (typeof(value) === "string") + value = value.charCodeAt(0); + if (typeof(value) !== "number" || isNaN(value)) + throw TypeError("value is not a number"); + if (end < start) + throw new RangeError("end < start"); + + // Fill 0 bytes; we're done + if (end === start) + return 0; + if (this.length == 0) + return 0; + + if (start < 0 || start >= this.length) + throw RangeError("start out of bounds"); + + if (end < 0 || end > this.length) + throw RangeError("end out of bounds"); + + let index = start; + while (index < end) this[index++] = value; + } } }); -Buffer.isBuffer = function isBuffer(buffer) { - return buffer instanceof Buffer -}; -exports.Buffer = Buffer; + +// Define nodejs Buffer's getter and setter functions that just proxy +// to internal DataView's equivalent methods. +[["readUInt16LE", "getUint16", true], + ["readUInt16BE", "getUint16", false], + ["readInt16LE", "getInt16", true], + ["readInt16BE", "getInt16", false], + ["readUInt32LE", "getInt32", true], + ["readUInt32BE", "getInt32", false], + ["readInt32LE", "getInt32", true], + ["readInt32BE", "getInt32", false], + ["readFloatLE", "getFloat32", true], + ["readFloatBE", "getFloat32", false], + ["readDoubleLE", "getFloat64", true], + ["readDoubleBE", "getFloat64", false], + ["readUInt8", "getUint8"], + ["readInt8", "getInt8"]].forEach(([alias, name, littleEndian]) => { + Object.defineProperty(Buffer.prototype, alias, { + value: function(offset) this.view[name](offset, littleEndian) + }); +}); + +[["writeUInt16LE", "setUint16", true], + ["writeUInt16BE", "setUint16", false], + ["writeInt16LE", "setInt16", true], + ["writeInt16BE", "setInt16", false], + ["writeUInt32LE", "setUint32", true], + ["writeUInt32BE", "setUint32", false], + ["writeInt32LE", "setInt32", true], + ["writeInt32BE", "setInt32", false], + ["writeFloatLE", "setFloat32", true], + ["writeFloatBE", "setFloat32", false], + ["writeDoubleLE", "setFloat64", true], + ["writeDoubleBE", "setFloat64", false], + ["writeUInt8", "setUint8"], + ["writeInt8", "setInt8"]].forEach(([alias, name, littleEndian]) => { + Object.defineProperty(Buffer.prototype, alias, { + value: function(value, offset) this.view[name](offset, value, littleEndian) + }); +}); + + +// List of supported encodings taken from: +// http://mxr.mozilla.org/mozilla-central/source/dom/encoding/labelsencodings.properties +const ENCODINGS = { "unicode-1-1-utf-8": 1, "utf-8": 1, "utf8": 1, + "866": 1, "cp866": 1, "csibm866": 1, "ibm866": 1, "csisolatin2": 1, + "iso-8859-2": 1, "iso-ir-101": 1, "iso8859-2": 1, "iso88592": 1, + "iso_8859-2": 1, "iso_8859-2:1987": 1, "l2": 1, "latin2": 1, "csisolatin3": 1, + "iso-8859-3": 1, "iso-ir-109": 1, "iso8859-3": 1, "iso88593": 1, + "iso_8859-3": 1, "iso_8859-3:1988": 1, "l3": 1, "latin3": 1, "csisolatin4": 1, + "iso-8859-4": 1, "iso-ir-110": 1, "iso8859-4": 1, "iso88594": 1, + "iso_8859-4": 1, "iso_8859-4:1988": 1, "l4": 1, "latin4": 1, + "csisolatincyrillic": 1, "cyrillic": 1, "iso-8859-5": 1, "iso-ir-144": 1, + "iso8859-5": 1, "iso88595": 1, "iso_8859-5": 1, "iso_8859-5:1988": 1, + "arabic": 1, "asmo-708": 1, "csiso88596e": 1, "csiso88596i": 1, + "csisolatinarabic": 1, "ecma-114": 1, "iso-8859-6": 1, "iso-8859-6-e": 1, + "iso-8859-6-i": 1, "iso-ir-127": 1, "iso8859-6": 1, "iso88596": 1, + "iso_8859-6": 1, "iso_8859-6:1987": 1, "csisolatingreek": 1, "ecma-118": 1, + "elot_928": 1, "greek": 1, "greek8": 1, "iso-8859-7": 1, "iso-ir-126": 1, + "iso8859-7": 1, "iso88597": 1, "iso_8859-7": 1, "iso_8859-7:1987": 1, + "sun_eu_greek": 1, "csiso88598e": 1, "csisolatinhebrew": 1, "hebrew": 1, + "iso-8859-8": 1, "iso-8859-8-e": 1, "iso-ir-138": 1, "iso8859-8": 1, + "iso88598": 1, "iso_8859-8": 1, "iso_8859-8:1988": 1, "visual": 1, + "csiso88598i": 1, "iso-8859-8-i": 1, "logical": 1, "csisolatin6": 1, + "iso-8859-10": 1, "iso-ir-157": 1, "iso8859-10": 1, "iso885910": 1, + "l6": 1, "latin6": 1, "iso-8859-13": 1, "iso8859-13": 1, "iso885913": 1, + "iso-8859-14": 1, "iso8859-14": 1, "iso885914": 1, "csisolatin9": 1, + "iso-8859-15": 1, "iso8859-15": 1, "iso885915": 1, "iso_8859-15": 1, + "l9": 1, "iso-8859-16": 1, "cskoi8r": 1, "koi": 1, "koi8": 1, "koi8-r": 1, + "koi8_r": 1, "koi8-u": 1, "csmacintosh": 1, "mac": 1, "macintosh": 1, + "x-mac-roman": 1, "dos-874": 1, "iso-8859-11": 1, "iso8859-11": 1, + "iso885911": 1, "tis-620": 1, "windows-874": 1, "cp1250": 1, + "windows-1250": 1, "x-cp1250": 1, "cp1251": 1, "windows-1251": 1, + "x-cp1251": 1, "ansi_x3.4-1968": 1, "ascii": 1, "cp1252": 1, "cp819": 1, + "csisolatin1": 1, "ibm819": 1, "iso-8859-1": 1, "iso-ir-100": 1, + "iso8859-1": 1, "iso88591": 1, "iso_8859-1": 1, "iso_8859-1:1987": 1, + "l1": 1, "latin1": 1, "us-ascii": 1, "windows-1252": 1, "x-cp1252": 1, + "cp1253": 1, "windows-1253": 1, "x-cp1253": 1, "cp1254": 1, "csisolatin5": 1, + "iso-8859-9": 1, "iso-ir-148": 1, "iso8859-9": 1, "iso88599": 1, + "iso_8859-9": 1, "iso_8859-9:1989": 1, "l5": 1, "latin5": 1, + "windows-1254": 1, "x-cp1254": 1, "cp1255": 1, "windows-1255": 1, + "x-cp1255": 1, "cp1256": 1, "windows-1256": 1, "x-cp1256": 1, "cp1257": 1, + "windows-1257": 1, "x-cp1257": 1, "cp1258": 1, "windows-1258": 1, + "x-cp1258": 1, "x-mac-cyrillic": 1, "x-mac-ukrainian": 1, "chinese": 1, + "csgb2312": 1, "csiso58gb231280": 1, "gb2312": 1, "gb_2312": 1, + "gb_2312-80": 1, "gbk": 1, "iso-ir-58": 1, "x-gbk": 1, "gb18030": 1, + "hz-gb-2312": 1, "big5": 1, "big5-hkscs": 1, "cn-big5": 1, "csbig5": 1, + "x-x-big5": 1, "cseucpkdfmtjapanese": 1, "euc-jp": 1, "x-euc-jp": 1, + "csiso2022jp": 1, "iso-2022-jp": 1, "csshiftjis": 1, "ms_kanji": 1, + "shift-jis": 1, "shift_jis": 1, "sjis": 1, "windows-31j": 1, "x-sjis": 1, + "cseuckr": 1, "csksc56011987": 1, "euc-kr": 1, "iso-ir-149": 1, "korean": 1, + "ks_c_5601-1987": 1, "ks_c_5601-1989": 1, "ksc5601": 1, "ksc_5601": 1, + "windows-949": 1, "csiso2022kr": 1, "iso-2022-kr": 1, "utf-16": 1, + "utf-16le": 1, "utf-16be": 1, "x-user-defined": 1 };
--- a/addon-sdk/source/lib/sdk/io/fs.js +++ b/addon-sdk/source/lib/sdk/io/fs.js @@ -7,20 +7,22 @@ module.metadata = { "stability": "experimental" }; const { Cc, Ci, CC } = require("chrome"); const { setTimeout } = require("../timers"); const { Stream, InputStream, OutputStream } = require("./stream"); +const { emit, on } = require("../event/core"); const { Buffer } = require("./buffer"); const { ns } = require("../core/namespace"); const { Class } = require("../core/heritage"); + const nsILocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath"); const FileOutputStream = CC("@mozilla.org/network/file-output-stream;1", "nsIFileOutputStream", "init"); const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", "nsIFileInputStream", "init"); const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"); @@ -28,16 +30,18 @@ const BinaryOutputStream = CC("@mozilla. "nsIBinaryOutputStream", "setOutputStream"); const StreamPump = CC("@mozilla.org/network/input-stream-pump;1", "nsIInputStreamPump", "init"); const { createOutputTransport, createInputTransport } = Cc["@mozilla.org/network/stream-transport-service;1"]. getService(Ci.nsIStreamTransportService); +const { OPEN_UNBUFFERED } = Ci.nsITransport; + const { REOPEN_ON_REWIND, DEFER_OPEN } = Ci.nsIFileInputStream; const { DIRECTORY_TYPE, NORMAL_FILE_TYPE } = Ci.nsIFile; const { NS_SEEK_SET, NS_SEEK_CUR, NS_SEEK_END } = Ci.nsISeekableStream; const FILE_PERMISSION = parseInt("0666", 8); const PR_UINT32_MAX = 0xfffffff; // Values taken from: @@ -152,24 +156,26 @@ const ReadStream = Class({ input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); // We use `nsIStreamTransportService` service to transform blocking // file input stream into a fully asynchronous stream that can be written // without blocking the main thread. let transport = createInputTransport(input, position, length, false); // Open an input stream on a transport. We don"t pass flags to guarantee // non-blocking stream semantics. Also we use defaults for segment size & // count. - let asyncInputStream = transport.openInputStream(null, 0, 0); - let binaryInputStream = BinaryInputStream(asyncInputStream); - nsIBinaryInputStream(fd, binaryInputStream); - let pump = StreamPump(asyncInputStream, position, length, 0, 0, false); + InputStream.prototype.initialize.call(this, { + asyncInputStream: transport.openInputStream(null, 0, 0) + }); - InputStream.prototype.initialize.call(this, { - input: binaryInputStream, pump: pump + // Close file descriptor on end and destroy the stream. + on(this, "end", _ => { + this.destroy(); + emit(this, "close"); }); + this.read(); }, destroy: function() { closeSync(this.fd); InputStream.prototype.destroy.call(this); } }); exports.ReadStream = ReadStream; @@ -206,31 +212,30 @@ const WriteStream = Class({ output.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); // We use `nsIStreamTransportService` service to transform blocking // file output stream into a fully asynchronous stream that can be written // without blocking the main thread. let transport = createOutputTransport(output, position, -1, false); // Open an output stream on a transport. We don"t pass flags to guarantee // non-blocking stream semantics. Also we use defaults for segment size & // count. - let asyncOutputStream = transport.openOutputStream(null, 0, 0); - // Finally we create a non-blocking binary output stream. This will allows - // us to write buffers as byte arrays without any further transcoding. - let binaryOutputStream = BinaryOutputStream(asyncOutputStream); - nsIBinaryOutputStream(fd, binaryOutputStream); + OutputStream.prototype.initialize.call(this, { + asyncOutputStream: transport.openOutputStream(OPEN_UNBUFFERED, 0, 0), + output: output + }); - // Storing output stream so that it can beaccessed later. - OutputStream.prototype.initialize.call(this, { - output: binaryOutputStream, - asyncOutputStream: asyncOutputStream + // For write streams "finish" basically means close. + on(this, "finish", _ => { + this.destroy(); + emit(this, "close"); }); }, destroy: function() { + OutputStream.prototype.destroy.call(this); closeSync(this.fd); - OutputStream.prototype.destroy.call(this); } }); exports.WriteStream = WriteStream; exports.createWriteStream = function createWriteStream(path, options) { return new WriteStream(path, options); }; const Stats = Class({ @@ -360,17 +365,17 @@ exports.truncate = truncate; function ftruncate(fd, length, callback) { write(fd, new Buffer(length), 0, length, 0, function(error) { callback(error); }); } exports.ftruncate = ftruncate; -function ftruncateSync(fd, length) { +function ftruncateSync(fd, length = 0) { writeSync(fd, new Buffer(length), 0, length, 0); } exports.ftruncateSync = ftruncateSync; function chownSync(path, uid, gid) { throw Error("Not implemented yet!!"); } exports.chownSync = chownSync; @@ -629,16 +634,18 @@ exports.close = close; /** * Synchronous open(2). */ function openSync(path, flags, mode) { let [ fd, flags, mode, file ] = [ { path: path }, Flags(flags), Mode(mode), nsILocalFile(path) ]; + nsIFile(fd, file); + // If trying to open file for just read that does not exists // need to throw exception as node does. if (!file.exists() && !isWritable(flags)) throw FSError("open", "ENOENT", 34, path); // If we want to open file in read mode we initialize input stream. if (isReadable(flags)) { let input = FileInputStream(file, flags, mode, DEFER_OPEN); @@ -670,17 +677,19 @@ function writeSync(fd, buffer, offset, l if (length + offset > buffer.length) { throw Error("Length is extends beyond buffer"); } else if (length + offset !== buffer.length) { buffer = buffer.slice(offset, offset + length); } let writeStream = new WriteStream(fd, { position: position, length: length }); - let output = nsIBinaryOutputStream(fd); + + let output = BinaryOutputStream(nsIFileOutputStream(fd)); + nsIBinaryOutputStream(fd, output); // We write content as a byte array as this will avoid any transcoding // if content was a buffer. output.writeByteArray(buffer.valueOf(), buffer.length); output.flush(); }; exports.writeSync = writeSync; /** @@ -731,20 +740,24 @@ function readSync(fd, buffer, offset, le // Setting a stream position, unless it"s `-1` which means current position. if (position >= 0) input.QueryInterface(Ci.nsISeekableStream).seek(NS_SEEK_SET, position); // We use `nsIStreamTransportService` service to transform blocking // file input stream into a fully asynchronous stream that can be written // without blocking the main thread. let binaryInputStream = BinaryInputStream(input); let count = length === ALL ? binaryInputStream.available() : length; - var bytes = binaryInputStream.readByteArray(count); - buffer.copy.call(bytes, buffer, offset); + if (offset === 0) binaryInputStream.readArrayBuffer(count, buffer.buffer); + else { + let chunk = new Buffer(count); + binaryInputStream.readArrayBuffer(count, chunk.buffer); + chunk.copy(buffer, offset); + } - return bytes; + return buffer.slice(offset, offset + count); }; exports.readSync = readSync; /** * Read data from the file specified by `fd`. * * `buffer` is the buffer that the data will be written to. * `offset` is offset within the buffer where writing will start. @@ -754,19 +767,19 @@ exports.readSync = readSync; * `position` is an integer specifying where to begin reading from in the file. * If `position` is `null`, data will be read from the current file position. * * The callback is given the three arguments, `(error, bytesRead, buffer)`. */ function read(fd, buffer, offset, length, position, callback) { let bytesRead = 0; let readStream = new ReadStream(fd, { position: position, length: length }); - readStream.on("data", function onData(chunck) { - chunck.copy(buffer, offset + bytesRead); - bytesRead += chunck.length; + readStream.on("data", function onData(data) { + data.copy(buffer, offset + bytesRead); + bytesRead += data.length; }); readStream.on("end", function onEnd() { callback(null, bytesRead, buffer); readStream.destroy(); }); }; exports.read = read; @@ -776,44 +789,52 @@ exports.read = read; * contents of the file. */ function readFile(path, encoding, callback) { if (isFunction(encoding)) { callback = encoding encoding = null } - let buffer = new Buffer(); + let buffer = null; try { let readStream = new ReadStream(path); - readStream.on("data", function(chunck) { - chunck.copy(buffer, buffer.length); + readStream.on("data", function(data) { + if (!buffer) buffer = data; + else { + let bufferred = buffer + buffer = new Buffer(buffer.length + data.length); + bufferred.copy(buffer, 0); + data.copy(buffer, bufferred.length); + } }); readStream.on("error", function onError(error) { callback(error); - readStream.destroy(); }); readStream.on("end", function onEnd() { + // Note: Need to destroy before invoking a callback + // so that file descriptor is released. + readStream.destroy(); callback(null, buffer); - readStream.destroy(); }); } catch (error) { setTimeout(callback, 0, error); } }; exports.readFile = readFile; /** * Synchronous version of `fs.readFile`. Returns the contents of the path. * If encoding is specified then this function returns a string. * Otherwise it returns a buffer. */ function readFileSync(path, encoding) { - let buffer = new Buffer(); let fd = openSync(path, "r"); + let size = fstatSync(fd).size; + let buffer = new Buffer(size); try { readSync(fd, buffer, 0, ALL, 0); } finally { closeSync(fd); } return buffer; }; @@ -828,23 +849,26 @@ function writeFile(path, content, encodi if (isFunction(encoding)) { callback = encoding encoding = null } if (isString(content)) content = new Buffer(content, encoding); let writeStream = new WriteStream(path); - writeStream.on("error", function onError(error) { + let error = null; + + writeStream.end(content, function() { + writeStream.destroy(); callback(error); - writeStream.destroy(); }); - writeStream.write(content, function onDrain() { + + writeStream.on("error", function onError(reason) { + error = reason; writeStream.destroy(); - callback(null); }); } catch (error) { callback(error); } }; exports.writeFile = writeFile; /**
--- a/addon-sdk/source/lib/sdk/io/stream.js +++ b/addon-sdk/source/lib/sdk/io/stream.js @@ -3,109 +3,55 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; module.metadata = { "stability": "experimental" }; +const { CC, Cc, Ci, Cu, Cr, components } = require("chrome"); const { EventTarget } = require("../event/target"); const { emit } = require("../event/core"); const { Buffer } = require("./buffer"); const { Class } = require("../core/heritage"); const { setTimeout } = require("../timers"); -const { ns } = require("../core/namespace"); + + +const MultiplexInputStream = CC("@mozilla.org/io/multiplex-input-stream;1", + "nsIMultiplexInputStream"); +const AsyncStreamCopier = CC("@mozilla.org/network/async-stream-copier;1", + "nsIAsyncStreamCopier", "init"); +const StringInputStream = CC("@mozilla.org/io/string-input-stream;1", + "nsIStringInputStream"); +const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1", + "nsIArrayBufferInputStream"); -function isFunction(value) typeof value === "function" +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); +const InputStreamPump = CC("@mozilla.org/network/input-stream-pump;1", + "nsIInputStreamPump", "init"); + +const threadManager = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager); + +const eventTarget = Cc["@mozilla.org/network/socket-transport-service;1"]. + getService(Ci.nsIEventTarget); + +let isFunction = value => typeof(value) === "function" function accessor() { let map = new WeakMap(); - return function(fd, value) { - if (value === null) map.delete(fd); - if (value !== undefined) map.set(fd, value); - return map.get(fd); + return function(target, value) { + if (value) + map.set(target, value); + return map.get(target); } } -let nsIInputStreamPump = accessor(); -let nsIAsyncOutputStream = accessor(); -let nsIInputStream = accessor(); -let nsIOutputStream = accessor(); - - -/** - * Utility function / hack that we use to figure if output stream is closed. - */ -function isClosed(stream) { - // We assume that stream is not closed. - let isClosed = false; - stream.asyncWait({ - // If `onClose` callback is called before outer function returns - // (synchronously) `isClosed` will be set to `true` identifying - // that stream is closed. - onOutputStreamReady: function onClose() isClosed = true - - // `WAIT_CLOSURE_ONLY` flag overrides the default behavior, causing the - // `onOutputStreamReady` notification to be suppressed until the stream - // becomes closed. - }, stream.WAIT_CLOSURE_ONLY, 0, null); - return isClosed; -} -/** - * Utility function takes output `stream`, `onDrain`, `onClose` callbacks and - * calls one of this callbacks depending on stream state. It is guaranteed - * that only one called will be called and it will be called asynchronously. - * @param {nsIAsyncOutputStream} stream - * @param {Function} onDrain - * callback that is called when stream becomes writable. - * @param {Function} onClose - * callback that is called when stream becomes closed. - */ -function onStateChange(stream, target) { - let isAsync = false; - stream.asyncWait({ - onOutputStreamReady: function onOutputStreamReady() { - // If `isAsync` was not yet set to `true` by the last line we know that - // `onOutputStreamReady` was called synchronously. In such case we just - // defer execution until next turn of event loop. - if (!isAsync) - return setTimeout(onOutputStreamReady, 0); - - // As it"s not clear what is a state of the stream (TODO: Is there really - // no better way ?) we employ hack (see details in `isClosed`) to verify - // if stream is closed. - emit(target, isClosed(stream) ? "close" : "drain"); - } - }, 0, 0, null); - isAsync = true; -} - -function pump(stream) { - let input = nsIInputStream(stream); - nsIInputStreamPump(stream).asyncRead({ - onStartRequest: function onStartRequest() { - emit(stream, "start"); - }, - onDataAvailable: function onDataAvailable(req, c, is, offset, count) { - try { - let bytes = input.readByteArray(count); - emit(stream, "data", new Buffer(bytes, stream.encoding)); - } catch (error) { - emit(stream, "error", error); - stream.readable = false; - } - }, - onStopRequest: function onStopRequest() { - stream.readable = false; - emit(stream, "end"); - } - }, null); -} - const Stream = Class({ extends: EventTarget, initialize: function() { this.readable = false; this.writable = false; this.encoding = null; }, setEncoding: function setEncoding(encoding) { @@ -115,17 +61,18 @@ const Stream = Class({ let source = this; function onData(chunk) { if (target.writable) { if (false === target.write(chunk)) source.pause(); } } function onDrain() { - if (source.readable) source.resume(); + if (source.readable) + source.resume(); } function onEnd() { target.end(); } function onPause() { source.pause(); } function onResume() { @@ -171,154 +118,320 @@ const Stream = Class({ emit(this, "resume"); }, destroySoon: function destroySoon() { this.destroy(); } }); exports.Stream = Stream; + +let nsIStreamListener = accessor(); +let nsIInputStreamPump = accessor(); +let nsIAsyncInputStream = accessor(); +let nsIBinaryInputStream = accessor(); + +const StreamListener = Class({ + initialize: function(stream) { + this.stream = stream; + }, + + // Next three methods are part of `nsIStreamListener` interface and are + // invoked by `nsIInputStreamPump.asyncRead`. + onDataAvailable: function(request, context, input, offset, count) { + let stream = this.stream; + let buffer = new ArrayBuffer(count); + nsIBinaryInputStream(stream).readArrayBuffer(count, buffer); + emit(stream, "data", new Buffer(buffer, stream.encoding)); + }, + + // Next two methods implement `nsIRequestObserver` interface and are invoked + // by `nsIInputStreamPump.asyncRead`. + onStartRequest: function() {}, + // Called to signify the end of an asynchronous request. We only care to + // discover errors. + onStopRequest: function(request, context, status) { + let stream = this.stream; + stream.readable = false; + if (!components.isSuccessCode(status)) + emit(stream, "error", status); + else + emit(stream, "end"); + } +}); + + const InputStream = Class({ extends: Stream, + readable: false, + paused: false, initialize: function initialize(options) { - let { input, pump } = options; + let { asyncInputStream } = options; this.readable = true; - this.paused = false; - nsIInputStream(this, input); - nsIInputStreamPump(this, pump); + + let binaryInputStream = new BinaryInputStream(asyncInputStream); + let inputStreamPump = new InputStreamPump(asyncInputStream, + -1, -1, 0, 0, false); + let streamListener = new StreamListener(this); + + nsIAsyncInputStream(this, asyncInputStream); + nsIInputStreamPump(this, inputStreamPump); + nsIBinaryInputStream(this, binaryInputStream); + nsIStreamListener(this, streamListener); + + this.asyncInputStream = asyncInputStream; + this.inputStreamPump = inputStreamPump; + this.binaryInputStream = binaryInputStream; }, get status() nsIInputStreamPump(this).status, - read: function() pump(this), + read: function() { + nsIInputStreamPump(this).asyncRead(nsIStreamListener(this), null); + }, pause: function pause() { this.paused = true; nsIInputStreamPump(this).suspend(); emit(this, "paused"); }, resume: function resume() { this.paused = false; nsIInputStreamPump(this).resume(); emit(this, "resume"); }, - destroy: function destroy() { + close: function close() { this.readable = false; - try { - emit(this, "close", null); - nsIInputStreamPump(this).cancel(null); - nsIInputStreamPump(this, null); + nsIInputStreamPump(this).cancel(Cr.NS_OK); + nsIBinaryInputStream(this).close(); + nsIAsyncInputStream(this).close(); + }, + destroy: function destroy() { + this.close(); - nsIInputStream(this).close(); - nsIInputStream(this, null); - } catch (error) { - emit(this, "error", error); - } + nsIInputStreamPump(this); + nsIAsyncInputStream(this); + nsIBinaryInputStream(this); + nsIStreamListener(this); } }); exports.InputStream = InputStream; + + +let nsIRequestObserver = accessor(); +let nsIAsyncOutputStream = accessor(); +let nsIAsyncStreamCopier = accessor(); +let nsIMultiplexInputStream = accessor(); + +const RequestObserver = Class({ + initialize: function(stream) { + this.stream = stream; + }, + // Method is part of `nsIRequestObserver` interface that is + // invoked by `nsIAsyncStreamCopier.asyncCopy`. + onStartRequest: function() {}, + // Method is part of `nsIRequestObserver` interface that is + // invoked by `nsIAsyncStreamCopier.asyncCopy`. + onStopRequest: function(request, context, status) { + let stream = this.stream; + stream.drained = true; + + // Remove copied chunk. + let multiplexInputStream = nsIMultiplexInputStream(stream); + multiplexInputStream.removeStream(0); + + // If there was an error report. + if (!components.isSuccessCode(status)) + emit(stream, "error", status); + + // If there more chunks in queue then flush them. + else if (multiplexInputStream.count) + stream.flush(); + + // If stream is still writable notify that queue has drained. + else if (stream.writable) + emit(stream, "drain"); + + // If stream is no longer writable close it. + else { + nsIAsyncStreamCopier(stream).cancel(Cr.NS_OK); + nsIMultiplexInputStream(stream).close(); + nsIAsyncOutputStream(stream).close(); + nsIAsyncOutputStream(stream).flush(); + } + } +}); + +const OutputStreamCallback = Class({ + initialize: function(stream) { + this.stream = stream; + }, + // Method is part of `nsIOutputStreamCallback` interface that + // is invoked by `nsIAsyncOutputStream.asyncWait`. It is registered + // with `WAIT_CLOSURE_ONLY` flag that overrides the default behavior, + // causing the `onOutputStreamReady` notification to be suppressed until + // the stream becomes closed. + onOutputStreamReady: function(nsIAsyncOutputStream) { + emit(this.stream, "finish"); + } +}); + const OutputStream = Class({ extends: Stream, + writable: false, + drained: true, + get bufferSize() { + let multiplexInputStream = nsIMultiplexInputStream(this); + return multiplexInputStream && multiplexInputStream.available(); + }, initialize: function initialize(options) { - let { output, asyncOutputStream } = options; + let { asyncOutputStream, output } = options; + this.writable = true; + + // Ensure that `nsIAsyncOutputStream` was provided. + asyncOutputStream.QueryInterface(Ci.nsIAsyncOutputStream); - this.writable = true; - nsIOutputStream(this, output); + // Create a `nsIMultiplexInputStream` and `nsIAsyncStreamCopier`. Former + // is used to queue written data chunks that `asyncStreamCopier` will + // asynchronously drain into `asyncOutputStream`. + let multiplexInputStream = MultiplexInputStream(); + let asyncStreamCopier = AsyncStreamCopier(multiplexInputStream, + output || asyncOutputStream, + eventTarget, + // nsIMultiplexInputStream + // implemnts .readSegments() + true, + // nsIOutputStream may or + // may not implemnet + // .writeSegments(). + false, + // Use default buffer size. + null, + // Should not close an input. + false, + // Should not close an output. + false); + + // Create `requestObserver` implementing `nsIRequestObserver` interface + // in the constructor that's gonna be reused across several flushes. + let requestObserver = RequestObserver(this); + + + // Create observer that implements `nsIOutputStreamCallback` and register + // using `WAIT_CLOSURE_ONLY` flag. That way it will be notfied once + // `nsIAsyncOutputStream` is closed. + asyncOutputStream.asyncWait(OutputStreamCallback(this), + asyncOutputStream.WAIT_CLOSURE_ONLY, + 0, + threadManager.currentThread); + + nsIRequestObserver(this, requestObserver); nsIAsyncOutputStream(this, asyncOutputStream); + nsIMultiplexInputStream(this, multiplexInputStream); + nsIAsyncStreamCopier(this, asyncStreamCopier); + + this.asyncOutputStream = asyncOutputStream; + this.multiplexInputStream = multiplexInputStream; + this.asyncStreamCopier = asyncStreamCopier; }, write: function write(content, encoding, callback) { - let output = nsIOutputStream(this); - let asyncOutputStream = nsIAsyncOutputStream(this); - if (isFunction(encoding)) { callback = encoding; encoding = callback; } - // Flag indicating whether or not content has been flushed to the kernel - // buffer. - let isWritten = false; // If stream is not writable we throw an error. - if (!this.writable) - throw Error("stream not writable"); + if (!this.writable) throw Error("stream is not writable"); + + let chunk = null; - try { - // If content is not a buffer then we create one out of it. - if (!Buffer.isBuffer(content)) - content = new Buffer(content, encoding); + // If content is not a buffer then we create one out of it. + if (Buffer.isBuffer(content)) { + chunk = new ArrayBufferInputStream(); + chunk.setData(content.buffer, 0, content.length); + } + else { + chunk = new StringInputStream(); + chunk.setData(content, content.length); + } - // We write content as a byte array as this will avoid any transcoding - // if content was a buffer. - output.writeByteArray(content.valueOf(), content.length); - output.flush(); + if (callback) + this.once("drain", callback); + + // Queue up chunk to be copied to output sync. + nsIMultiplexInputStream(this).appendStream(chunk); + this.flush(); - if (callback) this.once("drain", callback); - onStateChange(asyncOutputStream, this); - return true; - } catch (error) { - // If errors occur we emit appropriate event. - emit(this, "error", error); + return this.drained; + }, + flush: function() { + if (this.drained) { + this.drained = false; + nsIAsyncStreamCopier(this).asyncCopy(nsIRequestObserver(this), null); } }, - flush: function flush() { - nsIOutputStream(this).flush(); - }, end: function end(content, encoding, callback) { if (isFunction(content)) { callback = content content = callback } if (isFunction(encoding)) { callback = encoding encoding = callback } - // Setting a listener to "close" event if passed. + // Setting a listener to "finish" event if passed. if (isFunction(callback)) - this.once("close", callback); + this.once("finish", callback); - // If content is passed then we defer closing until we finish with writing. + if (content) - this.write(content, encoding, end.bind(this)); - // If we don"t write anything, then we close an outputStream. - else - nsIOutputStream(this).close(); + this.write(content, encoding); + this.writable = false; + + // Close `asyncOutputStream` only if output has drained. If it's + // not drained than `asyncStreamCopier` is busy writing, so let + // it finish. Note that since `this.writable` is false copier will + // close `asyncOutputStream` once output drains. + if (this.drained) + nsIAsyncOutputStream(this).close(); }, - destroy: function destroy(callback) { - try { - this.end(callback); - nsIOutputStream(this, null); - nsIAsyncOutputStream(this, null); - } catch (error) { - emit(this, "error", error); - } + destroy: function destroy() { + nsIAsyncOutputStream(this).close(); + nsIAsyncOutputStream(this); + nsIMultiplexInputStream(this); + nsIAsyncStreamCopier(this); + nsIRequestObserver(this); } }); exports.OutputStream = OutputStream; const DuplexStream = Class({ extends: Stream, + implements: [InputStream, OutputStream], + allowHalfOpen: true, initialize: function initialize(options) { - let { input, output, pump } = options; + options = options || {}; + let { readable, writable, allowHalfOpen } = options; + + InputStream.prototype.initialize.call(this, options); + OutputStream.prototype.initialize.call(this, options); + + if (readable === false) + this.readable = false; - this.writable = true; - this.readable = true; - this.encoding = null; + if (writable === false) + this.writable = false; + + if (allowHalfOpen === false) + this.allowHalfOpen = false; - nsIInputStream(this, input); - nsIOutputStream(this, output); - nsIInputStreamPump(this, pump); + // If in a half open state and it's disabled enforce end. + this.once("end", () => { + if (!this.allowHalfOpen && (!this.readable || !this.writable)) + this.end(); + }); }, - read: InputStream.prototype.read, - pause: InputStream.prototype.pause, - resume: InputStream.prototype.resume, - - write: OutputStream.prototype.write, - flush: OutputStream.prototype.flush, - end: OutputStream.prototype.end, - destroy: function destroy(error) { - if (error) - emit(this, "error", error); InputStream.prototype.destroy.call(this); OutputStream.prototype.destroy.call(this); } }); exports.DuplexStream = DuplexStream;
--- a/addon-sdk/source/lib/sdk/lang/functional.js +++ b/addon-sdk/source/lib/sdk/lang/functional.js @@ -7,17 +7,17 @@ // those goes to him. "use strict"; module.metadata = { "stability": "unstable" }; -const { setTimeout } = require("../timers"); +const { setImmediate, setTimeout } = require("../timers"); const { deprecateFunction } = require("../util/deprecate"); /** * Takes `lambda` function and returns a method. When returned method is * invoked it calls wrapped `lambda` and passes `this` as a first argument * and given argument as rest. */ function method(lambda) { @@ -25,23 +25,22 @@ function method(lambda) { return lambda.apply(null, [this].concat(Array.slice(arguments))); } } exports.method = method; /** * Takes a function and returns a wrapped one instead, calling which will call * original function in the next turn of event loop. This is basically utility - * to do `setTimeout(function() { ... }, 0)`, with a difference that returned + * to do `setImmediate(function() { ... })`, with a difference that returned * function is reused, instead of creating a new one each time. This also allows * to use this functions as event listeners. */ function defer(f) { - return function deferred() - setTimeout(invoke, 0, f, arguments, this); + return function deferred() setImmediate(invoke, f, arguments, this); } exports.defer = defer; // Exporting `remit` alias as `defer` may conflict with promises. exports.remit = defer; /* * Takes a funtion and returns a wrapped function that returns `this` */
--- a/addon-sdk/source/lib/sdk/panel.js +++ b/addon-sdk/source/lib/sdk/panel.js @@ -66,17 +66,16 @@ let models = new WeakMap(); let views = new WeakMap(); let workers = new WeakMap(); function viewFor(panel) views.get(panel) function modelFor(panel) models.get(panel) function panelFor(view) panels.get(view) function workerFor(panel) workers.get(panel) -getActiveView.define(Panel, viewFor); // Utility function takes `panel` instance and makes sure it will be // automatically hidden as soon as other panel is shown. let setupAutoHide = new function() { let refs = new WeakMap(); return function setupAutoHide(panel) { // Create system event listener that reacts to any panel showing and @@ -228,16 +227,19 @@ const Panel = Class({ domPanel.resize(view, model.width, model.height); return this; } }); exports.Panel = Panel; +// Note must be defined only after value to `Panel` is assigned. +getActiveView.define(Panel, viewFor); + // Filter panel events to only panels that are create by this module. let panelEvents = filter(events, function({target}) panelFor(target)); // Panel events emitted after panel has being shown. let shows = filter(panelEvents, function({type}) type === "popupshown"); // Panel events emitted after panel became hidden. let hides = filter(panelEvents, function({type}) type === "popuphidden");
--- a/addon-sdk/source/lib/sdk/system.js +++ b/addon-sdk/source/lib/sdk/system.js @@ -63,29 +63,21 @@ exports.exit = function exit(code) { stream.write(status, status.length); stream.flush(); stream.close(); } appStartup.quit(code ? E_ATTEMPT : E_FORCE); }; -exports.stdout = new function() { - let write = dump - if ('logFile' in options && options.logFile) { - let mode = PR_WRONLY | PR_CREATE_FILE | PR_APPEND; - let stream = openFile(options.logFile, mode); - write = function write(data) { - let text = String(data); - stream.write(text, text.length); - stream.flush(); - } - } - return Object.freeze({ write: write }); -}; +// Adapter for nodejs's stdout & stderr: +// http://nodejs.org/api/process.html#process_process_stdout +let stdout = Object.freeze({ write: dump, end: dump }); +exports.stdout = stdout; +exports.stderr = stdout; /** * Returns a path of the system's or application's special directory / file * associated with a given `id`. For list of possible `id`s please see: * https://developer.mozilla.org/en-US/docs/Code_snippets/File_I_O#Getting_files_in_special_directories * http://mxr.mozilla.org/mozilla-central/source/xpcom/io/nsAppDirectoryServiceDefs.h * @example * @@ -129,17 +121,17 @@ exports.build = appInfo.appBuildID; * The XUL application's UUID. * This has traditionally been in the form * `{AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}` but for some applications it may * be: "appname@vendor.tld". */ exports.id = appInfo.ID; /** - * The name of the application. + * The name of the application. */ exports.name = appInfo.name; /** * The XUL application's version, for example "0.8.0+" or "3.7a1pre". */ exports.version = appInfo.version;
--- a/addon-sdk/source/lib/sdk/system/globals.js +++ b/addon-sdk/source/lib/sdk/system/globals.js @@ -17,30 +17,17 @@ let consoleService = Cc['@mozilla.org/co QueryInterface(Ci.nsIConsoleService); // On windows dump does not writes into stdout so cfx can't read thous dumps. // To workaround this issue we write to a special file from which cfx will // read and print to the console. // For more details see: bug-673383 exports.dump = stdout.write; -// Bug 718230: We need to send console messages to stdout and JS Console -function forsakenConsoleDump(msg, level) { - stdout.write(msg); - - if (level === 'error') { - let error = ScriptError(); - msg = msg.replace(/^error: /, ''); - error.init(msg, null, null, 0, 0, 0, 'Add-on SDK'); - consoleService.logMessage(error); - } - else - consoleService.logStringMessage(msg); -}; -exports.console = new PlainTextConsole(forsakenConsoleDump); +exports.console = new PlainTextConsole(); // Provide CommonJS `define` to allow authoring modules in a format that can be // loaded both into jetpack and into browser via AMD loaders. Object.defineProperty(exports, 'define', { // `define` is provided as a lazy getter that binds below defined `define` // function to the module scope, so that require, exports and module // variables remain accessible. configurable: true,
--- a/addon-sdk/source/lib/sdk/tab/events.js +++ b/addon-sdk/source/lib/sdk/tab/events.js @@ -12,16 +12,17 @@ module.metadata = { "stability": "experimental" }; const { Ci } = require("chrome"); const { windows, isInteractive } = require("../window/utils"); const { events } = require("../browser/events"); const { open } = require("../event/dom"); const { filter, map, merge, expand } = require("../event/utils"); +const isFennec = require("sdk/system/xul-app").is("Fennec"); // Module provides event stream (in nodejs style) that emits data events // for all the tab events that happen in running firefox. At the moment // it does it by registering listeners on all browser windows and then // forwarding events when they occur to a stream. This will become obsolete // once Bug 843901 is fixed, and we'll just leverage observer notifications. // Set of tab events that this module going to aggregate and expose. @@ -52,9 +53,18 @@ let eventsFromFuture = expand(futureWind // tab events for them too. let interactiveWindows = windows("navigator:browser", { includePrivate: true }). filter(isInteractive); let eventsFromInteractive = merge(interactiveWindows.map(tabEventsFor)); // Finally merge stream of tab events from future windows and current windows // to cover all tab events on all windows that will open. -exports.events = merge([eventsFromInteractive, eventsFromFuture]); +let allEvents = merge([eventsFromInteractive, eventsFromFuture]); + +// Map events to Fennec format if necessary +exports.events = map(allEvents, function (event) { + return !isFennec ? event : { + type: event.type, + target: event.target.ownerDocument.defaultView.BrowserApp + .getTabForBrowser(event.target) + }; +});
--- a/addon-sdk/source/lib/sdk/tabs/tab-fennec.js +++ b/addon-sdk/source/lib/sdk/tabs/tab-fennec.js @@ -49,17 +49,19 @@ const Tab = Class({ get title() getTabTitle(tabNS(this).tab), set title(title) setTabTitle(tabNS(this).tab, title), /** * Location of the page currently loaded in this tab. * Changing this property will loads page under under the specified location. * @type {String} */ - get url() getTabURL(tabNS(this).tab), + get url() { + return tabNS(this).closed ? undefined : getTabURL(tabNS(this).tab); + }, set url(url) setTabURL(tabNS(this).tab, url), /** * URI of the favicon for the page currently loaded in this tab. * @type {String} */ get favicon() { /* @@ -90,16 +92,18 @@ const Tab = Class({ }, /** * The index of the tab relative to other tabs in the application window. * Changing this property will change order of the actual position of the tab. * @type {Number} */ get index() { + if (tabNS(this).closed) return undefined; + let tabs = tabNS(this).window.BrowserApp.tabs; let tab = tabNS(this).tab; for (var i = tabs.length; i >= 0; i--) { if (tabs[i] === tab) return i; } return null; }, @@ -146,18 +150,21 @@ const Tab = Class({ activate: function activate() { activateTab(tabNS(this).tab, tabNS(this).window); }, /** * Close the tab */ close: function close(callback) { - if (callback) - this.once(EVENTS.close.name, callback); + let tab = this; + this.once(EVENTS.close.name, function () { + tabNS(tab).closed = true; + if (callback) callback(); + }); closeTab(tabNS(this).tab); }, /** * Reload the tab */ reload: function reload() {
--- a/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js +++ b/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js @@ -21,20 +21,19 @@ function newTabWindow(options) { } Object.defineProperties(tabs, { open: { value: function open(options) { if (options.inNewWindow) { newTabWindow(options); return undefined; } - // Open in active window if new window was not required. let activeWindow = windows.activeWindow; - let privateState = !!options.isPrivate; + let privateState = (supportPrivateTabs && (options.isPrivate || isPrivate(activeWindow))) || false; // if the active window is in the state that we need then use it if (activeWindow && (!supportPrivateTabs || privateState === isPrivate(activeWindow))) { activeWindow.tabs.open(options); } else { // find a window in the state that we need let window = getWindow(privateState);
--- a/addon-sdk/source/lib/sdk/test/harness.js +++ b/addon-sdk/source/lib/sdk/test/harness.js @@ -3,17 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; module.metadata = { "stability": "experimental" }; -const { Cc,Ci } = require("chrome"); +const { Cc, Ci, Cu } = require("chrome"); const { Loader } = require('./loader'); const { serializeStack, parseStack } = require("toolkit/loader"); const { setTimeout } = require('../timers'); const memory = require('../deprecated/memory'); const { PlainTextConsole } = require("../console/plain-text"); const { when: unload } = require("../system/unload"); const { format, fromException } = require("../console/traceback"); const system = require("../system"); @@ -142,19 +142,21 @@ function dictDiff(last, curr) { return diff; } function reportMemoryUsage() { memory.gc(); var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] .getService(Ci.nsIMemoryReporterManager); + var reporters = mgr.enumerateReporters(); if (reporters.hasMoreElements()) print("\n"); + while (reporters.hasMoreElements()) { var reporter = reporters.getNext(); reporter.QueryInterface(Ci.nsIMemoryReporter); print(reporter.description + ": " + reporter.memoryUsed + "\n"); } var weakrefs = [info.weakref.get() for each (info in memory.getObjects())]; @@ -162,36 +164,34 @@ function reportMemoryUsage() { print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n"); } var gWeakrefInfo; function checkMemory() { memory.gc(); - setTimeout(function () { - memory.gc(); - setTimeout(function () { - let leaks = getPotentialLeaks(); - let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) { - return !(url in startLeaks.compartments); - }); + Cu.schedulePreciseGC(function () { + let leaks = getPotentialLeaks(); + + let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) { + return !(url in startLeaks.compartments); + }); - let windowURLs = Object.keys(leaks.windows).filter(function(url) { - return !(url in startLeaks.windows); - }); + let windowURLs = Object.keys(leaks.windows).filter(function(url) { + return !(url in startLeaks.windows); + }); - for (let url of compartmentURLs) - console.warn("LEAKED", leaks.compartments[url]); + for (let url of compartmentURLs) + console.warn("LEAKED", leaks.compartments[url]); - for (let url of windowURLs) - console.warn("LEAKED", leaks.windows[url]); + for (let url of windowURLs) + console.warn("LEAKED", leaks.windows[url]); - showResults(); - }); + showResults(); }); } function showResults() { if (gWeakrefInfo) { gWeakrefInfo.forEach( function(info) { var ref = info.weakref.get(); @@ -293,16 +293,17 @@ function getPotentialLeaks() { let uri = ioService.newURI("chrome://global/content/", "UTF-8", null); let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. getService(Ci.nsIChromeRegistry); uri = chromeReg.convertChromeURL(uri); let spec = uri.spec; let pos = spec.indexOf("!/"); WHITELIST_BASE_URLS.push(spec.substring(0, pos + 2)); + let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)"); let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/"); let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$"); let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active"); let windowDetails = new RegExp("^(.*), id=.*$"); function isPossibleLeak(item) { if (!item.location) return false; @@ -313,18 +314,19 @@ function getPotentialLeaks() { } return true; } let compartments = {}; let windows = {}; function logReporter(process, path, kind, units, amount, description) { - let matches = compartmentRegexp.exec(path); - if (matches) { + let matches; + + if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) { if (matches[1] in compartments) return; let details = compartmentDetails.exec(matches[1]); if (!details) { console.error("Unable to parse compartment detail " + matches[1]); return; } @@ -571,17 +573,17 @@ var runTests = exports.runTests = functi print("Running tests on " + system.name + " " + system.version + "/Gecko " + system.platformVersion + " (" + system.id + ") under " + system.platform + "/" + system.architecture + ".\n"); if (options.parseable) testConsole = new TestRunnerTinderboxConsole(options); else - testConsole = new TestRunnerConsole(new PlainTextConsole(print), options); + testConsole = new TestRunnerConsole(new PlainTextConsole(), options); loader = Loader(module, { console: testConsole, global: {} // useful for storing things like coverage testing. }); // Load these before getting initial leak stats as they will still be in // memory when we check later
--- a/addon-sdk/source/lib/sdk/timers.js +++ b/addon-sdk/source/lib/sdk/timers.js @@ -2,52 +2,104 @@ * 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/. */ 'use strict'; module.metadata = { "stability": "stable" }; -const { CC, Ci } = require('chrome'); -const { when: unload } = require('./system/unload'); +const { CC, Cc, Ci } = require("chrome"); +const { when: unload } = require("./system/unload"); const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer; -const Timer = CC('@mozilla.org/timer;1', 'nsITimer'); +const Timer = CC("@mozilla.org/timer;1", "nsITimer"); const timers = Object.create(null); +const threadManager = Cc["@mozilla.org/thread-manager;1"]. + getService(Ci.nsIThreadManager); +const prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + QueryInterface(Ci.nsIPrefBranch); + +let MIN_DELAY = 4; +// Try to get min timeout delay used by browser. +try { MIN_DELAY = prefBranch.getIntPref("dom.min_timeout_value"); } finally {} + // Last timer id. let lastID = 0; // Sets typer either by timeout or by interval // depending on a given type. -function setTimer(type, callback, delay) { +function setTimer(type, callback, delay, ...args) { let id = ++ lastID; let timer = timers[id] = Timer(); - let args = Array.slice(arguments, 3); timer.initWithCallback({ notify: function notify() { try { if (type === TYPE_ONE_SHOT) delete timers[id]; callback.apply(null, args); } catch(error) { console.exception(error); } } - }, delay || 0, type); + }, Math.max(delay || MIN_DELAY), type); return id; } function unsetTimer(id) { let timer = timers[id]; delete timers[id]; - if (timer) - timer.cancel(); + if (timer) timer.cancel(); } +let immediates = new Map(); + +let dispatcher = _ => { + // Allow scheduling of a new dispatch loop. + dispatcher.scheduled = false; + // Take a snapshot of timer `id`'s that have being present before + // starting a dispatch loop, in order to ignore timers registered + // in side effect to dispatch while also skipping immediates that + // were removed in side effect. + let ids = [id for ([id] of immediates)]; + for (let id of ids) { + let immediate = immediates.get(id); + if (immediate) { + immediates.delete(id); + try { immediate(); } + catch (error) { console.exception(error); } + } + } +} + +function setImmediate(callback, ...params) { + let id = ++ lastID; + // register new immediate timer with curried params. + immediates.set(id, _ => callback.apply(callback, params)); + // if dispatch loop is not scheduled schedule one. Own scheduler + if (!dispatcher.scheduled) { + dispatcher.scheduled = true; + threadManager.currentThread.dispatch(dispatcher, + Ci.nsIThread.DISPATCH_NORMAL); + } + return id; +} + +function clearImmediate(id) { + immediates.delete(id); +} + +// Bind timers so that toString-ing them looks same as on native timers. +exports.setImmediate = setImmediate.bind(null); +exports.clearImmediate = clearImmediate.bind(null); exports.setTimeout = setTimer.bind(null, TYPE_ONE_SHOT); exports.setInterval = setTimer.bind(null, TYPE_REPEATING_SLACK); exports.clearTimeout = unsetTimer.bind(null); exports.clearInterval = unsetTimer.bind(null); -unload(function() { Object.keys(timers).forEach(unsetTimer) }); +// all timers are cleared out on unload. +unload(function() { + immediates.clear(); + Object.keys(timers).forEach(unsetTimer) +});
--- a/addon-sdk/source/lib/sdk/util/array.js +++ b/addon-sdk/source/lib/sdk/util/array.js @@ -96,18 +96,19 @@ function fromIterator(iterator) { else { for (let item of iterator) array.push(item); } return array; } exports.fromIterator = fromIterator; -function find(array, predicate) { +function find(array, predicate, fallback) { var index = 0; var count = array.length; while (index < count) { var value = array[index]; if (predicate(value)) return value; else index = index + 1; } + return fallback; } exports.find = find;
--- a/addon-sdk/source/lib/sdk/window/utils.js +++ b/addon-sdk/source/lib/sdk/window/utils.js @@ -13,16 +13,18 @@ const observers = require('../deprecated const { defer } = require('sdk/core/promise'); const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. getService(Ci.nsIWindowWatcher); const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. getService(Ci.nsIAppShellService); const WM = Cc['@mozilla.org/appshell/window-mediator;1']. getService(Ci.nsIWindowMediator); +const io = Cc['@mozilla.org/network/io-service;1']. + getService(Ci.nsIIOService); const BROWSER = 'navigator:browser', URI_BROWSER = 'chrome://browser/content/browser.xul', NAME = '_blank', FEATURES = 'chrome,all,dialog=no,non-private'; function isWindowPrivate(win) { if (!win) @@ -179,28 +181,31 @@ function serializeFeatures(options) { * @params {nsIDOMWindow} options.parent * Used as parent for the created window. * @params {String} options.name * Optional name that is assigned to the window. * @params {Object} options.features * Map of key, values like: `{ width: 10, height: 15, chrome: true, private: true }`. */ function open(uri, options) { - options = options || {}; + uri = uri || URI_BROWSER; + options = options || {} + + if (['chrome', 'resource', 'data'].indexOf(io.newURI(uri, null, null).scheme) < 0) + throw new Error('only chrome, resource and data uris are allowed'); + let newWindow = windowWatcher. openWindow(options.parent || null, - uri || URI_BROWSER, + uri, options.name || null, serializeFeatures(options.features || {}), options.args || null); return newWindow; } - - exports.open = open; function onFocus(window) { let { resolve, promise } = defer(); if (isFocused(window)) { resolve(window); }
--- a/addon-sdk/source/python-lib/cuddlefish/options_xul.py +++ b/addon-sdk/source/python-lib/cuddlefish/options_xul.py @@ -47,16 +47,19 @@ def validate_prefs(options): def parse_options(options, jetpack_id): doc = Document() root = doc.createElement("vbox") root.setAttribute("xmlns", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul") doc.appendChild(root) for pref in options: + if ("hidden" in pref and pref["hidden"] == True): + continue; + setting = doc.createElement("setting") setting.setAttribute("pref-name", pref["name"]) setting.setAttribute("data-jetpack-id", jetpack_id) setting.setAttribute("pref", "extensions." + jetpack_id + "." + pref["name"]) setting.setAttribute("type", pref["type"]) setting.setAttribute("title", pref["title"]) if ("description" in pref):
--- a/addon-sdk/source/python-lib/cuddlefish/runner.py +++ b/addon-sdk/source/python-lib/cuddlefish/runner.py @@ -25,17 +25,17 @@ FILTER_ONLY_CONSOLE_FROM_ADB = re.compil # Used to detect the currently running test PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n') # Maximum time we'll wait for tests to finish, in seconds. # The purpose of this timeout is to recover from infinite loops. It should be # longer than the amount of time any test run takes, including those on slow # machines running slow (debug) versions of Firefox. -RUN_TIMEOUT = 45 * 60 # 45 minutes +RUN_TIMEOUT = 1.5 * 60 * 60 # 1.5 Hour # Maximum time we'll wait for tests to emit output, in seconds. # The purpose of this timeout is to recover from hangs. It should be longer # than the amount of time any test takes to report results. OUTPUT_TIMEOUT = 60 # one minute def follow_file(filename): """ @@ -491,19 +491,16 @@ def run_app(harness_root_dir, manifest_r fileno,logfile = tempfile.mkstemp(prefix="harness-log-") os.close(fileno) logfile_tail = follow_file(logfile) atexit.register(maybe_remove_logfile) logfile = os.path.abspath(os.path.expanduser(logfile)) maybe_remove_logfile() - if app_type != "fennec-on-device": - harness_options['logFile'] = logfile - env = {} env.update(os.environ) env['MOZ_NO_REMOTE'] = '1' env['XPCOM_DEBUG_BREAK'] = 'stack' env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1' env.update(extra_environment) if norun: cmdargs.append("-no-remote")
--- a/addon-sdk/source/python-lib/mozrunner/winprocess.py +++ b/addon-sdk/source/python-lib/mozrunner/winprocess.py @@ -325,23 +325,19 @@ GetExitCodeProcessProto = WINFUNCTYPE(BO GetExitCodeProcessFlags = ((1, "hProcess"), (2, "lpExitCode")) GetExitCodeProcess = GetExitCodeProcessProto( ("GetExitCodeProcess", windll.kernel32), GetExitCodeProcessFlags) GetExitCodeProcess.errcheck = ErrCheckBool def CanCreateJobObject(): - currentProc = GetCurrentProcess() - if IsProcessInJob(currentProc): - jobinfo = QueryInformationJobObject(HANDLE(0), 'JobObjectExtendedLimitInformation') - limitflags = jobinfo['BasicLimitInformation']['LimitFlags'] - return bool(limitflags & JOB_OBJECT_LIMIT_BREAKAWAY_OK) or bool(limitflags & JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK) - else: - return True + # Running firefox in a job (from cfx) hangs on sites using flash plugin + # so job creation is turned off for now. (see Bug 768651). + return False ### testing functions def parent(): print 'Starting parent' currentProc = GetCurrentProcess() if IsProcessInJob(currentProc): print >> sys.stderr, "You should not be in a job object to test"
--- a/addon-sdk/source/test/addons/content-permissions/main.js +++ b/addon-sdk/source/test/addons/content-permissions/main.js @@ -1,20 +1,21 @@ /* 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/. */ "use strict"; -const xulApp = require("sdk/system/xul-app"); const { PageMod } = require("sdk/page-mod"); const tabs = require("sdk/tabs"); +const { startServerAsync } = require("sdk/test/httpd"); + +const serverPort = 8099; exports.testCrossDomainIframe = function(assert, done) { - let serverPort = 8099; - let server = require("sdk/test/httpd").startServerAsync(serverPort); + let server = startServerAsync(serverPort); server.registerPathHandler("/iframe", function handle(request, response) { response.write("<html><body>foo</body></html>"); }); let pageMod = PageMod({ include: "about:*", contentScript: "new " + function ContentScriptScope() { self.on("message", function (url) { @@ -26,29 +27,33 @@ exports.testCrossDomainIframe = function iframe.setAttribute("src", url); document.documentElement.appendChild(iframe); }); }, onAttach: function(w) { w.on("message", function (body) { assert.equal(body, "foo", "received iframe html content"); pageMod.destroy(); - w.tab.close(); - server.stop(done); + w.tab.close(function() { + server.stop(done); + }); }); + w.postMessage("http://localhost:8099/iframe"); } }); - tabs.open("about:credits"); + tabs.open({ + url: "about:home", + inBackground: true + }); }; exports.testCrossDomainXHR = function(assert, done) { - let serverPort = 8099; - let server = require("sdk/test/httpd").startServerAsync(serverPort); + let server = startServerAsync(serverPort); server.registerPathHandler("/xhr", function handle(request, response) { response.write("foo"); }); let pageMod = PageMod({ include: "about:*", contentScript: "new " + function ContentScriptScope() { self.on("message", function (url) { @@ -60,27 +65,24 @@ exports.testCrossDomainXHR = function(as }; request.send(null); }); }, onAttach: function(w) { w.on("message", function (body) { assert.equal(body, "foo", "received XHR content"); pageMod.destroy(); - w.tab.close(); - server.stop(done); + w.tab.close(function() { + server.stop(done); + }); }); + w.postMessage("http://localhost:8099/xhr"); } }); - tabs.open("about:credits"); + tabs.open({ + url: "about:home", + inBackground: true + }); }; -if (!xulApp.versionInRange(xulApp.platformVersion, "17.0a2", "*")) { - module.exports = { - "test Unsupported Application": function Unsupported (assert) { - assert.pass("This firefox version doesn't support cross-domain-content permission."); - } - }; -} - require("sdk/test/runner").runTestsFromModule(module);
--- a/addon-sdk/source/test/addons/layout-change/main.js +++ b/addon-sdk/source/test/addons/layout-change/main.js @@ -74,18 +74,20 @@ exports["test compatibility"] = function require("sdk/console/traceback"), "sdk/console/traceback -> traceback"); assert.equal(require("unload"), require("sdk/system/unload"), "sdk/system/unload -> unload"); assert.equal(require("hotkeys"), require("sdk/hotkeys"), "sdk/hotkeys -> hotkeys"); - assert.equal(require("clipboard"), - require("sdk/clipboard"), "sdk/clipboard -> clipboard"); + if (app.is("Firefox")) { + assert.equal(require("clipboard"), + require("sdk/clipboard"), "sdk/clipboard -> clipboard"); + } assert.equal(require("windows"), require("sdk/windows"), "sdk/windows -> windows"); assert.equal(require("page-worker"), require("sdk/page-worker"), "sdk/page-worker -> page-worker"); assert.equal(require("timer"),
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/addons/main/main.js @@ -0,0 +1,35 @@ +/* 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/. */ +'use strict'; + +const { setTimeout } = require('sdk/timers'); + +let mainStarted = false; + +exports.main = function main(options, callbacks) { + mainStarted = true; + + let tests = {}; + + tests.testMainArguments = function(assert) { + assert.ok(!!options, 'options argument provided to main'); + assert.ok('loadReason' in options, 'loadReason is in options provided by main'); + assert.equal(typeof callbacks.print, 'function', 'callbacks.print is a function'); + assert.equal(typeof callbacks.quit, 'function', 'callbacks.quit is a function'); + assert.equal(options.loadReason, 'install', 'options.loadReason is install'); + } + + require('sdk/test/runner').runTestsFromModule({exports: tests}); +} + +// this causes a fail if main does not start +setTimeout(function() { + if (mainStarted) + return; + + // main didn't start, fail.. + require("sdk/test/runner").runTestsFromModule({exports: { + testFail: function(assert) assert.fail('Main did not start..') + }}); +}, 500);
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/addons/main/package.json @@ -0,0 +1,3 @@ +{ + "id": "test-main" +}
--- a/addon-sdk/source/test/addons/private-browsing-supported/main.js +++ b/addon-sdk/source/test/addons/private-browsing-supported/main.js @@ -1,24 +1,24 @@ /* 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/. */ 'use strict'; const { merge } = require('sdk/util/object'); -const app = require("sdk/system/xul-app"); +const app = require('sdk/system/xul-app'); const { isGlobalPBSupported } = require('sdk/private-browsing/utils'); merge(module.exports, require('./test-tabs'), require('./test-page-mod'), require('./test-selection'), require('./test-panel'), require('./test-private-browsing'), isGlobalPBSupported ? require('./test-global-private-browsing') : {} ); // Doesn't make sense to test window-utils and windows on fennec, // as there is only one window which is never private -if (!app.is("Fennec")) +if (!app.is('Fennec')) merge(module.exports, require('./test-windows')); require('sdk/test/runner').runTestsFromModule(module);
--- a/addon-sdk/source/test/addons/private-browsing-supported/test-tabs.js +++ b/addon-sdk/source/test/addons/private-browsing-supported/test-tabs.js @@ -1,15 +1,17 @@ 'use strict'; const tabs = require('sdk/tabs'); const { is } = require('sdk/system/xul-app'); const { isPrivate } = require('sdk/private-browsing'); const pbUtils = require('sdk/private-browsing/utils'); const { getOwnerWindow } = require('sdk/private-browsing/window/utils'); +const { promise: windowPromise, close, focus } = require('sdk/window/helpers'); +const { getMostRecentBrowserWindow } = require('sdk/window/utils'); exports.testPrivateTabsAreListed = function (assert, done) { let originalTabCount = tabs.length; tabs.open({ url: 'about:blank', isPrivate: true, onOpen: function(tab) { @@ -21,12 +23,91 @@ exports.testPrivateTabsAreListed = funct 'New private window\'s tab are visible in tabs list'); } else { // Global case, openDialog didn't opened a private window/tab assert.ok(!isPrivate(tab), "tab isn't private"); assert.equal(tabs.length, originalTabCount + 1, 'New non-private window\'s tab is visible in tabs list'); } + tab.close(done); } }); } + +exports.testOpenTabWithPrivateActiveWindowNoIsPrivateOption = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.ok(isPrivate(window), 'new window is private'); + + tabs.open({ + url: 'about:blank', + onOpen: function(tab) { + assert.ok(isPrivate(tab), 'new tab is private'); + assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +} + +exports.testOpenTabWithNonPrivateActiveWindowNoIsPrivateOption = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.equal(isPrivate(window), false, 'new window is not private'); + + tabs.open({ + url: 'about:blank', + onOpen: function(tab) { + assert.equal(isPrivate(tab), false, 'new tab is not private'); + assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +} + +exports.testOpenTabWithPrivateActiveWindowWithIsPrivateOptionTrue = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: true }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.ok(isPrivate(window), 'new window is private'); + + tabs.open({ + url: 'about:blank', + isPrivate: true, + onOpen: function(tab) { + assert.ok(isPrivate(tab), 'new tab is private'); + assert.ok(isPrivate(getOwnerWindow(tab)), 'new tab window is private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the private window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +} + +exports.testOpenTabWithNonPrivateActiveWindowWithIsPrivateOptionFalse = function(assert, done) { + let window = getMostRecentBrowserWindow().OpenBrowserWindow({ private: false }); + + windowPromise(window, 'load').then(focus).then(function (window) { + assert.equal(isPrivate(window), false, 'new window is not private'); + + tabs.open({ + url: 'about:blank', + isPrivate: false, + onOpen: function(tab) { + assert.equal(isPrivate(tab), false, 'new tab is not private'); + assert.equal(isPrivate(getOwnerWindow(tab)), false, 'new tab window is not private'); + assert.strictEqual(getOwnerWindow(tab), window, 'the tab window and the new window are the same'); + + close(window).then(done, assert.fail); + } + }) + }, assert.fail).then(null, assert.fail); +}
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/addons/simple-prefs/lib/main.js @@ -0,0 +1,82 @@ +/* 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/. */ +'use strict'; + +const { Cu } = require('chrome'); +const sp = require('sdk/simple-prefs'); +const app = require('sdk/system/xul-app'); +const self = require('sdk/self'); +const tabs = require('sdk/tabs'); + +const { AddonManager } = Cu.import('resource://gre/modules/AddonManager.jsm', {}); + +exports.testDefaultValues = function (assert) { + assert.equal(sp.prefs.myHiddenInt, 5, 'myHiddenInt default is 5'); + assert.equal(sp.prefs.myInteger, 8, 'myInteger default is 8'); + assert.equal(sp.prefs.somePreference, 'TEST', 'somePreference default is correct'); +} + +exports.testOptionsType = function(assert, done) { + AddonManager.getAddonByID(self.id, function(aAddon) { + assert.equal(aAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE, 'options type is inline'); + done(); + }); +} + +if (app.is('Firefox')) { + exports.testAOM = function(assert, done) { + tabs.open({ + url: 'about:addons', + onReady: function(tab) { + tab.attach({ + contentScript: 'AddonManager.getAddonByID("' + self.id + '", function(aAddon) {\n' + + 'unsafeWindow.gViewController.viewObjects.detail.node.addEventListener("ViewChanged", function whenViewChanges() {\n' + + 'unsafeWindow.gViewController.viewObjects.detail.node.removeEventListener("ViewChanged", whenViewChanges, false);\n' + + 'setTimeout(function() {\n' + // TODO: figure out why this is necessary.. + 'self.postMessage({\n' + + 'somePreference: getAttributes(unsafeWindow.document.querySelector("setting[title=\'some-title\']")),\n' + + 'myInteger: getAttributes(unsafeWindow.document.querySelector("setting[title=\'my-int\']")),\n' + + 'myHiddenInt: getAttributes(unsafeWindow.document.querySelector("setting[title=\'hidden-int\']"))\n' + + '});\n' + + '}, 250);\n' + + '}, false);\n' + + 'unsafeWindow.gViewController.commands.cmd_showItemDetails.doCommand(aAddon, true);\n' + + '});\n' + + 'function getAttributes(ele) {\n' + + 'if (!ele) return {};\n' + + 'return {\n' + + 'pref: ele.getAttribute("pref"),\n' + + 'type: ele.getAttribute("type"),\n' + + 'title: ele.getAttribute("title"),\n' + + 'desc: ele.getAttribute("desc")\n' + + '}\n' + + '}\n', + onMessage: function(msg) { + // test somePreference + assert.equal(msg.somePreference.type, 'string', 'some pref is a string'); + assert.equal(msg.somePreference.pref, 'extensions.'+self.id+'.somePreference', 'somePreference path is correct'); + assert.equal(msg.somePreference.title, 'some-title', 'somePreference title is correct'); + assert.equal(msg.somePreference.desc, 'Some short description for the preference', 'somePreference description is correct'); + + // test myInteger + assert.equal(msg.myInteger.type, 'integer', 'myInteger is a int'); + assert.equal(msg.myInteger.pref, 'extensions.'+self.id+'.myInteger', 'extensions.test-simple-prefs.myInteger'); + assert.equal(msg.myInteger.title, 'my-int', 'myInteger title is correct'); + assert.equal(msg.myInteger.desc, 'How many of them we have.', 'myInteger desc is correct'); + + // test myHiddenInt + assert.equal(msg.myHiddenInt.type, undefined, 'myHiddenInt was not displayed'); + assert.equal(msg.myHiddenInt.pref, undefined, 'myHiddenInt was not displayed'); + assert.equal(msg.myHiddenInt.title, undefined, 'myHiddenInt was not displayed'); + assert.equal(msg.myHiddenInt.desc, undefined, 'myHiddenInt was not displayed'); + + tab.close(done); + } + }); + } + }); + } +} + +require('sdk/test/runner').runTestsFromModule(module);
new file mode 100644 --- /dev/null +++ b/addon-sdk/source/test/addons/simple-prefs/package.json @@ -0,0 +1,23 @@ +{ + "id": "test-simple-prefs", + "preferences": [{ + "name": "somePreference", + "title": "some-title", + "description": "Some short description for the preference", + "type": "string", + "value": "TEST" + }, + { + "description": "How many of them we have.", + "name": "myInteger", + "type": "integer", + "value": 8, + "title": "my-int" + }, { + "name": "myHiddenInt", + "type": "integer", + "hidden": true, + "value": 5, + "title": "hidden-int" + }] +}
--- a/addon-sdk/source/test/places-helper.js +++ b/addon-sdk/source/test/places-helper.js @@ -14,16 +14,17 @@ const tagsrv = Cc['@mozilla.org/browser/ getService(Ci.nsITaggingService); const asyncHistory = Cc['@mozilla.org/browser/history;1']. getService(Ci.mozIAsyncHistory); const { send } = require('sdk/addon/events'); const { setTimeout } = require('sdk/timers'); const { newURI } = require('sdk/url/utils'); const { defer, all } = require('sdk/core/promise'); const { once } = require('sdk/system/events'); +const { set } = require('sdk/preferences/service'); const { Bookmark, Group, Separator, save, search, MENU, TOOLBAR, UNSORTED } = require('sdk/places/bookmarks'); function invalidResolve (assert) { return function (e) { @@ -40,22 +41,35 @@ function invalidReject (assert) { exports.invalidReject = invalidReject; // Removes all children of group function clearBookmarks (group) { group ? bmsrv.removeFolderChildren(group.id) : clearAllBookmarks(); } -exports.clearBookmarks = clearBookmarks; function clearAllBookmarks () { [MENU, TOOLBAR, UNSORTED].forEach(clearBookmarks); } -exports.clearAllBookmarks = clearAllBookmarks; + +function clearHistory (done) { + hsrv.removeAllPages(); + once('places-expiration-finished', done); +} + +// Cleans bookmarks and history and disables maintanance +function resetPlaces (done) { + // Set last maintenance to current time to prevent + // Places DB maintenance occuring and locking DB + set('places.database.lastMaintenance', Math.floor(Date.now() / 1000)); + clearAllBookmarks(); + clearHistory(done); +} +exports.resetPlaces = resetPlaces; function compareWithHost (assert, item) { let id = item.id; let type = item.type === 'group' ? bmsrv.TYPE_FOLDER : bmsrv['TYPE_' + item.type.toUpperCase()]; let url = item.url && !item.url.endsWith('/') ? item.url + '/' : item.url; if (type === bmsrv.TYPE_BOOKMARK) { assert.equal(url, bmsrv.getBookmarkURI(id).spec.toString(), 'Matches host url'); @@ -100,22 +114,16 @@ function createVisit (url) { place.visits = [{ transitionType: hsrv.TRANSITION_LINK, visitDate: +(new Date()) * 1000, referredURI: undefined }]; return place; } -function clearHistory (done) { - hsrv.removeAllPages(); - once('places-expiration-finished', done); -} -exports.clearHistory = clearHistory; - function createBookmark (data) { data = data || {}; let item = { title: data.title || 'Moz', url: data.url || (!data.type || data.type === 'bookmark' ? 'http://moz.com/' : undefined), tags: data.tags || (!data.type || data.type === 'bookmark' ?
--- a/addon-sdk/source/test/tabs/test-fennec-tabs.js +++ b/addon-sdk/source/test/tabs/test-fennec-tabs.js @@ -106,22 +106,21 @@ exports.testTabProperties = function(tes let tabsLen = tabs.length; tabs.open({ url: url, onReady: function(tab) { test.assertEqual(tab.title, "foo", "title of the new tab matches"); test.assertEqual(tab.url, url, "URL of the new tab matches"); test.assert(tab.favicon, "favicon of the new tab is not empty"); // TODO: remove need for this test by implementing the favicon feature - // Poors man deepEqual with JSON.stringify... - test.assertEqual(JSON.stringify(messages), - JSON.stringify(['tab.favicon is deprecated, and ' + - 'currently favicon helpers are not yet supported ' + - 'by Fennec']), - "favicon logs an error for now"); + test.assertEqual(messages[0].msg, + "tab.favicon is deprecated, and " + + "currently favicon helpers are not yet supported " + + "by Fennec", + "favicon logs an error for now"); test.assertEqual(tab.style, null, "style of the new tab matches"); test.assertEqual(tab.index, tabsLen, "index of the new tab matches"); test.assertNotEqual(tab.getThumbnail(), null, "thumbnail of the new tab matches"); test.assertNotEqual(tab.id, null, "a tab object always has an id property"); tab.close(function() { loader.unload();
--- a/addon-sdk/source/test/test-addon-installer.js +++ b/addon-sdk/source/test/test-addon-installer.js @@ -128,17 +128,9 @@ exports["test Update"] = function (asser function next() { events = []; AddonInstaller.install(ADDON_PATH).then(onInstalled, onFailure); } next(); } -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass("Skipping this test until Fennec support is implemented."); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-addon-page.js +++ b/addon-sdk/source/test/test-addon-page.js @@ -1,14 +1,20 @@ /* 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/. */ 'use strict'; +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + const { isTabOpen, activateTab, openTab, closeTab, getURI } = require('sdk/tabs/utils'); const windows = require('sdk/deprecated/window-utils'); const { LoaderWithHookedConsole } = require('sdk/test/loader'); const { setTimeout } = require('sdk/timers'); const { is } = require('sdk/system/xul-app'); const tabs = require('sdk/tabs'); const isAustralis = "gCustomizeMode" in windows.activeBrowserWindow;
--- a/addon-sdk/source/test/test-array.js +++ b/addon-sdk/source/test/test-array.js @@ -80,8 +80,16 @@ exports.testUnique = function(test) { function compareArray (a, b) { test.assertEqual(a.length, b.length); for (let i = 0; i < a.length; i++) { test.assertEqual(a[i], b[i]); } } }; + +exports.testFind = function(test) { + let isOdd = (x) => x % 2; + test.assertEqual(array.find([2, 4, 5, 7, 8, 9], isOdd), 5); + test.assertEqual(array.find([2, 4, 6, 8], isOdd), undefined); + test.assertEqual(array.find([2, 4, 6, 8], isOdd, null), null); +}; +
--- a/addon-sdk/source/test/test-browser-events.js +++ b/addon-sdk/source/test/test-browser-events.js @@ -87,19 +87,9 @@ exports["test browser events ignore othe done(); } }); // Open window and close it to trigger observers. let window = open("data:text/html,not a browser"); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 793071"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-clipboard.js +++ b/addon-sdk/source/test/test-clipboard.js @@ -1,14 +1,16 @@ /* 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/. */ "use strict"; +require("sdk/clipboard"); + const { Cc, Ci } = require("chrome"); const imageTools = Cc["@mozilla.org/image/tools;1"]. getService(Ci.imgITools); const io = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); @@ -213,21 +215,9 @@ exports["test Set Image Type Wrong Data" var wrongPNG = "data:image/png" + base64jpeg.substr(15); assert.throws(function () { clip.set(wrongPNG, flavor); }, "Unable to decode data given in a valid image."); }; -// TODO: Test error cases. - -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 789757"); - } - } -} - require("test").run(exports)
--- a/addon-sdk/source/test/test-content-script.js +++ b/addon-sdk/source/test/test-content-script.js @@ -1,57 +1,71 @@ /* 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/. */ const hiddenFrames = require("sdk/frame/hidden-frame"); - +const { create: makeFrame } = require("sdk/frame/utils"); +const { window } = require("sdk/addon/window"); const { Loader } = require('sdk/test/loader'); +const { URL } = require("sdk/url"); +const testURI = require("sdk/self").data.url("test.html"); +const testHost = URL(testURI).scheme + '://' + URL(testURI).host; /* * Utility function that allow to easily run a proxy test with a clean * new HTML document. See first unit test for usage. */ function createProxyTest(html, callback) { return function (assert, done) { - let url = 'data:text/html;charset=utf-8,' + encodeURI(html); - - let hiddenFrame = hiddenFrames.add(hiddenFrames.HiddenFrame({ - onReady: function () { + let url = 'data:text/html;charset=utf-8,' + encodeURIComponent(html); + let principalLoaded = false; - function onDOMReady() { - hiddenFrame.element.removeEventListener("DOMContentLoaded", onDOMReady, - false); + let element = makeFrame(window.document, { + nodeName: "iframe", + type: "content", + allowJavascript: true, + allowPlugins: true, + allowAuth: true, + uri: testURI + }); - let xrayWindow = hiddenFrame.element.contentWindow; - let rawWindow = xrayWindow.wrappedJSObject; + element.addEventListener("DOMContentLoaded", onDOMReady, false); + + function onDOMReady() { + // Reload frame after getting principal from `testURI` + if (!principalLoaded) { + element.setAttribute("src", url); + principalLoaded = true; + return; + } - let isDone = false; - let helper = { - xrayWindow: xrayWindow, - rawWindow: rawWindow, - createWorker: function (contentScript) { - return createWorker(assert, xrayWindow, contentScript, helper.done); - }, - done: function () { - if (isDone) - return; - isDone = true; - hiddenFrames.remove(hiddenFrame); - done(); - } - } - callback(helper, assert); + assert.equal(element.getAttribute("src"), url, "correct URL loaded"); + element.removeEventListener("DOMContentLoaded", onDOMReady, + false); + let xrayWindow = element.contentWindow; + let rawWindow = xrayWindow.wrappedJSObject; + + let isDone = false; + let helper = { + xrayWindow: xrayWindow, + rawWindow: rawWindow, + createWorker: function (contentScript) { + return createWorker(assert, xrayWindow, contentScript, helper.done); + }, + done: function () { + if (isDone) + return; + isDone = true; + element.parentNode.removeChild(element); + done(); } - - hiddenFrame.element.addEventListener("DOMContentLoaded", onDOMReady, false); - hiddenFrame.element.setAttribute("src", url); - - } - })); + }; + callback(helper, assert); + } }; } function createWorker(assert, xrayWindow, contentScript, done) { let loader = Loader(module); let Worker = loader.require("sdk/content/worker").Worker; let worker = Worker({ window: xrayWindow, @@ -160,19 +174,19 @@ exports["test postMessage"] = createProx // Listen without proxies, to check that it will work in regular case // simulate listening from a web document. ifWindow.addEventListener("message", function listener(event) { ifWindow.removeEventListener("message", listener, false); // As we are in system principal, event is an XrayWrapper // xrays use current compartments when calling postMessage method. // Whereas js proxies was using postMessage method compartment, // not the caller one. - assert.equal(event.source, helper.xrayWindow, - "event.source is the top window"); - assert.equal(event.origin, "null", "origin is null"); + assert.strictEqual(event.source, helper.xrayWindow, + "event.source is the top window"); + assert.equal(event.origin, testHost, "origin matches testHost"); assert.equal(event.data, "{\"foo\":\"bar\\n \\\"escaped\\\".\"}", "message data is correct"); helper.done(); }, false); helper.createWorker( @@ -211,38 +225,40 @@ exports["test Object Listener"] = create } ); }); exports["test Object Listener 2"] = createProxyTest("", function (helper) { helper.createWorker( - 'new ' + function ContentScriptScope() { + ('new ' + function ContentScriptScope() { + // variable replaced with `testHost` + let testHost = "TOKEN"; // Verify object as DOM event listener let myMessageListener = { called: false, handleEvent: function(event) { window.removeEventListener("message", myMessageListener, true); assert(this == myMessageListener, "`this` is the original object"); assert(!this.called, "called only once"); this.called = true; assert(event.target == document.defaultView, "event.target is the wrapped window"); assert(event.source == document.defaultView, "event.source is the wrapped window"); - assert(event.origin == "null", "origin is null"); + assert(event.origin == testHost, "origin matches testHost"); assert(event.data == "ok", "message data is correct"); done(); } }; window.addEventListener("message", myMessageListener, true); document.defaultView.postMessage("ok", '*'); } - ); + ).replace("TOKEN", testHost)); }); let html = '<input id="input" type="text" /><input id="input3" type="checkbox" />' + '<input id="input2" type="checkbox" />'; /* Disable test to keep tree green until Bug 756214 is fixed. exports.testStringOverload = createProxyTest(html, function (helper, test) { @@ -825,19 +841,9 @@ exports["test MutationObvserver"] = crea // Modify the DOM link.setAttribute("href", "bar"); } ); }); -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 806813"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-content-symbiont.js +++ b/addon-sdk/source/test/test-content-symbiont.js @@ -2,16 +2,17 @@ * 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/. */ "use strict"; const { Cc, Ci } = require('chrome'); const { Symbiont } = require('sdk/content/symbiont'); const self = require('sdk/self'); const { close } = require('sdk/window/helpers'); +const app = require("sdk/system/xul-app"); function makeWindow() { let content = '<?xml version="1.0"?>' + '<window ' + 'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">' + '<iframe id="content" type="content"/>' + '</window>'; @@ -61,16 +62,21 @@ exports['test:constructing symbiont && v "start", "contentScriptWhen is as specified in options." ); contentSymbiont.destroy(); }; exports["test:communication with worker global scope"] = function(assert, done) { + if (app.is('Fennec')) { + assert.pass('Test skipped on Fennec'); + done(); + } + let window = makeWindow(); let contentSymbiont; function onMessage1(message) { assert.equal(message, 1, "Program gets message via onMessage."); contentSymbiont.removeListener('message', onMessage1); contentSymbiont.on('message', onMessage2); contentSymbiont.postMessage(2); @@ -172,19 +178,9 @@ exports["test:`addon` is not available w worker.port.on('cs-to-addon', function (hasAddon) { assert.equal(hasAddon, false, "`addon` is not available"); worker.destroy(); done(); }); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 806815"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-content-worker.js +++ b/addon-sdk/source/test/test-content-worker.js @@ -1,14 +1,21 @@ /* 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/. */ "use strict"; +// Skipping due to window creation being unsupported in Fennec +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + const { Cc, Ci } = require("chrome"); const { setTimeout } = require("sdk/timers"); const { LoaderWithHookedConsole } = require("sdk/test/loader"); const { Worker } = require("sdk/content/worker"); const { close } = require("sdk/window/helpers"); const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo"; @@ -685,19 +692,9 @@ exports["test:global postMessage"] = Wor }); assert.equal(worker.url, window.location.href, "worker.url works"); worker.postMessage("hi!"); } ); -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 806817"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-context-menu.js +++ b/addon-sdk/source/test/test-context-menu.js @@ -2,16 +2,18 @@ /* vim:set ts=2 sw=2 sts=2 et: */ /* 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/. */ 'use strict'; let { Cc, Ci } = require("chrome"); +require("sdk/context-menu"); + const { Loader } = require('sdk/test/loader'); const timer = require("sdk/timers"); const { merge } = require("sdk/util/object"); // These should match the same constants in the module. const ITEM_CLASS = "addon-context-menu-item"; const SEPARATOR_CLASS = "addon-context-menu-separator"; const OVERFLOW_THRESH_DEFAULT = 10; @@ -3131,26 +3133,16 @@ exports.testSelectionInOuterFrameNoMatch test.checkMenu(items, items, []); test.done(); }); }); }; // NO TESTS BELOW THIS LINE! /////////////////////////////////////////////////// -// Run only a dummy test if context-menu doesn't support the host app. -if (!require("sdk/system/xul-app").is("Firefox")) { - module.exports = { - testAppNotSupported: function (test) { - test.pass("context-menu does not support this application."); - } - }; -} - - // This makes it easier to run tests by handling things like opening the menu, // opening new windows, making assertions, etc. Methods on |test| can be called // on instances of this class. Don't forget to call done() to end the test! // WARNING: This looks up items in popups by comparing labels, so don't give two // items the same label. function TestHelper(test) { // default waitUntilDone timeout is 10s, which is too short on the win7 // buildslave
--- a/addon-sdk/source/test/test-fs.js +++ b/addon-sdk/source/test/test-fs.js @@ -4,16 +4,17 @@ "use strict"; const { pathFor } = require("sdk/system"); const fs = require("sdk/io/fs"); const url = require("sdk/url"); const path = require("sdk/fs/path"); const { Buffer } = require("sdk/io/buffer"); +const { is } = require("sdk/system/xul-app"); // Use profile directory to list / read / write files. const profilePath = pathFor("ProfD"); const fileNameInProfile = "compatibility.ini"; const dirNameInProfile = "extensions"; const filePathInProfile = path.join(profilePath, fileNameInProfile); const dirPathInProfile = path.join(profilePath, dirNameInProfile); const mkdirPath = path.join(profilePath, "sdk-fixture-mkdir"); @@ -21,29 +22,28 @@ const writePath = path.join(profilePath, const unlinkPath = path.join(profilePath, "sdk-fixture-unlink"); const truncatePath = path.join(profilePath, "sdk-fixture-truncate"); const renameFromPath = path.join(profilePath, "sdk-fixture-rename-from"); const renameToPath = path.join(profilePath, "sdk-fixture-rename-to"); const profileEntries = [ "compatibility.ini", "extensions", - "extensions.ini", "prefs.js" // There are likely to be a lot more files but we can't really // on consistent list so we limit to this. ]; -exports["test readir"] = function(assert, end) { +exports["test readdir"] = function(assert, end) { var async = false; fs.readdir(profilePath, function(error, entries) { assert.ok(async, "readdir is async"); assert.ok(!error, "there is no error when reading directory"); assert.ok(profileEntries.length <= entries.length, - "got et least number of entries we expect"); + "got at least number of entries we expect"); assert.ok(profileEntries.every(function(entry) { return entries.indexOf(entry) >= 0; }), "all profiles are present"); end(); }); async = true; }; @@ -62,23 +62,23 @@ exports["test readdir error"] = function async = true; }; exports["test readdirSync"] = function(assert) { var async = false; var entries = fs.readdirSync(profilePath); assert.ok(profileEntries.length <= entries.length, - "got et least number of entries we expect"); + "got at least number of entries we expect"); assert.ok(profileEntries.every(function(entry) { return entries.indexOf(entry) >= 0; }), "all profiles are present"); }; -exports["test readirSync error"] = function(assert) { +exports["test readdirSync error"] = function(assert) { var async = false; var path = profilePath + "-does-not-exists"; try { fs.readdirSync(path); assert.fail(Error("No error was thrown")); } catch (error) { assert.equal(error.message, "ENOENT, readdir " + path); assert.equal(error.code, "ENOENT", "error has a code"); @@ -87,16 +87,17 @@ exports["test readirSync error"] = funct } }; exports["test readFile"] = function(assert, end) { let async = false; fs.readFile(filePathInProfile, function(error, content) { assert.ok(async, "readFile is async"); assert.ok(!error, "error is falsy"); + assert.ok(Buffer.isBuffer(content), "readFile returns buffer"); assert.ok(typeof(content.length) === "number", "buffer has length"); assert.ok(content.toString().indexOf("[Compatibility]") >= 0, "content contains expected data"); end(); }); async = true; }; @@ -333,17 +334,16 @@ exports["test fs.truncateSync fs.unlinkS exports["test fs.truncate"] = function(assert, end) { let path = truncatePath; if (!fs.existsSync(path)) { let async = false; fs.truncate(path, 0, function(error) { assert.ok(async, "truncate is async"); - console.log(error); assert.ok(!error, "no error"); assert.equal(fs.existsSync(path), true, "file was created"); fs.unlinkSync(path); assert.equal(fs.existsSync(path), false, "file was removed"); end(); }) async = true; } @@ -454,9 +454,33 @@ exports["test fs.writeFile"] = function( fs.unlinkSync(path); assert.ok(!fs.exists(path), "file was removed"); end(); }); async = true; }; +exports["test fs.writeFile (with large files)"] = function(assert, end) { + let path = writePath; + let content = ""; + + for (var i = 0; i < 100000; i++) { + content += "buffer\n"; + } + + var async = false; + fs.writeFile(path, content, function(error) { + assert.ok(async, "fs write is async"); + assert.ok(!error, "error is falsy"); + assert.ok(fs.existsSync(path), "file was created"); + assert.equal(fs.readFileSync(path).toString(), + content, + "contet was written"); + fs.unlinkSync(path); + assert.ok(!fs.exists(path), "file was removed"); + + end(); + }); + async = true; +}; + require("test").run(exports);
--- a/addon-sdk/source/test/test-indexed-db.js +++ b/addon-sdk/source/test/test-indexed-db.js @@ -3,33 +3,30 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; let xulApp = require("sdk/system/xul-app"); if (xulApp.versionInRange(xulApp.platformVersion, "16.0a1", "*")) { new function tests() { -const { indexedDB, IDBKeyRange, DOMException, IDBCursor, IDBTransaction, - IDBOpenDBRequest, IDBDatabase, IDBIndex, IDBObjectStore, IDBRequest +const { indexedDB, IDBKeyRange, DOMException } = require("sdk/indexed-db"); exports["test indexedDB is frozen"] = function(assert){ let original = indexedDB.open; let f = function(){}; assert.throws(function(){indexedDB.open = f}); assert.equal(indexedDB.open,original); assert.notEqual(indexedDB.open,f); }; exports["test db variables"] = function(assert) { - [ indexedDB, IDBKeyRange, DOMException, IDBCursor, IDBTransaction, - IDBOpenDBRequest, IDBOpenDBRequest, IDBDatabase, IDBIndex, - IDBObjectStore, IDBRequest + [ indexedDB, IDBKeyRange, DOMException ].forEach(function(value) { assert.notEqual(typeof(value), "undefined", "variable is defined"); }); } exports["test open"] = function(assert, done) { let request = indexedDB.open("MyTestDatabase"); request.onerror = function(event) {
--- a/addon-sdk/source/test/test-observer-service.js +++ b/addon-sdk/source/test/test-observer-service.js @@ -17,23 +17,23 @@ exports.testUnloadAndErrorLogging = func var badCb = function(subject, data) { throw new Error("foo"); }; sbobsvc.add("blarg", cb); observers.notify("blarg", "yo yo"); test.assertEqual(timesCalled, 1); sbobsvc.add("narg", badCb); observers.notify("narg", "yo yo"); - var lines = messages[0].split("\n"); - test.assertEqual(lines[0], "error: " + require("sdk/self").name + ": An exception occurred."); - test.assertEqual(lines[0], "error: " + require("sdk/self").name + ": An exception occurred."); - test.assertEqual(lines[1], "Error: foo"); + + test.assertEqual(messages[0], "console.error: " + require("sdk/self").name + ": \n"); + var lines = messages[1].split("\n"); + test.assertEqual(lines[0], " Message: Error: foo"); + test.assertEqual(lines[1], " Stack:"); // Keep in mind to update "18" to the line of "throw new Error("foo")" - test.assertEqual(lines[2], module.uri + " 18"); - test.assertEqual(lines[3], "Traceback (most recent call last):"); + test.assert(lines[2].indexOf(module.uri + ":18") != -1); loader.unload(); observers.notify("blarg", "yo yo"); test.assertEqual(timesCalled, 1); }; exports.testObserverService = function(test) { var ios = Cc['@mozilla.org/network/io-service;1']
--- a/addon-sdk/source/test/test-page-mod.js +++ b/addon-sdk/source/test/test-page-mod.js @@ -14,16 +14,18 @@ const windowUtils = require('sdk/depreca const { getTabContentWindow, getActiveTab, setTabURL, openTab, closeTab } = require('sdk/tabs/utils'); const xulApp = require("sdk/system/xul-app"); const { data, isPrivateBrowsingSupported } = require('sdk/self'); const { isPrivate } = require('sdk/private-browsing'); const { openWebpage } = require('./private-browsing/helper'); const { isTabPBSupported, isWindowPBSupported, isGlobalPBSupported } = require('sdk/private-browsing/utils'); const promise = require("sdk/core/promise"); const { pb } = require('./private-browsing/helper'); +const { URL } = require("sdk/url"); +const testPageURI = require("sdk/self").data.url("test.html"); /* XXX This can be used to delay closing the test Firefox instance for interactive * testing or visual inspection. This test is registered first so that it runs * the last. */ exports.delay = function(test) { if (false) { test.waitUntilDone(60000); timer.setTimeout(function() {test.done();}, 4000); @@ -114,26 +116,26 @@ exports.testPageModIncludes = function(t // so we attach it on 'start'. contentScriptWhen: 'start', onAttach: function(worker) { worker.postMessage(this.include[0]); } }; } - testPageMod(test, "about:buildconfig", [ + testPageMod(test, testPageURI, [ createPageModTest("*", false), createPageModTest("*.google.com", false), - createPageModTest("about:*", true), - createPageModTest("about:", false), - createPageModTest("about:buildconfig", true) + createPageModTest("resource:*", true), + createPageModTest("resource:", false), + createPageModTest(testPageURI, true) ], function (win, done) { - test.waitUntil(function () win.localStorage["about:buildconfig"], - "about:buildconfig page-mod to be executed") + test.waitUntil(function () win.localStorage[testPageURI], + testPageURI + " page-mod to be executed") .then(function () { asserts.forEach(function(fn) { fn(test, win); }); done(); }); } );
--- a/addon-sdk/source/test/test-page-worker.js +++ b/addon-sdk/source/test/test-page-worker.js @@ -2,16 +2,18 @@ * 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/. */ "use strict"; const { Loader } = require('sdk/test/loader'); const Pages = require("sdk/page-worker"); const Page = Pages.Page; +const { URL } = require("sdk/url"); +const testURI = require("sdk/self").data.url("test.html"); const ERR_DESTROYED = "Couldn't find the worker to receive this message. " + "The script may not be initialized yet, or may already have been unloaded."; exports.testSimplePageCreation = function(assert, done) { let page = new Page({ contentScript: "self.postMessage(window.location.href)", @@ -151,24 +153,33 @@ exports.testValidateOptions = function(a "Validation correctly denied a non-function onMessage." ); assert.pass("Options validation is working."); } exports.testContentAndAllowGettersAndSetters = function(assert, done) { let content = "data:text/html;charset=utf-8,<script>window.localStorage.allowScript=3;</script>"; + + // Load up the page with testURI initially for the resource:// principal, + // then load the actual data:* content, as data:* URIs no longer + // have localStorage let page = Page({ - contentURL: content, - contentScript: "self.postMessage(window.localStorage.allowScript)", + contentURL: testURI, + contentScript: "if (window.location.href==='"+testURI+"')" + + " self.postMessage('reload');" + + "else " + + " self.postMessage(window.localStorage.allowScript)", contentScriptWhen: "end", onMessage: step0 }); function step0(message) { + if (message === 'reload') + return page.contentURL = content; assert.equal(message, "3", "Correct value expected for allowScript - 3"); assert.equal(page.contentURL, content, "Correct content expected"); page.removeListener('message', step0); page.on('message', step1); page.allow = { script: false }; page.contentURL = content =
--- a/addon-sdk/source/test/test-places-bookmarks.js +++ b/addon-sdk/source/test/test-places-bookmarks.js @@ -20,19 +20,19 @@ const { defer: async } = require('sdk/la const { before, after } = require('sdk/test/utils'); const { Bookmark, Group, Separator, save, search, remove, MENU, TOOLBAR, UNSORTED } = require('sdk/places/bookmarks'); const { - invalidResolve, invalidReject, clearBookmarks, createTree, - compareWithHost, clearAllBookmarks, createBookmark, createBookmarkItem, - createBookmarkTree, addVisits + invalidResolve, invalidReject, createTree, + compareWithHost, createBookmark, createBookmarkItem, + createBookmarkTree, addVisits, resetPlaces } = require('./places-helper'); const { promisedEmitter } = require('sdk/places/utils'); const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1']. getService(Ci.nsINavBookmarksService); const tagsrv = Cc['@mozilla.org/browser/tagging-service;1']. getService(Ci.nsITaggingService); exports.testDefaultFolders = function (assert) { @@ -518,67 +518,73 @@ exports.testRemoveAllChildren = function }; exports.testResolution = function (assert, done) { let firstSave, secondSave; createBookmarkItem().then((item) => { firstSave = item; assert.ok(item.updated, 'bookmark has updated time'); item.title = 'my title'; - save(item).on('data', (item) => { - secondSave = item; - assert.ok(firstSave.updated < secondSave.updated, 'snapshots have different update times'); - firstSave.title = 'updated title'; - save(firstSave, { resolve: (mine, theirs) => { - assert.equal(mine.title, 'updated title', 'correct data for my object'); - assert.equal(theirs.title, 'my title', 'correct data for their object'); - assert.equal(mine.url, theirs.url, 'other data is equal'); - assert.equal(mine.group, theirs.group, 'other data is equal'); - assert.ok(mine !== firstSave, 'instance is not passed in'); - assert.ok(theirs !== secondSave, 'instance is not passed in'); - assert.equal(mine.toString(), '[object Object]', 'serialized objects'); - assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); - mine.title = 'a new title'; - return mine; - }}).on('end', (results) => { - let result = results[0]; - assert.equal(result.title, 'a new title', 'resolve handles results'); - done(); - }); - }); + // Ensure delay so a different save time is set + return delayed(item); + }).then(saveP) + .then(items => { + let item = items[0]; + secondSave = item; + assert.ok(firstSave.updated < secondSave.updated, 'snapshots have different update times'); + firstSave.title = 'updated title'; + return saveP(firstSave, { resolve: (mine, theirs) => { + assert.equal(mine.title, 'updated title', 'correct data for my object'); + assert.equal(theirs.title, 'my title', 'correct data for their object'); + assert.equal(mine.url, theirs.url, 'other data is equal'); + assert.equal(mine.group, theirs.group, 'other data is equal'); + assert.ok(mine !== firstSave, 'instance is not passed in'); + assert.ok(theirs !== secondSave, 'instance is not passed in'); + assert.equal(mine.toString(), '[object Object]', 'serialized objects'); + assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); + mine.title = 'a new title'; + return mine; + }}); + }).then((results) => { + let result = results[0]; + assert.equal(result.title, 'a new title', 'resolve handles results'); + done(); }); }; /* * Same as the resolution test, but with the 'unsaved' snapshot */ exports.testResolutionMapping = function (assert, done) { let bookmark = Bookmark({ title: 'moz', url: 'http://bookmarks4life.com/' }); - save(bookmark).on('end', (saved) => { - saved = saved[0]; + let saved; + saveP(bookmark).then(data => { + saved = data[0]; saved.title = 'updated title'; - save(saved).on('end', () => { - bookmark.title = 'conflicting title'; - save(bookmark, { resolve: (mine, theirs) => { - assert.equal(mine.title, 'conflicting title', 'correct data for my object'); - assert.equal(theirs.title, 'updated title', 'correct data for their object'); - assert.equal(mine.url, theirs.url, 'other data is equal'); - assert.equal(mine.group, theirs.group, 'other data is equal'); - assert.ok(mine !== bookmark, 'instance is not passed in'); - assert.ok(theirs !== saved, 'instance is not passed in'); - assert.equal(mine.toString(), '[object Object]', 'serialized objects'); - assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); - mine.title = 'a new title'; - return mine; - }}).on('end', (results) => { - let result = results[0]; - assert.equal(result.title, 'a new title', 'resolve handles results'); - done(); - }); - }); + // Ensure a delay for different updated times + return delayed(saved); + }).then(saveP) + .then(() => { + bookmark.title = 'conflicting title'; + return saveP(bookmark, { resolve: (mine, theirs) => { + assert.equal(mine.title, 'conflicting title', 'correct data for my object'); + assert.equal(theirs.title, 'updated title', 'correct data for their object'); + assert.equal(mine.url, theirs.url, 'other data is equal'); + assert.equal(mine.group, theirs.group, 'other data is equal'); + assert.ok(mine !== bookmark, 'instance is not passed in'); + assert.ok(theirs !== saved, 'instance is not passed in'); + assert.equal(mine.toString(), '[object Object]', 'serialized objects'); + assert.equal(theirs.toString(), '[object Object]', 'serialized objects'); + mine.title = 'a new title'; + return mine; + }}); + }).then((results) => { + let result = results[0]; + assert.equal(result.title, 'a new title', 'resolve handles results'); + done(); }); }; exports.testUpdateTags = function (assert, done) { createBookmarkItem({ tags: ['spidermonkey'] }).then(bookmark => { bookmark.tags.add('jagermonkey'); bookmark.tags.add('ionmonkey'); bookmark.tags.delete('spidermonkey'); @@ -936,24 +942,26 @@ exports.testCheckSaveOrder = function (a saveP(bookmarks).then(results => { for (let i = 0; i < bookmarks.length; i++) assert.equal(results[i].url, bookmarks[i].url, 'correct ordering of bookmark results'); done(); }); }; -before(exports, name => { - clearAllBookmarks(); -}); - -after(exports, name => { - clearAllBookmarks(); -}); +before(exports, (name, assert, done) => resetPlaces(done)); +after(exports, (name, assert, done) => resetPlaces(done)); function saveP () { return promisedEmitter(save.apply(null, Array.slice(arguments))); } function searchP () { return promisedEmitter(search.apply(null, Array.slice(arguments))); } + +function delayed (value, ms) { + let { promise, resolve } = defer(); + setTimeout(() => resolve(value), ms || 10); + return promise; +} + require('test').run(exports);
--- a/addon-sdk/source/test/test-places-favicon.js +++ b/addon-sdk/source/test/test-places-favicon.js @@ -14,17 +14,17 @@ const { Cc, Ci, Cu } = require('chrome') const { getFavicon } = require('sdk/places/favicon'); const tabs = require('sdk/tabs'); const open = tabs.open; const port = 8099; const host = 'http://localhost:' + port; const { onFaviconChange, serve, binFavicon } = require('./favicon-helpers'); const { once } = require('sdk/system/events'); const { defer } = require('sdk/core/promise'); -const { clearHistory } = require('./places-helper'); +const { resetPlaces } = require('./places-helper'); const faviconService = Cc["@mozilla.org/browser/favicon-service;1"]. getService(Ci.nsIFaviconService); exports.testStringGetFaviconCallbackSuccess = function (assert, done) { let name = 'callbacksuccess' let srv = makeServer(name); let url = host + '/' + name + '.html'; let favicon = host + '/' + name + '.ico'; @@ -176,15 +176,15 @@ function waitAndExpire (url) { }); faviconService.expireAllFavicons(); }); return deferred.promise; } function complete(tab, srv, done) { tab.close(function () { - clearHistory(() => { + resetPlaces(() => { srv.stop(done); }); }); } require("test").run(exports);
--- a/addon-sdk/source/test/test-places-history.js +++ b/addon-sdk/source/test/test-places-history.js @@ -9,26 +9,25 @@ module.metadata = { } }; const { Cc, Ci } = require('chrome'); const { defer, all } = require('sdk/core/promise'); const { has } = require('sdk/util/array'); const { setTimeout } = require('sdk/timers'); const { before, after } = require('sdk/test/utils'); +const { set } = require('sdk/preferences/service'); const { search } = require('sdk/places/history'); const { - invalidResolve, invalidReject, clearBookmarks, createTree, - compareWithHost, clearAllBookmarks, addVisits, clearHistory + invalidResolve, invalidReject, createTree, + compareWithHost, addVisits, resetPlaces } = require('./places-helper'); const { promisedEmitter } = require('sdk/places/utils'); -const hsrv = Cc['@mozilla.org/browser/nav-history-service;1']. - getService(Ci.nsINavHistoryService); exports.testEmptyQuery = function (assert, done) { let within = toBeWithin(); addVisits([ 'http://simplequery-1.com', 'http://simplequery-2.com' ]).then(searchP).then(results => { assert.equal(results.length, 2, 'Correct number of entries returned'); assert.equal(results[0].url, 'http://simplequery-1.com/', @@ -234,21 +233,16 @@ exports.testEmitters = function (assert, function toBeWithin (range) { range = range || 2000; var current = new Date() * 1000; // convert to microseconds return compared => { return compared - current < range; }; } -function clear (done) { - clearAllBookmarks(); - clearHistory(done); -} - function searchP () { return promisedEmitter(search.apply(null, Array.slice(arguments))); } -before(exports, (name, assert, done) => clear(done)); -after(exports, (name, assert, done) => clear(done)); +before(exports, (name, assert, done) => resetPlaces(done)); +after(exports, (name, assert, done) => resetPlaces(done)); require('test').run(exports);
--- a/addon-sdk/source/test/test-places-host.js +++ b/addon-sdk/source/test/test-places-host.js @@ -9,32 +9,33 @@ module.metadata = { } }; const { Cc, Ci } = require('chrome'); const { defer, all } = require('sdk/core/promise'); const { setTimeout } = require('sdk/timers'); const { newURI } = require('sdk/url/utils'); const { send } = require('sdk/addon/events'); +const { set } = require('sdk/preferences/service'); +const { before, after } = require('sdk/test/utils'); require('sdk/places/host/host-bookmarks'); require('sdk/places/host/host-tags'); require('sdk/places/host/host-query'); const { - invalidResolve, invalidReject, clearBookmarks, createTree, - compareWithHost, clearAllBookmarks, createBookmark, createBookmarkTree + invalidResolve, invalidReject, createTree, + compareWithHost, createBookmark, createBookmarkTree, resetPlaces } = require('./places-helper'); const bmsrv = Cc['@mozilla.org/browser/nav-bookmarks-service;1']. getService(Ci.nsINavBookmarksService); const hsrv = Cc['@mozilla.org/browser/nav-history-service;1']. getService(Ci.nsINavHistoryService); const tagsrv = Cc['@mozilla.org/browser/tagging-service;1']. getService(Ci.nsITaggingService); -clearAllBookmarks(); exports.testBookmarksCreate = function (assert, done) { let items = [{ title: 'my title', url: 'http://moz.com', tags: ['some', 'tags', 'yeah'], type: 'bookmark' }, { @@ -46,17 +47,16 @@ exports.testBookmarksCreate = function ( group: bmsrv.unfiledBookmarksFolder }]; all(items.map(function (item) { return send('sdk-places-bookmarks-create', item).then(function (data) { compareWithHost(assert, data); }, invalidReject(assert)); })).then(function () { - clearAllBookmarks(); done(); }, invalidReject(assert)); }; exports.testBookmarksCreateFail = function (assert, done) { let items = [{ title: 'my title', url: 'not-a-url', @@ -67,17 +67,16 @@ exports.testBookmarksCreateFail = functi }, { group: bmsrv.unfiledBookmarksFolder }]; all(items.map(function (item) { return send('sdk-places-bookmarks-create', item).then(null, function (reason) { assert.ok(reason, 'bookmark create should fail'); }); })).then(function () { - clearAllBookmarks(); done(); }); }; exports.testBookmarkLastUpdated = function (assert, done) { let timestamp; let item; createBookmark().then(function (data) { @@ -89,33 +88,31 @@ exports.testBookmarkLastUpdated = functi item.title = 'updated mozilla'; return send('sdk-places-bookmarks-save', item).then(function (data) { let deferred = defer(); setTimeout(function () deferred.resolve(data), 100); return deferred.promise; }); }).then(function (data) { assert.ok(data.updated > timestamp, 'time has elapsed and updated the updated property'); - clearAllBookmarks(); done(); }); }; exports.testBookmarkRemove = function (assert, done) { let id; createBookmark().then(function (data) { id = data.id; compareWithHost(assert, data); // ensure bookmark exists bmsrv.getItemTitle(id); // does not throw an error return send('sdk-places-bookmarks-remove', data); }).then(function () { assert.throws(function () { bmsrv.getItemTitle(id); }, 'item should no longer exist'); - clearAllBookmarks(); done(); }, console.error); }; exports.testBookmarkGet = function (assert, done) { let bookmark; createBookmark().then(function (data) { bookmark = data; @@ -128,17 +125,16 @@ exports.testBookmarkGet = function (asse 'correctly fetched tag ' + tag); } assert.equal(bookmark.tags.length, data.tags.length, 'same amount of tags'); } else assert.equal(bookmark[prop], data[prop], 'correctly fetched ' + prop); }); - clearAllBookmarks(); done(); }); }; exports.testTagsTag = function (assert, done) { let url; createBookmark().then(function (data) { url = data.url; @@ -146,17 +142,16 @@ exports.testTagsTag = function (assert, url: data.url, tags: ['mozzerella', 'foxfire'] }); }).then(function () { let tags = tagsrv.getTagsForURI(newURI(url)); assert.ok(~tags.indexOf('mozzerella'), 'first tag found'); assert.ok(~tags.indexOf('foxfire'), 'second tag found'); assert.ok(~tags.indexOf('firefox'), 'default tag found'); assert.equal(tags.length, 3, 'no extra tags'); - clearAllBookmarks(); done(); }); }; exports.testTagsUntag = function (assert, done) { let item; createBookmark({tags: ['tag1', 'tag2', 'tag3']}).then(function (data) { item = data; @@ -166,49 +161,46 @@ exports.testTagsUntag = function (assert }); }).then(function () { let tags = tagsrv.getTagsForURI(newURI(item.url)); assert.ok(~tags.indexOf('tag1'), 'first tag persisted'); assert.ok(~tags.indexOf('tag3'), 'second tag persisted'); assert.ok(!~tags.indexOf('firefox'), 'first tag removed'); assert.ok(!~tags.indexOf('tag2'), 'second tag removed'); assert.equal(tags.length, 2, 'no extra tags'); - clearAllBookmarks(); done(); }); }; exports.testTagsGetURLsByTag = function (assert, done) { let item; createBookmark().then(function (data) { item = data; return send('sdk-places-tags-get-urls-by-tag', { tag: 'firefox' }); }).then(function(urls) { assert.equal(item.url, urls[0], 'returned correct url'); assert.equal(urls.length, 1, 'returned only one url'); - clearAllBookmarks(); done(); }); }; exports.testTagsGetTagsByURL = function (assert, done) { let item; createBookmark({ tags: ['firefox', 'mozilla', 'metal']}).then(function (data) { item = data; return send('sdk-places-tags-get-tags-by-url', { url: data.url, }); }).then(function(tags) { assert.ok(~tags.indexOf('firefox'), 'returned first tag'); assert.ok(~tags.indexOf('mozilla'), 'returned second tag'); assert.ok(~tags.indexOf('metal'), 'returned third tag'); assert.equal(tags.length, 3, 'returned all tags'); - clearAllBookmarks(); done(); }); }; exports.testHostQuery = function (assert, done) { all([ createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }), createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }), @@ -223,17 +215,16 @@ exports.testHostQuery = function (assert assert.equal(results[0].url, 'http://mozilla.com/', 'is sorted by URI asc'); return send('sdk-places-query', { queries: { tags: ['mozilla'] }, options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only }); }).then(results => { assert.equal(results.length, 2, 'should only return two'); assert.equal(results[0].url, 'http://firefox.com/', 'is sorted by URI desc'); - clearAllBookmarks(); done(); }); }; exports.testHostMultiQuery = function (assert, done) { all([ createBookmark({ url: 'http://firefox.com', tags: ['firefox', 'mozilla'] }), createBookmark({ url: 'http://mozilla.com', tags: ['mozilla'] }), @@ -248,37 +239,38 @@ exports.testHostMultiQuery = function (a assert.equal(results[0].url, 'http://firefox.com/', 'should match URL or tag'); assert.equal(results[1].url, 'http://thunderbird.com/', 'should match URL or tag'); return send('sdk-places-query', { queries: [{ tags: ['firefox'], url: 'http://mozilla.com/' }], options: { sortingMode: 5, queryType: 1 } // sort by URI descending, bookmarks only }); }).then(results => { assert.equal(results.length, 0, 'query props should be AND\'d'); - clearAllBookmarks(); done(); }); }; exports.testGetAllBookmarks = function (assert, done) { createBookmarkTree().then(() => { return send('sdk-places-bookmarks-get-all', {}); }).then(res => { assert.equal(res.length, 8, 'all bookmarks returned'); - clearAllBookmarks(); done(); }, console.error); }; exports.testGetAllChildren = function (assert, done) { createBookmarkTree().then(results => { return send('sdk-places-bookmarks-get-children', { id: results.filter(({title}) => title === 'mozgroup')[0].id }); }).then(results => { assert.equal(results.length, 5, 'should return all children and folders at a single depth'); - clearAllBookmarks(); done(); }); }; + +before(exports, (name, assert, done) => resetPlaces(done)); +after(exports, (name, assert, done) => resetPlaces(done)); + require('test').run(exports);
--- a/addon-sdk/source/test/test-plain-text-console.js +++ b/addon-sdk/source/test/test-plain-text-console.js @@ -31,121 +31,134 @@ exports.testPlainTextConsole = function( prefs.reset(ADDON_LOG_LEVEL_PREF); var Console = require("sdk/console/plain-text").PlainTextConsole; var con = new Console(print); test.pass("PlainTextConsole instantiates"); con.log('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "info: " + name + ": testing 1 2,3,4\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing, 1, Array [2,3,4]\n", "PlainTextConsole.log() must work."); con.info('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "info: " + name + ": testing 1 2,3,4\n", + test.assertEqual(lastPrint(), "console.info: " + name + ": testing, 1, Array [2,3,4]\n", "PlainTextConsole.info() must work."); con.warn('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "warn: " + name + ": testing 1 2,3,4\n", + test.assertEqual(lastPrint(), "console.warn: " + name + ": testing, 1, Array [2,3,4]\n", "PlainTextConsole.warn() must work."); con.error('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "error: " + name + ": testing 1 2,3,4\n", + test.assertEqual(prints[0], "console.error: " + name + ": \n", "PlainTextConsole.error() must work."); + test.assertEqual(prints[1], " testing\n") + test.assertEqual(prints[2], " 1\n") + test.assertEqual(prints[3], "Array\n - 0 = 2\n - 1 = 3\n - 2 = 4\n - length = 3\n"); + prints = []; con.debug('testing', 1, [2, 3, 4]); - test.assertEqual(lastPrint(), "debug: " + name + ": testing 1 2,3,4\n", + test.assertEqual(prints[0], "console.debug: " + name + ": \n", "PlainTextConsole.debug() must work."); + test.assertEqual(prints[1], " testing\n") + test.assertEqual(prints[2], " 1\n") + test.assertEqual(prints[3], "Array\n - 0 = 2\n - 1 = 3\n - 2 = 4\n - length = 3\n"); + prints = []; con.log('testing', undefined); - test.assertEqual(lastPrint(), "info: " + name + ": testing undefined\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing, undefined\n", "PlainTextConsole.log() must stringify undefined."); con.log('testing', null); - test.assertEqual(lastPrint(), "info: " + name + ": testing null\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing, null\n", "PlainTextConsole.log() must stringify null."); + // TODO: Fix console.jsm to detect custom toString. con.log("testing", { toString: function() "obj.toString()" }); - test.assertEqual(lastPrint(), "info: " + name + ": testing obj.toString()\n", - "PlainTextConsole.log() must stringify custom toString."); + test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n", + "PlainTextConsole.log() doesn't printify custom toString."); con.log("testing", { toString: function() { throw "fail!"; } }); - test.assertEqual(lastPrint(), "info: " + name + ": testing <toString() error>\n", + test.assertEqual(lastPrint(), "console.log: " + name + ": testing, {}\n", "PlainTextConsole.log() must stringify custom bad toString."); + con.exception(new Error("blah")); - var tbLines = prints[0].split("\n"); - test.assertEqual(tbLines[0], "error: " + name + ": An exception occurred."); - test.assertEqual(tbLines[1], "Error: blah"); - test.assertEqual(tbLines[2], module.uri + " 74"); - test.assertEqual(tbLines[3], "Traceback (most recent call last):"); + + test.assertEqual(prints[0], "console.error: " + name + ": \n"); + let tbLines = prints[1].split("\n"); + test.assertEqual(tbLines[0], " Message: Error: blah"); + test.assertEqual(tbLines[1], " Stack:"); + test.assert(prints[1].indexOf(module.uri + ":84") !== -1); + prints = [] - prints = []; try { loadSubScript("invalid-url", {}); test.fail("successed in calling loadSubScript with invalid-url"); } catch(e) { con.exception(e); } - var tbLines = prints[0].split("\n"); - test.assertEqual(tbLines[0], "error: " + name + ": An exception occurred."); - test.assertEqual(tbLines[1], "Error creating URI (invalid URL scheme?)"); - test.assertEqual(tbLines[2], "Traceback (most recent call last):"); + test.assertEqual(prints[0], "console.error: " + name + ": \n"); + test.assertEqual(prints[1], " Error creating URI (invalid URL scheme?)\n"); + prints = []; + con.trace(); + let tbLines = prints[0].split("\n"); + test.assertEqual(tbLines[0], "console.trace: " + name + ": "); + test.assert(tbLines[1].indexOf("_ain-text-console.js 105") == 0); prints = []; - con.trace(); - tbLines = prints[0].split("\n"); - test.assertEqual(tbLines[0], "info: " + name + ": Traceback (most recent call last):"); - test.assertEqual(tbLines[tbLines.length - 4].trim(), "con.trace();"); // Whether or not console methods should print at the various log levels, // structured as a hash of levels, each of which contains a hash of methods, // each of whose value is whether or not it should print, i.e.: // { [level]: { [method]: [prints?], ... }, ... }. let levels = { all: { debug: true, log: true, info: true, warn: true, error: true }, debug: { debug: true, log: true, info: true, warn: true, error: true }, info: { debug: false, log: true, info: true, warn: true, error: true }, warn: { debug: false, log: false, info: false, warn: true, error: true }, error: { debug: false, log: false, info: false, warn: false, error: true }, off: { debug: false, log: false, info: false, warn: false, error: false }, }; // The messages we use to test the various methods, as a hash of methods. let messages = { - debug: "debug: " + name + ": \n", - log: "info: " + name + ": \n", - info: "info: " + name + ": \n", - warn: "warn: " + name + ": \n", - error: "error: " + name + ": \n", + debug: "console.debug: " + name + ": \n \n", + log: "console.log: " + name + ": \n", + info: "console.info: " + name + ": \n", + warn: "console.warn: " + name + ": \n", + error: "console.error: " + name + ": \n \n", }; for (let level in levels) { let methods = levels[level]; for (let method in methods) { // We have to reset the log level pref each time we run the test // because the test runner relies on the console to print test output, // and test results would not get printed to the console for some // values of the pref. prefs.set(SDK_LOG_LEVEL_PREF, level); con[method](""); prefs.set(SDK_LOG_LEVEL_PREF, "all"); - test.assertEqual(lastPrint(), (methods[method] ? messages[method] : null), + test.assertEqual(prints.join(""), + (methods[method] ? messages[method] : ""), "at log level '" + level + "', " + method + "() " + (methods[method] ? "prints" : "doesn't print")); + prints = []; } } prefs.set(SDK_LOG_LEVEL_PREF, "off"); prefs.set(ADDON_LOG_LEVEL_PREF, "all"); con.debug(""); - test.assertEqual(lastPrint(), messages["debug"], + test.assertEqual(prints.join(""), messages["debug"], "addon log level 'all' overrides SDK log level 'off'"); + prints = []; prefs.set(SDK_LOG_LEVEL_PREF, "all"); prefs.set(ADDON_LOG_LEVEL_PREF, "off"); con.error(""); prefs.reset(ADDON_LOG_LEVEL_PREF); test.assertEqual(lastPrint(), null, "addon log level 'off' overrides SDK log level 'all'");
--- a/addon-sdk/source/test/test-system-events.js +++ b/addon-sdk/source/test/test-system-events.js @@ -5,16 +5,18 @@ const events = require("sdk/system/events"); const self = require("sdk/self"); const { Cc, Ci, Cu } = require("chrome"); const { setTimeout } = require("sdk/timers"); const { Loader, LoaderWithHookedConsole2 } = require("sdk/test/loader"); const nsIObserverService = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); +let isConsoleEvent = (topic) => + !!~["console-api-log-event", "console-storage-cache-event"].indexOf(topic) exports["test basic"] = function(assert) { let type = Date.now().toString(32); let timesCalled = 0; function handler(subject, data) { timesCalled++; }; events.on(type, handler); @@ -43,20 +45,20 @@ exports["test error reporting"] = functi let lineNumber; try { brokenHandler() } catch (error) { lineNumber = error.lineNumber } let errorType = Date.now().toString(32); events.on(errorType, brokenHandler); events.emit(errorType, { data: "yo yo" }); - assert.equal(messages.length, 1, "Got an exception"); - let text = messages[0]; - assert.ok(text.indexOf(self.name + ": An exception occurred.") >= 0, - "error is logged"); + assert.equal(messages.length, 2, "Got an exception"); + assert.equal(messages[0], "console.error: " + self.name + ": \n", + "error is logged"); + let text = messages[1]; assert.ok(text.indexOf("Error: foo") >= 0, "error message is logged"); assert.ok(text.indexOf(module.uri) >= 0, "module uri is logged"); assert.ok(text.indexOf(lineNumber) >= 0, "error line is logged"); events.off(errorType, brokenHandler); loader.unload(); }; @@ -99,16 +101,19 @@ exports["test handle nsIObserverService let type = Date.now().toString(32); let timesCalled = 0; let lastSubject = null; let lastData = null; let lastType = null; function handler({ subject, data, type }) { + // Ignores internal console events + if (isConsoleEvent(type)) + return; timesCalled++; lastSubject = subject; lastData = data; lastType = type; }; events.on(type, handler); nsIObserverService.notifyObservers(uri, type, "some data"); @@ -163,33 +168,35 @@ exports["test emit to nsIObserverService let lastTopic = null; var topic = Date.now().toString(32) let nsIObserver = { QueryInterface: function() { return nsIObserver; }, observe: function(subject, topic, data) { + // Ignores internal console events + if (isConsoleEvent(topic)) + return; timesCalled = timesCalled + 1; lastSubject = subject; lastData = data; lastTopic = topic; } }; nsIObserverService.addObserver(nsIObserver, topic, false); events.emit(topic, { subject: uri, data: "some data" }); assert.equal(timesCalled, 1, "emit notifies observers"); assert.equal(lastTopic, topic, "event type is notification topic"); assert.equal(lastSubject.wrappedJSObject.object, uri, "event.subject is notification subject"); assert.equal(lastData, "some data", "event.data is notification data"); - function customSubject() {} function customData() {} events.emit(topic, { subject: customSubject, data: customData }); assert.equal(timesCalled, 2, "emit notifies observers"); assert.equal(lastTopic, topic, "event.type is notification"); assert.equal(lastSubject.wrappedJSObject.object, customSubject, "event.subject is notification subject"); @@ -201,20 +208,21 @@ exports["test emit to nsIObserverService assert.equal(timesCalled, 2, "removed observers no longer invoked"); nsIObserverService.addObserver(nsIObserver, "*", false); events.emit(topic, { data: "data again" }); assert.equal(timesCalled, 3, "emit notifies * observers"); + assert.equal(lastTopic, topic, "event.type is notification"); assert.equal(lastSubject, null, "event.subject is notification subject"); assert.equal(lastData, "data again", "event.data is notification data"); nsIObserverService.removeObserver(nsIObserver, "*"); - + events.emit(topic, { data: "last data" }); assert.equal(timesCalled, 3, "removed observers no longer invoked"); } require("test").run(exports);
--- a/addon-sdk/source/test/test-tab-events.js +++ b/addon-sdk/source/test/test-tab-events.js @@ -5,171 +5,234 @@ "use strict"; const { Loader } = require("sdk/test/loader"); const utils = require("sdk/tabs/utils"); const { open, close } = require("sdk/window/helpers"); const { getMostRecentBrowserWindow } = require("sdk/window/utils"); const { events } = require("sdk/tab/events"); const { on, off } = require("sdk/event/core"); -const { resolve } = require("sdk/core/promise"); +const { resolve, defer } = require("sdk/core/promise"); let isFennec = require("sdk/system/xul-app").is("Fennec"); -function test(scenario, currentWindow) { - let useActiveWindow = isFennec || currentWindow; +function test(options) { return function(assert, done) { - let actual = []; - function handler(event) actual.push(event) + let tabEvents = []; + let tabs = []; + let { promise, resolve: resolveP } = defer(); + let win = isFennec ? resolve(getMostRecentBrowserWindow()) : + open(null, { + features: { private: true, toolbar:true, chrome: true } + }); + let window = null; - let win = useActiveWindow ? resolve(getMostRecentBrowserWindow()) : - open(null, { - features: { private: true, toolbar:true, chrome: true } - }); - let window = null; + // Firefox events are fired sync; Fennec events async + // this normalizes the tests + function handler (event) { + tabEvents.push(event); + runIfReady(); + } + + function runIfReady () { + let releventEvents = getRelatedEvents(tabEvents, tabs); + if (options.readyWhen(releventEvents)) + options.end({ + tabs: tabs, + events: releventEvents, + assert: assert, + done: resolveP + }); + } win.then(function(w) { window = w; on(events, "data", handler); - return scenario(assert, window, actual); + options.start({ tabs: tabs, window: window }); + + // Execute here for synchronous FF events, as the handlers + // were called before tabs were pushed to `tabs` + runIfReady(); + return promise; }).then(function() { off(events, "data", handler); - return useActiveWindow ? null : close(window); + return isFennec ? null : close(window); }).then(done, assert.fail); - } + }; } -exports["test current window"] = test(function(assert, window, events) { - // Just making sure that tab events work for already opened tabs not only - // for new windows. - let tab = utils.openTab(window, 'data:text/plain,open'); - utils.closeTab(tab); - - let [open, select, close] = events; - - assert.equal(open.type, "TabOpen"); - assert.equal(open.target, tab); - - assert.equal(select.type, "TabSelect"); - assert.equal(select.target, tab); +// Just making sure that tab events work for already opened tabs not only +// for new windows. +exports["test current window"] = test({ + readyWhen: events => events.length === 3, + start: ({ tabs, window }) => { + let tab = utils.openTab(window, 'data:text/plain,open'); + tabs.push(tab); + utils.closeTab(tab); + }, + end: ({ tabs, events, assert, done }) => { + let [open, select, close] = events; + let tab = tabs[0]; - assert.equal(close.type, "TabClose"); - assert.equal(close.target, tab); -}); + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); -exports["test open"] = test(function(assert, window, events) { - let tab = utils.openTab(window, 'data:text/plain,open'); - let [open, select] = events; + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); - assert.equal(open.type, "TabOpen"); - assert.equal(open.target, tab); - - assert.equal(select.type, "TabSelect"); - assert.equal(select.target, tab); + assert.equal(close.type, "TabClose"); + assert.equal(close.target, tab); + done(); + } }); -exports["test open -> close"] = test(function(assert, window, events) { - // First tab is useless we just open it so that closing second tab won't - // close window on some platforms. - let _ = utils.openTab(window, 'daat:text/plain,ignore'); - let tab = utils.openTab(window, 'data:text/plain,open-close'); - utils.closeTab(tab); - - let [_open, _select, open, select, close] = events; +exports["test open"] = test({ + readyWhen: events => events.length === 2, + start: ({ tabs, window }) => { + tabs.push(utils.openTab(window, 'data:text/plain,open')); + }, + end: ({ tabs, events, assert, done }) => { + let [open, select] = events; + let tab = tabs[0]; - assert.equal(open.type, "TabOpen"); - assert.equal(open.target, tab); + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); - assert.equal(select.type, "TabSelect"); - assert.equal(select.target, tab); - - assert.equal(close.type, "TabClose"); - assert.equal(close.target, tab); + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); + done(); + } }); -exports["test open -> open -> select"] = test(function(assert, window, events) { - let tab1 = utils.openTab(window, 'data:text/plain,Tab-1'); - let tab2 = utils.openTab(window, 'data:text/plain,Tab-2'); - utils.activateTab(tab1, window); - - let [open1, select1, open2, select2, select3] = events; - - // Open first tab - assert.equal(open1.type, "TabOpen", "first tab opened") - assert.equal(open1.target, tab1, "event.target is first tab") - - assert.equal(select1.type, "TabSelect", "first tab seleceted") - assert.equal(select1.target, tab1, "event.target is first tab") - - - // Open second tab - assert.equal(open2.type, "TabOpen", "second tab opened"); - assert.equal(open2.target, tab2, "event.target is second tab"); - - assert.equal(select2.type, "TabSelect", "second tab seleceted"); - assert.equal(select2.target, tab2, "event.target is second tab"); +exports["test open -> close"] = test({ + readyWhen: events => events.length === 3, + start: ({ tabs, window }) => { + // First tab is useless we just open it so that closing second tab won't + // close window on some platforms. + utils.openTab(window, 'data:text/plain,ignore'); + let tab = utils.openTab(window, 'data:text/plain,open-close'); + tabs.push(tab); + utils.closeTab(tab); + }, + end: ({ tabs, events, assert, done }) => { + let [open, select, close] = events; + let tab = tabs[0]; - // Select first tab - assert.equal(select3.type, "TabSelect", "tab seleceted"); - assert.equal(select3.target, tab1, "event.target is first tab"); -}); - -exports["test open -> pin -> unpin"] = test(function(assert, window, events) { - let tab = utils.openTab(window, 'data:text/plain,pin-unpin'); - utils.pin(tab); - utils.unpin(tab); - - let [open, select, move, pin, unpin] = events; - - assert.equal(open.type, "TabOpen"); - assert.equal(open.target, tab); + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); - assert.equal(select.type, "TabSelect"); - assert.equal(select.target, tab); + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); - if (isFennec) { - assert.pass("Tab pin / unpin is not supported by Fennec"); - } - else { - assert.equal(move.type, "TabMove"); - assert.equal(move.target, tab); - - assert.equal(pin.type, "TabPinned"); - assert.equal(pin.target, tab); - - assert.equal(unpin.type, "TabUnpinned"); - assert.equal(unpin.target, tab); + assert.equal(close.type, "TabClose"); + assert.equal(close.target, tab); + done(); } }); -exports["test open -> open -> move "] = test(function(assert, window, events) { - let tab1 = utils.openTab(window, 'data:text/plain,Tab-1'); - let tab2 = utils.openTab(window, 'data:text/plain,Tab-2'); - utils.move(tab1, 2); - - let [open1, select1, open2, select2, move] = events; - - // Open first tab - assert.equal(open1.type, "TabOpen", "first tab opened"); - assert.equal(open1.target, tab1, "event.target is first tab"); - - assert.equal(select1.type, "TabSelect", "first tab seleceted") - assert.equal(select1.target, tab1, "event.target is first tab"); - +exports["test open -> open -> select"] = test({ + readyWhen: events => events.length === 5, + start: ({tabs, window}) => { + tabs.push(utils.openTab(window, 'data:text/plain,Tab-1')); + tabs.push(utils.openTab(window, 'data:text/plain,Tab-2')); + utils.activateTab(tabs[0], window); + }, + end: ({ tabs, events, assert, done }) => { + let [ tab1, tab2 ] = tabs; + let tab1Events = 0; + getRelatedEvents(events, tab1).map(event => { + tab1Events++; + if (tab1Events === 1) + assert.equal(event.type, "TabOpen", "first tab opened"); + else + assert.equal(event.type, "TabSelect", "first tab selected"); + assert.equal(event.target, tab1); + }); + assert.equal(tab1Events, 3, "first tab has 3 events"); - // Open second tab - assert.equal(open2.type, "TabOpen", "second tab opened"); - assert.equal(open2.target, tab2, "event.target is second tab"); - - assert.equal(select2.type, "TabSelect", "second tab seleceted"); - assert.equal(select2.target, tab2, "event.target is second tab"); - - if (isFennec) { - assert.pass("Tab index changes not supported on Fennec yet") - } - else { - // Move first tab - assert.equal(move.type, "TabMove", "tab moved"); - assert.equal(move.target, tab1, "event.target is first tab"); + let tab2Opened; + getRelatedEvents(events, tab2).map(event => { + if (!tab2Opened) + assert.equal(event.type, "TabOpen", "second tab opened"); + else + assert.equal(event.type, "TabSelect", "second tab selected"); + tab2Opened = true; + assert.equal(event.target, tab2); + }); + done(); } }); -require("test").run(exports); +exports["test open -> pin -> unpin"] = test({ + readyWhen: events => events.length === (isFennec ? 2 : 5), + start: ({ tabs, window }) => { + tabs.push(utils.openTab(window, 'data:text/plain,pin-unpin')); + utils.pin(tabs[0]); + utils.unpin(tabs[0]); + }, + end: ({ tabs, events, assert, done }) => { + let [open, select, move, pin, unpin] = events; + let tab = tabs[0]; + + assert.equal(open.type, "TabOpen"); + assert.equal(open.target, tab); + + assert.equal(select.type, "TabSelect"); + assert.equal(select.target, tab); + + if (isFennec) { + assert.pass("Tab pin / unpin is not supported by Fennec"); + } + else { + assert.equal(move.type, "TabMove"); + assert.equal(move.target, tab); + + assert.equal(pin.type, "TabPinned"); + assert.equal(pin.target, tab); + + assert.equal(unpin.type, "TabUnpinned"); + assert.equal(unpin.target, tab); + } + done(); + } +}); + +exports["test open -> open -> move "] = test({ + readyWhen: events => events.length === (isFennec ? 4 : 5), + start: ({tabs, window}) => { + tabs.push(utils.openTab(window, 'data:text/plain,Tab-1')); + tabs.push(utils.openTab(window, 'data:text/plain,Tab-2')); + utils.move(tabs[0], 2); + }, + end: ({ tabs, events, assert, done }) => { + let [ tab1, tab2 ] = tabs; + let tab1Events = 0; + getRelatedEvents(events, tab1).map(event => { + tab1Events++; + if (tab1Events === 1) + assert.equal(event.type, "TabOpen", "first tab opened"); + else if (tab1Events === 2) + assert.equal(event.type, "TabSelect", "first tab selected"); + else if (tab1Events === 3 && isFennec) + assert.equal(event.type, "TabMove", "first tab moved"); + assert.equal(event.target, tab1); + }); + assert.equal(tab1Events, isFennec ? 2 : 3, + "correct number of events for first tab"); + + let tab2Events = 0; + getRelatedEvents(events, tab2).map(event => { + tab2Events++; + if (tab2Events === 1) + assert.equal(event.type, "TabOpen", "second tab opened"); + else + assert.equal(event.type, "TabSelect", "second tab selected"); + assert.equal(event.target, tab2); + }); + done(); + } +}); + +function getRelatedEvents (events, tabs) { + return events.filter(({target}) => ~([].concat(tabs)).indexOf(target)); +} + +require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-tab-observer.js +++ b/addon-sdk/source/test/test-tab-observer.js @@ -1,14 +1,21 @@ /* 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/. */ "use strict"; +// TODO Fennec support in Bug #894525 +module.metadata = { + "engines": { + "Firefox": "*" + } +} + const { openTab, closeTab } = require("sdk/tabs/utils"); const { Loader } = require("sdk/test/loader"); const { setTimeout } = require("sdk/timers"); exports["test unload tab observer"] = function(assert, done) { let loader = Loader(module); let window = loader.require("sdk/deprecated/window-utils").activeBrowserWindow; @@ -31,20 +38,10 @@ exports["test unload tab observer"] = fu // Enqueuing asserts to make sure that assertion is not performed early. setTimeout(function () { assert.equal(1, opened, "observer open was called before unload only"); assert.equal(1, closed, "observer close was called before unload only"); done(); }, 0); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." - ); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-tab-utils.js +++ b/addon-sdk/source/test/test-tab-utils.js @@ -1,57 +1,50 @@ 'use strict'; const { getTabs } = require('sdk/tabs/utils'); const { isGlobalPBSupported, isWindowPBSupported, isTabPBSupported } = require('sdk/private-browsing/utils'); const { browserWindows } = require('sdk/windows'); const tabs = require('sdk/tabs'); const { pb } = require('./private-browsing/helper'); const { isPrivate } = require('sdk/private-browsing'); -const { openTab, closeTab, getTabContentWindow } = require('sdk/tabs/utils'); +const { openTab, closeTab, getTabContentWindow, getOwnerWindow } = require('sdk/tabs/utils'); const { open, close } = require('sdk/window/helpers'); const { windows } = require('sdk/window/utils'); const { getMostRecentBrowserWindow } = require('sdk/window/utils'); const { fromIterator } = require('sdk/util/array'); -if (isGlobalPBSupported) { +if (isWindowPBSupported) { exports.testGetTabs = function(assert, done) { - pb.once('start', function() { - tabs.open({ - url: 'about:blank', - inNewWindow: true, - onOpen: function(tab) { - assert.equal(getTabs().length, 2, 'there are two tabs'); - assert.equal(browserWindows.length, 2, 'there are two windows'); - pb.once('stop', function() { - done(); - }); - pb.deactivate(); - } - }); - }); - pb.activate(); - }; -} -else if (isWindowPBSupported) { - exports.testGetTabs = function(assert, done) { + let tabCount = getTabs().length; + let windowCount = browserWindows.length; + open(null, { features: { private: true, toolbar: true, chrome: true } }).then(function(window) { assert.ok(isPrivate(window), 'new tab is private'); - assert.equal(getTabs().length, 1, 'there is one tab found'); - assert.equal(browserWindows.length, 1, 'there is one window found'); + + assert.equal(getTabs().length, tabCount, 'there are no new tabs found'); + getTabs().forEach(function(tab) { + assert.equal(isPrivate(tab), false, 'all found tabs are not private'); + assert.equal(isPrivate(getOwnerWindow(tab)), false, 'all found tabs are not private'); + assert.equal(isPrivate(getTabContentWindow(tab)), false, 'all found tabs are not private'); + }); + + assert.equal(browserWindows.length, windowCount, 'there are no new windows found'); fromIterator(browserWindows).forEach(function(window) { - assert.ok(!isPrivate(window), 'all found windows are not private'); + assert.equal(isPrivate(window), false, 'all found windows are not private'); }); + assert.equal(windows(null, {includePrivate: true}).length, 2, 'there are really two windows'); + close(window).then(done); }); }; } else if (isTabPBSupported) { exports.testGetTabs = function(assert, done) { let startTabCount = getTabs().length; let tab = openTab(getMostRecentBrowserWindow(), 'about:blank', { @@ -66,12 +59,9 @@ else if (isTabPBSupported) { 'the last tab is the opened tab'); assert.equal(browserWindows.length, 1, 'there is only one window'); closeTab(tab); done(); }; } -// Test disabled because of bug 855771 -module.exports = {}; - require('test').run(exports);
--- a/addon-sdk/source/test/test-tab.js +++ b/addon-sdk/source/test/test-tab.js @@ -1,16 +1,17 @@ /* 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/. */ const tabs = require("sdk/tabs"); // From addon-kit const windowUtils = require("sdk/deprecated/window-utils"); const { getTabForWindow } = require('sdk/tabs/helpers'); +const app = require("sdk/system/xul-app"); // The primary test tab var primaryTab; // We have an auxiliary tab to test background tabs. var auxTab; // The window for the outer iframe in the primary test page @@ -117,29 +118,22 @@ exports["test behavior on close"] = func assert.equal(tab.url, "about:mozilla", "Tab has the expected url"); // if another test ends before closing a tab then index != 1 here assert.ok(tab.index >= 1, "Tab has the expected index, a value greater than 0"); tab.close(function () { assert.equal(tab.url, undefined, "After being closed, tab attributes are undefined (url)"); assert.equal(tab.index, undefined, "After being closed, tab attributes are undefined (index)"); - // Ensure that we can call destroy multiple times without throwing - tab.destroy(); - tab.destroy(); + if (app.is("Firefox")) { + // Ensure that we can call destroy multiple times without throwing; + // Fennec doesn't use this internal utility + tab.destroy(); + tab.destroy(); + } done(); }); } }); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See Bug 809362"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-timer.js +++ b/addon-sdk/source/test/test-timer.js @@ -1,131 +1,177 @@ /* 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/. */ var timer = require("sdk/timers"); const { Loader } = require("sdk/test/loader"); -exports.testSetTimeout = function(test) { +exports.testSetTimeout = function(assert, end) { timer.setTimeout(function() { - test.pass("testSetTimeout passed"); - test.done(); + assert.pass("testSetTimeout passed"); + end(); }, 1); - test.waitUntilDone(); }; -exports.testParamedSetTimeout = function(test) { +exports.testParamedSetTimeout = function(assert, end) { let params = [1, 'foo', { bar: 'test' }, null, undefined]; timer.setTimeout.apply(null, [function() { - test.assertEqual(arguments.length, params.length); + assert.equal(arguments.length, params.length); for (let i = 0, ii = params.length; i < ii; i++) - test.assertEqual(params[i], arguments[i]); - test.done(); + assert.equal(params[i], arguments[i]); + end(); }, 1].concat(params)); - test.waitUntilDone(); }; -exports.testClearTimeout = function(test) { +exports.testClearTimeout = function(assert, end) { var myFunc = function myFunc() { - test.fail("myFunc() should not be called in testClearTimeout"); + assert.fail("myFunc() should not be called in testClearTimeout"); }; var id = timer.setTimeout(myFunc, 1); timer.setTimeout(function() { - test.pass("testClearTimeout passed"); - test.done(); + assert.pass("testClearTimeout passed"); + end(); }, 2); timer.clearTimeout(id); - test.waitUntilDone(); }; -exports.testParamedClearTimeout = function(test) { +exports.testParamedClearTimeout = function(assert, end) { let params = [1, 'foo', { bar: 'test' }, null, undefined]; var myFunc = function myFunc() { - test.fail("myFunc() should not be called in testClearTimeout"); + assert.fail("myFunc() should not be called in testClearTimeout"); }; var id = timer.setTimeout(myFunc, 1); timer.setTimeout.apply(null, [function() { - test.assertEqual(arguments.length, params.length); + assert.equal(arguments.length, params.length); for (let i = 0, ii = params.length; i < ii; i++) - test.assertEqual(params[i], arguments[i]); - test.done(); + assert.equal(params[i], arguments[i]); + end(); }, 1].concat(params)); timer.clearTimeout(id); - test.waitUntilDone(); }; -exports.testSetInterval = function (test) { +exports.testSetInterval = function (assert, end) { var count = 0; var id = timer.setInterval(function () { count++; if (count >= 5) { timer.clearInterval(id); - test.pass("testSetInterval passed"); - test.done(); + assert.pass("testSetInterval passed"); + end(); } }, 1); - test.waitUntilDone(); }; -exports.testParamedSetInerval = function(test) { +exports.testParamedSetInerval = function(assert, end) { let params = [1, 'foo', { bar: 'test' }, null, undefined]; let count = 0; let id = timer.setInterval.apply(null, [function() { count ++; if (count < 5) { - test.assertEqual(arguments.length, params.length); + assert.equal(arguments.length, params.length); for (let i = 0, ii = params.length; i < ii; i++) - test.assertEqual(params[i], arguments[i]); + assert.equal(params[i], arguments[i]); } else { timer.clearInterval(id); - test.done(); + end(); } }, 1].concat(params)); - test.waitUntilDone(); }; -exports.testClearInterval = function (test) { +exports.testClearInterval = function (assert, end) { timer.clearInterval(timer.setInterval(function () { - test.fail("setInterval callback should not be called"); + assert.fail("setInterval callback should not be called"); }, 1)); var id = timer.setInterval(function () { timer.clearInterval(id); - test.pass("testClearInterval passed"); - test.done(); + assert.pass("testClearInterval passed"); + end(); }, 2); - test.waitUntilDone(); }; -exports.testParamedClearInterval = function(test) { +exports.testParamedClearInterval = function(assert, end) { timer.clearInterval(timer.setInterval(function () { - test.fail("setInterval callback should not be called"); + assert.fail("setInterval callback should not be called"); }, 1, timer, {}, null)); let id = timer.setInterval(function() { timer.clearInterval(id); - test.assertEqual(3, arguments.length); - test.done(); + assert.equal(3, arguments.length); + end(); }, 2, undefined, 'test', {}); - test.waitUntilDone(); }; -exports.testUnload = function(test) { +exports.testImmediate = function(assert, end) { + let actual = []; + let ticks = 0; + timer.setImmediate(function(...params) { + actual.push(params); + assert.equal(ticks, 1, "is a next tick"); + assert.deepEqual(actual, [["start", "immediates"]]); + }, "start", "immediates"); + + timer.setImmediate(function(...params) { + actual.push(params); + assert.deepEqual(actual, [["start", "immediates"], + ["added"]]); + assert.equal(ticks, 1, "is a next tick"); + timer.setImmediate(function(...params) { + actual.push(params); + assert.equal(ticks, 2, "is second tick"); + assert.deepEqual(actual, [["start", "immediates"], + ["added"], + [], + ["last", "immediate", "handler"], + ["side-effect"]]); + end(); + }, "side-effect"); + }, "added"); + + timer.setImmediate(function(...params) { + actual.push(params); + assert.equal(ticks, 1, "is a next tick"); + assert.deepEqual(actual, [["start", "immediates"], + ["added"], + []]); + timer.clearImmediate(removeID); + }); + + function removed() { + assert.fail("should be removed"); + } + let removeID = timer.setImmediate(removed); + + timer.setImmediate(function(...params) { + actual.push(params); + assert.equal(ticks, 1, "is a next tick"); + assert.deepEqual(actual, [["start", "immediates"], + ["added"], + [], + ["last", "immediate", "handler"]]); + ticks = ticks + 1; + }, "last", "immediate", "handler"); + + + ticks = ticks + 1; +}; + +exports.testUnload = function(assert, end) { var loader = Loader(module); var sbtimer = loader.require("sdk/timers"); var myFunc = function myFunc() { - test.fail("myFunc() should not be called in testUnload"); + assert.fail("myFunc() should not be called in testUnload"); }; sbtimer.setTimeout(myFunc, 1); sbtimer.setTimeout(myFunc, 1, 'foo', 4, {}, undefined); sbtimer.setInterval(myFunc, 1); sbtimer.setInterval(myFunc, 1, {}, null, 'bar', undefined, 87); loader.unload(); timer.setTimeout(function() { - test.pass("timer testUnload passed"); - test.done(); + assert.pass("timer testUnload passed"); + end(); }, 2); - test.waitUntilDone(); }; +require("test").run(exports); \ No newline at end of file
--- a/addon-sdk/source/test/test-window-events.js +++ b/addon-sdk/source/test/test-window-events.js @@ -1,13 +1,19 @@ /* 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/. */ +"use strict"; -"use strict"; +// Opening new windows in Fennec causes issues +module.metadata = { + engines: { + 'Firefox': '*' + } +}; const { Loader } = require("sdk/test/loader"); const { open, getMostRecentBrowserWindow, getOuterId } = require("sdk/window/utils"); exports["test browser events"] = function(assert, done) { let loader = Loader(module); let { events } = loader.require("sdk/window/events"); let { on, off } = loader.require("sdk/event/core"); @@ -38,19 +44,9 @@ exports["test browser events"] = functio done(); } }); // Open window and close it to trigger observers. let window = open(); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 793071"); - } - } -} - -require("test").run(exports); +require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-window-loader.js +++ b/addon-sdk/source/test/test-window-loader.js @@ -1,13 +1,20 @@ /* 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/. */ "use strict"; +// Opening new windows in Fennec causes issues +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + const { WindowLoader } = require('sdk/windows/loader'), { Trait } = require('sdk/deprecated/traits'); const Loader = Trait.compose( WindowLoader, { constructor: function Loader(options) { this._onLoad = options.onLoad; @@ -112,19 +119,8 @@ exports['test create loader from opened } }); }, onUnload: function(window) { onUnloadCalled = true; } }); }; - -if (require("sdk/system/xul-app").is("Fennec")) { - - module.exports = { - "test Unsupported Test": function UnsupportedTest (test) { - test.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 809409"); - } - } -}
--- a/addon-sdk/source/test/test-window-observer.js +++ b/addon-sdk/source/test/test-window-observer.js @@ -1,13 +1,20 @@ /* 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/. */ "use strict"; +// Opening new windows in Fennec causes issues +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + const { Loader } = require("sdk/test/loader"); const { open, close } = require("sdk/window/helpers"); const { browserWindows: windows } = require("sdk/windows"); const { isBrowser } = require('sdk/window/utils'); exports["test unload window observer"] = function(assert, done) { // Hacky way to be able to create unloadable modules via makeSandboxedLoader. let loader = Loader(module); @@ -37,19 +44,9 @@ exports["test unload window observer"] = then(function() { // Enqueuing asserts to make sure that assertion is not performed early. assert.equal(1, opened, "observer open was called before unload only"); assert.equal(windowsOpen + 1, closed, "observer close was called before unload only"); }). then(done, assert.fail); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 793071"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-window-utils-private-browsing.js +++ b/addon-sdk/source/test/test-window-utils-private-browsing.js @@ -1,13 +1,20 @@ /* 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/. */ 'use strict'; +// Fennec support tracked in bug #809412 +module.metadata = { + 'engines': { + 'Firefox': '*' + } +}; + const windowUtils = require('sdk/deprecated/window-utils'); const { Cc, Ci } = require('chrome'); const { isWindowPBSupported } = require('sdk/private-browsing/utils'); const { getFrames, getWindowTitle, onFocus, isWindowPrivate } = require('sdk/window/utils'); const { open, close, focus } = require('sdk/window/helpers'); const WM = Cc['@mozilla.org/appshell/window-mediator;1'].getService(Ci.nsIWindowMediator); const { isPrivate } = require('sdk/private-browsing'); const { fromIterator: toArray } = require('sdk/util/array'); @@ -209,19 +216,9 @@ exports.testWindowIteratorIgnoresPrivate assert.ok(toArray(windowUtils.windowIterator()).indexOf(window) > -1, "window is in windowIterator()"); } close(window).then(done); }); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 809412"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-window-utils.js +++ b/addon-sdk/source/test/test-window-utils.js @@ -1,13 +1,19 @@ /* 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/. */ "use strict"; +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + const windowUtils = require("sdk/deprecated/window-utils"); const timer = require("sdk/timers"); const { Cc, Ci } = require("chrome"); const { Loader } = require("sdk/test/loader"); const { open, getFrames, getWindowTitle, onFocus } = require('sdk/window/utils'); const { close } = require('sdk/window/helpers'); const { fromIterator: toArray } = require('sdk/util/array'); @@ -286,19 +292,9 @@ exports.testWindowIterator = function(as assert.ok(toArray(windowUtils.windowIterator()).indexOf(window) !== -1, "window is now in windowIterator()"); // Wait for the window unload before ending test close(window).then(done); }, false); }; -if (require("sdk/system/xul-app").is("Fennec")) { - module.exports = { - "test Unsupported Test": function UnsupportedTest (assert) { - assert.pass( - "Skipping this test until Fennec support is implemented." + - "See bug 809412"); - } - } -} - require("test").run(exports);
--- a/addon-sdk/source/test/test-window-utils2.js +++ b/addon-sdk/source/test/test-window-utils2.js @@ -1,13 +1,20 @@ /* 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/. */ 'use strict'; +// Opening new windows in Fennec causes issues +module.metadata = { + engines: { + 'Firefox': '*' + } +}; + const { Ci } = require('chrome'); const { open, backgroundify, windows, isBrowser, getXULWindow, getBaseWindow, getToplevelWindow, getMostRecentWindow, getMostRecentBrowserWindow } = require('sdk/window/utils'); const { close } = require('sdk/window/helpers'); const windowUtils = require('sdk/deprecated/window-utils'); exports['test get nsIBaseWindow from nsIDomWindow'] = function(assert) { @@ -56,16 +63,44 @@ exports['test new top window with option assert.equal(window.innerHeight, 100, 'height is set'); assert.equal(window.innerWidth, 200, 'height is set'); assert.equal(window.toolbar.visible, true, 'toolbar was set'); // Wait for the window unload before ending test close(window).then(done); }; +exports['test new top window with various URIs'] = function(assert, done) { + let msg = 'only chrome, resource and data uris are allowed'; + assert.throws(function () { + open('foo'); + }, msg); + assert.throws(function () { + open('http://foo'); + }, msg); + assert.throws(function () { + open('https://foo'); + }, msg); + assert.throws(function () { + open('ftp://foo'); + }, msg); + assert.throws(function () { + open('//foo'); + }, msg); + + let chromeWindow = open('chrome://foo/content/'); + assert.ok(~windows().indexOf(chromeWindow), 'chrome URI works'); + + let resourceWindow = open('resource://foo'); + assert.ok(~windows().indexOf(resourceWindow), 'resource URI works'); + + // Wait for the window unload before ending test + close(chromeWindow).then(close.bind(null, resourceWindow)).then(done); +}; + exports.testBackgroundify = function(assert, done) { let window = open('data:text/html;charset=utf-8,backgroundy'); assert.ok(~windows().indexOf(window), 'window is in the list of windows'); let backgroundy = backgroundify(window); assert.equal(backgroundy, window, 'backgroundify returs give window back'); assert.ok(!~windows().indexOf(window), 'backgroundifyied window is in the list of windows');
--- a/b2g/app/Makefile.in +++ b/b2g/app/Makefile.in @@ -25,17 +25,17 @@ LIBS += \ -lEGL \ -lhardware_legacy \ -lhardware \ -lcutils \ $(DEPTH)/media/libpng/$(LIB_PREFIX)mozpng.$(LIB_SUFFIX) \ $(DEPTH)/widget/gonk/libdisplay/$(LIB_PREFIX)display.$(LIB_SUFFIX) \ $(MOZ_ZLIB_LIBS) \ $(NULL) -ifeq (17,$(ANDROID_VERSION)) +ifeq (18,$(ANDROID_VERSION)) LIBS += \ -lgui \ -lsuspend \ $(NULL) endif OS_LDFLAGS += -Wl,--export-dynamic LOCAL_INCLUDES += -I$(topsrcdir)/widget/gonk/libdisplay endif
--- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -374,20 +374,21 @@ pref("browser.link.open_newwindow.restri pref("dom.mozBrowserFramesEnabled", true); // Enable a (virtually) unlimited number of mozbrowser processes. // We'll run out of PIDs on UNIX-y systems before we hit this limit. pref("dom.ipc.processCount", 100000); pref("dom.ipc.browser_frames.oop_by_default", false); -// WebSMS +// SMS/MMS pref("dom.sms.enabled", true); pref("dom.sms.strict7BitEncoding", false); // Disabled by default. pref("dom.sms.requestStatusReport", true); // Enabled by default. +pref("dom.mms.requestStatusReport", true); // Enabled by default. // WebContacts pref("dom.mozContacts.enabled", true); pref("dom.navigator-property.disable.mozContacts", false); pref("dom.global-constructor.disable.mozContact", false); // Shortnumber matching needed for e.g. Brazil: // 01187654321 can be found with 87654321
--- a/b2g/chrome/content/dbg-browser-actors.js +++ b/b2g/chrome/content/dbg-browser-actors.js @@ -116,15 +116,15 @@ Object.defineProperty(ContentTabActor.pr Object.defineProperty(ContentTabActor.prototype, "url", { get: function() { return this.browser.document.documentURI; }, enumerable: true, configurable: false }); -Object.defineProperty(ContentTabActor.prototype, "contentWindow", { +Object.defineProperty(ContentTabActor.prototype, "window", { get: function() { return this.browser; }, enumerable: true, configurable: false });
--- a/b2g/chrome/content/payment.js +++ b/b2g/chrome/content/payment.js @@ -4,64 +4,125 @@ * 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/. */ // This JS shim contains the callbacks to fire DOMRequest events for // navigator.pay API within the payment processor's scope. "use strict"; -dump("======================= payment.js ======================= \n"); +let _DEBUG = false; +function _debug(s) { dump("== Payment flow == " + s + "\n"); } +_debug("Frame script injected"); let { classes: Cc, interfaces: Ci, utils: Cu } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"); XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); #ifdef MOZ_B2G_RIL -XPCOMUtils.defineLazyServiceGetter(this, "mobileConnection", +XPCOMUtils.defineLazyServiceGetter(this, "iccProvider", "@mozilla.org/ril/content-helper;1", - "nsIMobileConnectionProvider"); + "nsIIccProvider"); + +XPCOMUtils.defineLazyServiceGetter(this, "smsService", + "@mozilla.org/sms/smsservice;1", + "nsISmsService"); + +const kSilentSmsReceivedTopic = "silent-sms-received"; + +const MOBILEMESSAGECALLBACK_CID = + Components.ID("{b484d8c9-6be4-4f94-ab60-c9c7ebcc853d}"); + +// In order to send messages through nsISmsService, we need to implement +// nsIMobileMessageCallback, as the WebSMS API implementation is not usable +// from JS. +function SilentSmsRequest() { +} +SilentSmsRequest.prototype = { + __exposedProps__: { + onsuccess: 'rw', + onerror: 'rw' + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileMessageCallback]), + + classID: MOBILEMESSAGECALLBACK_CID, + + set onsuccess(aSuccessCallback) { + this._onsuccess = aSuccessCallback; + }, + + set onerror(aErrorCallback) { + this._onerror = aErrorCallback; + }, + + notifyMessageSent: function notifyMessageSent(aMessage) { + if (_DEBUG) { + _debug("Silent message successfully sent"); + } + this._onsuccess(aMessage); + }, + + notifySendMessageFailed: function notifySendMessageFailed(aError) { + if (_DEBUG) { + _debug("Error sending silent message " + aError); + } + this._onerror(aError); + } +}; #endif - const kClosePaymentFlowEvent = "close-payment-flow-dialog"; -let _requestId; +let gRequestId; + +let gBrowser = Services.wm.getMostRecentWindow("navigator:browser"); let PaymentProvider = { - +#ifdef MOZ_B2G_RIL __exposedProps__: { paymentSuccess: 'r', paymentFailed: 'r', - iccIds: 'r' + iccIds: 'r', + mcc: 'r', + mnc: 'r', + sendSilentSms: 'r', + observeSilentSms: 'r', + removeSilentSmsObserver: 'r' }, +#else + __exposedProps__: { + paymentSuccess: 'r', + paymentFailed: 'r' + }, +#endif _closePaymentFlowDialog: function _closePaymentFlowDialog(aCallback) { // After receiving the payment provider confirmation about the // successful or failed payment flow, we notify the UI to close the // payment flow dialog and return to the caller application. let id = kClosePaymentFlowEvent + "-" + uuidgen.generateUUID().toString(); - let browser = Services.wm.getMostRecentWindow("navigator:browser"); - let content = browser.getContentWindow(); + let content = gBrowser.getContentWindow(); if (!content) { return; } let detail = { type: kClosePaymentFlowEvent, id: id, - requestId: _requestId + requestId: gRequestId }; // In order to avoid race conditions, we wait for the UI to notify that // it has successfully closed the payment flow and has recovered the // caller app, before notifying the parent process to fire the success // or error event over the DOMRequest. content.addEventListener("mozContentEvent", function closePaymentFlowReturn(evt) { @@ -72,54 +133,181 @@ let PaymentProvider = { content.removeEventListener("mozContentEvent", closePaymentFlowReturn); let glue = Cc["@mozilla.org/payment/ui-glue;1"] .createInstance(Ci.nsIPaymentUIGlue); glue.cleanup(); }); - browser.shell.sendChromeEvent(detail); + gBrowser.shell.sendChromeEvent(detail); + +#ifdef MOZ_B2G_RIL + this._cleanUp(); +#endif }, paymentSuccess: function paymentSuccess(aResult) { + if (_DEBUG) { + _debug("paymentSuccess " + aResult); + } + PaymentProvider._closePaymentFlowDialog(function notifySuccess() { - if (!_requestId) { + if (!gRequestId) { return; } cpmm.sendAsyncMessage("Payment:Success", { result: aResult, - requestId: _requestId }); + requestId: gRequestId }); }); }, paymentFailed: function paymentFailed(aErrorMsg) { + if (_DEBUG) { + _debug("paymentFailed " + aErrorMsg); + } + PaymentProvider._closePaymentFlowDialog(function notifyError() { - if (!_requestId) { + if (!gRequestId) { return; } cpmm.sendAsyncMessage("Payment:Failed", { errorMsg: aErrorMsg, - requestId: _requestId }); + requestId: gRequestId }); }); }, +#ifdef MOZ_B2G_RIL + // Until bug 814629 is done, we only have support for a single SIM, so we + // can only provide information for a single ICC. However, we return an array + // so the payment provider facing API won't need to change once we support + // multiple SIMs. + get iccIds() { -#ifdef MOZ_B2G_RIL - // Until bug 814629 is done, we only have support for a single SIM, so we - // can only provide a single ICC ID. However, we return an array so the - // payment provider facing API won't need to change once we support - // multiple SIMs. - return [mobileConnection.iccInfo.iccid]; -#else - return null; -#endif + return [iccProvider.iccInfo.iccid]; + }, + + get mcc() { + return [iccProvider.iccInfo.mcc]; + }, + + get mnc() { + return [iccProvider.iccInfo.mnc]; + }, + + _silentNumbers: null, + _silentSmsObservers: null, + + sendSilentSms: function sendSilentSms(aNumber, aMessage) { + if (_DEBUG) { + _debug("Sending silent message " + aNumber + " - " + aMessage); + } + + let request = new SilentSmsRequest(); + smsService.send(aNumber, aMessage, true, request); + return request; + }, + + observeSilentSms: function observeSilentSms(aNumber, aCallback) { + if (_DEBUG) { + _debug("observeSilentSms " + aNumber); + } + + if (!this._silentSmsObservers) { + this._silentSmsObservers = {}; + this._silentNumbers = []; + Services.obs.addObserver(this._onSilentSms.bind(this), + kSilentSmsReceivedTopic, + false); + } + + if (!this._silentSmsObservers[aNumber]) { + this._silentSmsObservers[aNumber] = []; + this._silentNumbers.push(aNumber); + smsService.addSilentNumber(aNumber); + } + + if (this._silentSmsObservers[aNumber].indexOf(aCallback) == -1) { + this._silentSmsObservers[aNumber].push(aCallback); + } }, + removeSilentSmsObserver: function removeSilentSmsObserver(aNumber, aCallback) { + if (_DEBUG) { + _debug("removeSilentSmsObserver " + aNumber); + } + + if (!this._silentSmsObservers || !this._silentSmsObservers[aNumber]) { + if (_DEBUG) { + _debug("No observers for " + aNumber); + } + return; + } + + let index = this._silentSmsObservers[aNumber].indexOf(aCallback); + if (index != -1) { + this._silentSmsObservers[aNumber].splice(index, 1); + if (this._silentSmsObservers[aNumber].length == 0) { + this._silentSmsObservers[aNumber] = null; + this._silentNumbers.splice(this._silentNumbers.indexOf(aNumber), 1); + smsService.removeSilentNumber(aNumber); + } + } else if (_DEBUG) { + _debug("No callback found for " + aNumber); + } + }, + + _onSilentSms: function _onSilentSms(aSubject, aTopic, aData) { + if (_DEBUG) { + _debug("Got silent message! " + aSubject.sender + " - " + aSubject.body); + } + + let number = aSubject.sender; + if (!number || this._silentNumbers.indexOf(number) == -1) { + if (_DEBUG) { + _debug("No observers for " + number); + } + return; + } + + this._silentSmsObservers[number].forEach(function(callback) { + callback(aSubject); + }); + }, + + _cleanUp: function _cleanUp() { + if (_DEBUG) { + _debug("Cleaning up!"); + } + + if (!this._silentNumbers) { + return; + } + + while (this._silentNumbers.length) { + let number = this._silentNumbers.pop(); + smsService.removeSilentNumber(number); + } + this._silentNumbers = null; + this._silentSmsObservers = null; + Services.obs.removeObserver(this._onSilentSms, kSilentSmsReceivedTopic); + } +#endif }; // We save the identifier of the DOM request, so we can dispatch the results // of the payment flow to the appropriate content process. addMessageListener("Payment:LoadShim", function receiveMessage(aMessage) { - _requestId = aMessage.json.requestId; + gRequestId = aMessage.json.requestId; }); addEventListener("DOMWindowCreated", function(e) { content.wrappedJSObject.mozPaymentProvider = PaymentProvider; }); + +#ifdef MOZ_B2G_RIL +// If the trusted dialog is not closed via paymentSuccess or paymentFailed +// a mozContentEvent with type 'cancel' is sent from the UI. We need to listen +// for this event to clean up the silent sms observers if any exists. +gBrowser.getContentWindow().addEventListener("mozContentEvent", function(e) { + if (e.detail.type === "cancel") { + PaymentProvider._cleanUp(); + } +}); +#endif
--- a/b2g/chrome/content/settings.js +++ b/b2g/chrome/content/settings.js @@ -128,16 +128,21 @@ SettingsListener.observe('language.curre Services.prefs.setBoolPref('dom.sms.strict7BitEncoding', value); }); SettingsListener.observe('ril.sms.requestStatusReport.enabled', false, function(value) { Services.prefs.setBoolPref('dom.sms.requestStatusReport', value); }); + SettingsListener.observe('ril.mms.requestStatusReport.enabled', false, + function(value) { + Services.prefs.setBoolPref('dom.mms.requestStatusReport', value); + }); + SettingsListener.observe('ril.cellbroadcast.disabled', false, function(value) { Services.prefs.setBoolPref('ril.cellbroadcast.disabled', value); }); SettingsListener.observe('ril.radio.disabled', false, function(value) { Services.prefs.setBoolPref('ril.radio.disabled', value);
--- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -550,16 +550,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'), + onlyShowApp: msg.onlyShowApp, + showApp: msg.showApp, target: msg.target, expectingSystemMessage: true, extra: msg.extra }); }, receiveMessage: function shell_receiveMessage(message) { var activities = { 'content-handler': { name: 'view', response: null }, @@ -972,16 +974,17 @@ let RemoteDebugger = { this._promptDone = true; }, // Start the debugger server. start: function debugger_start() { if (!DebuggerServer.initialized) { // Ask for remote connections. DebuggerServer.init(this.prompt.bind(this)); + DebuggerServer.chromeWindowType = "navigator:browser"; DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.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");
--- a/b2g/components/B2GComponents.manifest +++ b/b2g/components/B2GComponents.manifest @@ -15,16 +15,20 @@ component {88b3eb21-d072-4e3b-886d-f89d8 contract @mozilla.org/updates/update-prompt;1 {88b3eb21-d072-4e3b-886d-f89d8c49fe59} #endif # MozKeyboard.js component {397a7fdf-2254-47be-b74e-76625a1a66d5} MozKeyboard.js contract @mozilla.org/b2g-keyboard;1 {397a7fdf-2254-47be-b74e-76625a1a66d5} category JavaScript-navigator-property mozKeyboard @mozilla.org/b2g-keyboard;1 +component {5c7f4ce1-a946-4adc-89e6-c908226341a0} MozKeyboard.js +contract @mozilla.org/b2g-inputmethod;1 {5c7f4ce1-a946-4adc-89e6-c908226341a0} +category JavaScript-navigator-property mozInputMethod @mozilla.org/b2g-inputmethod;1 + # DirectoryProvider.js component {9181eb7c-6f87-11e1-90b1-4f59d80dd2e5} DirectoryProvider.js contract @mozilla.org/browser/directory-provider;1 {9181eb7c-6f87-11e1-90b1-4f59d80dd2e5} category xpcom-directory-providers browser-directory-provider @mozilla.org/browser/directory-provider;1 # ActivitiesGlue.js component {70a83123-7467-4389-a309-3e81c74ad002} ActivitiesGlue.js contract @mozilla.org/dom/activities/ui-glue;1 {70a83123-7467-4389-a309-3e81c74ad002}
--- a/b2g/components/Keyboard.jsm +++ b/b2g/components/Keyboard.jsm @@ -16,17 +16,18 @@ Cu.import("resource://gre/modules/XPCOMU XPCOMUtils.defineLazyServiceGetter(this, "ppmm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); let Keyboard = { _messageManager: null, _messageNames: [ 'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions', - 'SetSelectionRange', 'ReplaceSurroundingText' + 'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker', + 'SwitchToNextInputMethod', 'HideInputMethod' ], get messageManager() { if (this._messageManager && !Cu.isDeadWrapper(this._messageManager)) return this._messageManager; throw Error('no message manager set'); }, @@ -115,16 +116,22 @@ let Keyboard = { this.setSelectedOption(msg); break; case 'Keyboard:SetSelectionRange': this.setSelectionRange(msg); break; case 'Keyboard:ReplaceSurroundingText': this.replaceSurroundingText(msg); break; + case 'Keyboard:SwitchToNextInputMethod': + this.switchToNextInputMethod(); + break; + case 'Keyboard:ShowInputMethodPicker': + this.showInputMethodPicker(); + break; } }, handleFormsInput: function keyboardHandleFormsInput(msg) { this.messageManager = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner) .frameLoader.messageManager; ppmm.broadcastAsyncMessage('Keyboard:FocusChange', msg.data); @@ -155,12 +162,26 @@ let Keyboard = { removeFocus: function keyboardRemoveFocus() { this.messageManager.sendAsyncMessage('Forms:Select:Blur', {}); }, replaceSurroundingText: function keyboardReplaceSurroundingText(msg) { this.messageManager.sendAsyncMessage('Forms:ReplaceSurroundingText', msg.data); + }, + + showInputMethodPicker: function keyboardShowInputMethodPicker() { + let browser = Services.wm.getMostRecentWindow("navigator:browser"); + browser.shell.sendChromeEvent({ + type: "input-method-show-picker" + }); + }, + + switchToNextInputMethod: function keyboardSwitchToNextInputMethod() { + let browser = Services.wm.getMostRecentWindow("navigator:browser"); + browser.shell.sendChromeEvent({ + type: "input-method-switch-to-next" + }); } }; Keyboard.init();
--- a/b2g/components/MozKeyboard.js +++ b/b2g/components/MozKeyboard.js @@ -76,17 +76,17 @@ MozKeyboard.prototype = { sendKey: function mozKeyboardSendKey(keyCode, charCode) { charCode = (charCode == undefined) ? keyCode : charCode; let mainThread = tm.mainThread; let utils = this._utils; function send(type) { mainThread.dispatch(function() { - utils.sendKeyEvent(type, keyCode, charCode, null); + utils.sendKeyEvent(type, keyCode, charCode, null); }, mainThread.DISPATCH_NORMAL); } send("keydown"); send("keypress"); send("keyup"); }, @@ -192,9 +192,90 @@ MozKeyboard.prototype = { observe: function mozKeyboardObserve(subject, topic, data) { let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; if (wId == this.innerWindowID) this.uninit(); } }; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozKeyboard]); +/** + * ============================================== + * InputMethodManager + * ============================================== + */ +function MozInputMethodManager() { } + +MozInputMethodManager.prototype = { + classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIInputMethodManager + ]), + + classInfo: XPCOMUtils.generateCI({ + "classID": Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"), + "contractID": "@mozilla.org/b2g-imm;1", + "interfaces": [Ci.nsIInputMethodManager], + "flags": Ci.nsIClassInfo.DOM_OBJECT, + "classDescription": "B2G Input Method Manager" + }), + + showAll: function() { + cpmm.sendAsyncMessage('Keyboard:ShowInputMethodPicker', {}); + }, + + next: function() { + cpmm.sendAsyncMessage('Keyboard:SwitchToNextInputMethod', {}); + }, + + supportsSwitching: function() { + return true; + }, + + hide: function() { + cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {}); + } +}; + +/** + * ============================================== + * InputMethod + * ============================================== + */ +function MozInputMethod() { } + +MozInputMethod.prototype = { + classID: Components.ID("{5c7f4ce1-a946-4adc-89e6-c908226341a0}"), + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIInputMethod, + Ci.nsIDOMGlobalPropertyInitializer + ]), + + classInfo: XPCOMUtils.generateCI({ + "classID": Components.ID("{5c7f4ce1-a946-4adc-89e6-c908226341a0}"), + "contractID": "@mozilla.org/b2g-inputmethod;1", + "interfaces": [Ci.nsIInputMethod], + "flags": Ci.nsIClassInfo.DOM_OBJECT, + "classDescription": "B2G Input Method" + }), + + init: function mozInputMethodInit(win) { + let principal = win.document.nodePrincipal; + let perm = Services.perms + .testExactPermissionFromPrincipal(principal, "keyboard"); + if (perm != Ci.nsIPermissionManager.ALLOW_ACTION) { + dump("No permission to use the keyboard API for " + + principal.origin + "\n"); + return null; + } + + this._mgmt = new MozInputMethodManager(); + }, + + get mgmt() { + return this._mgmt; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory( + [MozKeyboard, MozInputMethodManager, MozInputMethod]);
--- a/b2g/components/b2g.idl +++ b/b2g/components/b2g.idl @@ -71,8 +71,41 @@ interface nsIB2GKeyboard : nsISupports * beginning of the current selection range. Defaults to 0. * @param afterLength The number of characters to be deleted after the * beginning of the current selection range. Defaults to 0. */ void replaceSurroundingText(in DOMString text, [optional] in long beforeLength, [optional] in long afterLength); }; + +// Manages the list of IMEs, enables/disables IME and switches to an IME. +[scriptable, uuid(e51a6fa0-ef85-11e2-b778-0800200c9a66)] +interface nsIInputMethodManager : nsISupports +{ + // Ask the OS to show a list of available IMEs for users to switch from. + // OS should ignore this request if the app is currently not the active one. + void showAll(); + + // Ask the OS to switch away from the current active Keyboard app. + // OS should ignore this request if the app is currently not the active one. + void next(); + + // To know if the OS supports IME switching or not. + // Use case: let the keyboard app knows if it is necessary to show the "IME switching" + // (globe) button. We have a use case that when there is only one IME enabled, we + // should not show the globe icon. + boolean supportsSwitching(); + + // Ask the OS to hide the current active Keyboard app. (was: |removeFocus()|) + // OS should ignore this request if the app is currently not the active one. + // The OS will void the current input context (if it exists). + // This method belong to |mgmt| because we would like to allow Keyboard to access to + // this method w/o a input context. + void hide(); +}; + +[scriptable, uuid(5c7f4ce1-a946-4adc-89e6-c908226341a0)] +interface nsIInputMethod : nsISupports +{ + // Input Method Manager contain a few global methods expose to apps + readonly attribute nsIInputMethodManager mgmt; +};
--- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "878cc221e0fdadb4d42dc110945533104f6dd572", + "revision": "7669b3265def0eed0473acd938897704007afaf3", "repo_path": "/integration/gaia-central" }
--- a/b2g/config/leo/releng-leo.tt +++ b/b2g/config/leo/releng-leo.tt @@ -1,12 +1,12 @@ [ { -"size": 117247732, -"digest": "16e74278e4e9b0d710df77d68af1677c91823dccfc611ab00ee617298a63787f9f9892bd1a41eccb8d45fb18d61bfda0dbd1de88f1861c14b4b44da3b94a4eca", +"size": 114839868, +"digest": "4754612c52330f4d25a250eb52adbf245950e3411a46f45836950b96fdff99e37e3563667d9a0069a7097f3900af3710d9e94010a26bf2e7e1a27c97f37ef447", "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/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -522,16 +522,17 @@ @BINPATH@/components/Activities.manifest @BINPATH@/components/ActivityOptions.js @BINPATH@/components/ActivityProxy.js @BINPATH@/components/ActivityRequestHandler.js @BINPATH@/components/ActivityWrapper.js @BINPATH@/components/ActivityMessageConfigurator.js @BINPATH@/components/TCPSocket.js +@BINPATH@/components/TCPServerSocket.js @BINPATH@/components/TCPSocketParentIntermediary.js @BINPATH@/components/TCPSocket.manifest @BINPATH@/components/AppProtocolHandler.js @BINPATH@/components/AppProtocolHandler.manifest @BINPATH@/components/Payment.js @BINPATH@/components/PaymentFlowInfo.js
--- a/browser/app/blocklist.xml +++ b/browser/app/blocklist.xml @@ -1,10 +1,10 @@ <?xml version="1.0"?> -<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1374187152000"> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1375219877000"> <emItems> <emItem blockID="i350" id="sqlmoz@facebook.com"> <versionRange minVersion="0" maxVersion="*" severity="3"> </versionRange> </emItem> <emItem blockID="i58" id="webmaster@buzzzzvideos.info"> <versionRange minVersion="0" maxVersion="*"> </versionRange> @@ -130,16 +130,20 @@ <emItem blockID="i97" id="support3_en@adobe122.com"> <versionRange minVersion="0" maxVersion="*"> </versionRange> </emItem> <emItem blockID="i382" id="{6926c7f7-6006-42d1-b046-eba1b3010315}"> <versionRange minVersion="0" maxVersion="*" severity="1"> </versionRange> </emItem> + <emItem blockID="i429" id="{B40794A0-7477-4335-95C5-8CB9BBC5C4A5}"> + <versionRange minVersion="0" maxVersion="*" severity="3"> + </versionRange> + </emItem> <emItem blockID="i11" id="yslow@yahoo-inc.com"> <versionRange minVersion="2.0.5" maxVersion="2.0.5"> <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"> <versionRange minVersion="3.5.7" maxVersion="*" /> </targetApplication> </versionRange> </emItem> <emItem blockID="i62" id="jid0-EcdqvFOgWLKHNJPuqAnawlykCGZ@jetpack">
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -62,16 +62,23 @@ pref("extensions.hotfix.certs.1.sha1Fing // Disable add-ons that are not installed by the user in all scopes by default. // See the SCOPE constants in AddonManager.jsm for values to use here. pref("extensions.autoDisableScopes", 15); // Dictionary download preference pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/"); +// At startup, should we check to see if the installation +// date is older than some threshold +pref("app.update.checkInstallTime", true); + +// The number of days a binary is permitted to be old without checking is defined in +// firefox-branding.js (app.update.checkInstallTime.days) + // The minimum delay in seconds for the timer to fire. // default=2 minutes pref("app.update.timerMinimumDelay", 120); // App-specific update preferences // The interval to check for updates (app.update.interval) is defined in // firefox-branding.js @@ -218,18 +225,16 @@ pref("general.skins.selectedSkin", "clas pref("general.smoothScroll", true); #ifdef UNIX_BUT_NOT_MAC pref("general.autoScroll", false); #else pref("general.autoScroll", true); #endif -pref("general.useragent.complexOverride.moodle", false); // bug 797703 - // At startup, check if we're the default browser and prompt user if not. pref("browser.shell.checkDefaultBrowser", true); pref("browser.shell.shortcutFavicons",true); // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore pref("browser.startup.page", 1); pref("browser.startup.homepage", "chrome://branding/locale/browserconfig.properties"); @@ -328,16 +333,20 @@ pref("browser.download.manager.flashCoun pref("browser.download.manager.addToRecentDocs", true); pref("browser.download.manager.quitBehavior", 0); pref("browser.download.manager.scanWhenDone", true); pref("browser.download.manager.resumeOnWakeDelay", 10000); // This allows disabling the Downloads Panel in favor of the old interface. pref("browser.download.useToolkitUI", false); +// This allows disabling the animated notifications shown by +// the Downloads Indicator when a download starts or completes. +pref("browser.download.animateNotifications", true); + // This records whether or not the panel has been shown at least once. pref("browser.download.panel.shown", false); // This records whether or not at least one session with the Downloads Panel // enabled has been completed already. pref("browser.download.panel.firstSessionCompleted", false); // search engines URL
--- a/browser/base/content/browser-social.js +++ b/browser/base/content/browser-social.js @@ -605,21 +605,23 @@ SocialShare = { let panel = this.panel; if (!SocialUI.enabled || this.iframe) return; this.panel.hidden = false; // create and initialize the panel for this window let iframe = document.createElement("iframe"); iframe.setAttribute("type", "content"); iframe.setAttribute("class", "social-share-frame"); + iframe.setAttribute("context", "contentAreaContextMenu"); + iframe.setAttribute("tooltip", "aHTMLTooltip"); iframe.setAttribute("flex", "1"); panel.appendChild(iframe); this.populateProviderMenu(); }, - + getSelectedProvider: function() { let provider; let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin"); if (lastProviderOrigin) { provider = Social._getProviderFromOrigin(lastProviderOrigin); } if (!provider) provider = Social.provider || Social.defaultProvider; @@ -1361,16 +1363,21 @@ SocialSidebar = { if (Social.provider.errorState == "frameworker-error") { SocialSidebar.setSidebarErrorMessage(); return; } // Make sure the right sidebar URL is loaded if (sbrowser.getAttribute("src") != Social.provider.sidebarURL) { sbrowser.setAttribute("src", Social.provider.sidebarURL); + PopupNotifications.locationChange(sbrowser); + } + + // if the document has not loaded, delay until it is + if (sbrowser.contentDocument.readyState != "complete") { sbrowser.addEventListener("load", SocialSidebar._loadListener, true); } else { this.setSidebarVisibilityState(true); } } }, _loadListener: function SocialSidebar_loadListener() {
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -3006,18 +3006,17 @@ const DOMLinkHandler = { } break; case "search": if (!searchAdded) { var type = link.type && link.type.toLowerCase(); type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); if (type == "application/opensearchdescription+xml" && link.title && - /^(?:https?|ftp):/i.test(link.href) && - !PrivateBrowsingUtils.isWindowPrivate(window)) { + /^(?:https?|ftp):/i.test(link.href)) { var engine = { title: link.title, href: link.href }; BrowserSearch.addEngine(engine, link.ownerDocument); searchAdded = true; } } break; } } @@ -3936,18 +3935,20 @@ var XULBrowserWindow = { this.reloadCommand.removeAttribute("disabled"); } if (gURLBar) { URLBarSetURI(aLocationURI); // Update starring UI BookmarkingUI.updateStarState(); - SocialMark.updateMarkState(); - SocialShare.update(); + if (SocialUI.enabled) { + SocialMark.updateMarkState(); + SocialShare.update(); + } } // Show or hide browser chrome based on the whitelist if (this.hideChromeForLocation(location)) { document.documentElement.setAttribute("disablechrome", "true"); } else { let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); if (ss.getTabValue(gBrowser.selectedTab, "appOrigin")) @@ -6400,17 +6401,17 @@ var gIdentityHandler = { gNavigatorBundle.getString("identity.encrypted2"); this._encryptionLabel[this.IDENTITY_MODE_IDENTIFIED] = gNavigatorBundle.getString("identity.encrypted2"); this._encryptionLabel[this.IDENTITY_MODE_UNKNOWN] = gNavigatorBundle.getString("identity.unencrypted"); this._encryptionLabel[this.IDENTITY_MODE_MIXED_DISPLAY_LOADED] = gNavigatorBundle.getString("identity.mixed_display_loaded"); this._encryptionLabel[this.IDENTITY_MODE_MIXED_ACTIVE_LOADED] = - gNavigatorBundle.getString("identity.mixed_active_loaded"); + gNavigatorBundle.getString("identity.mixed_active_loaded2"); this._encryptionLabel[this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED] = gNavigatorBundle.getString("identity.mixed_display_loaded_active_blocked"); return this._encryptionLabel; }, get _identityPopup () { delete this._identityPopup; return this._identityPopup = document.getElementById("identity-popup"); },
--- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -581,20 +581,18 @@ // cancelled a pending load which would have cleared // its anchor scroll detection temporary increment. if (aWebProgress.isTopLevel) this.mBrowser.userTypedClear += 2; if (this._shouldShowProgress(aRequest)) { if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { this.mTab.setAttribute("busy", "true"); - if (!gMultiProcessBrowser) { - if (!(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD)) - this.mTabBrowser.setTabTitleLoading(this.mTab); - } + if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD)) + this.mTabBrowser.setTabTitleLoading(this.mTab); } if (this.mTab.selected) this.mTabBrowser.mIsBusy = true; } } else if (aStateFlags & nsIWebProgressListener.STATE_STOP && aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { @@ -696,20 +694,19 @@ findBar.close(); // fix bug 253793 - turn off highlight when page changes findBar.getElement("highlight").checked = false; } // Don't clear the favicon if this onLocationChange was // triggered by a pushState or a replaceState. See bug 550565. - if (!gMultiProcessBrowser) { - if (aWebProgress.isLoadingDocument && - !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) - this.mBrowser.mIconURL = null; + if (aWebProgress.isLoadingDocument && + !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) { + this.mBrowser.mIconURL = null; } let autocomplete = this.mTabBrowser._placesAutocomplete; if (this.mBrowser.registeredOpenURI) { autocomplete.unregisterOpenPage(this.mBrowser.registeredOpenURI); delete this.mBrowser.registeredOpenURI; } // Tabs in private windows aren't registered as "Open" so @@ -825,40 +822,33 @@ ]]> </body> </method> <method name="useDefaultIcon"> <parameter name="aTab"/> <body> <![CDATA[ - // Bug 691610 - e10s support for useDefaultIcon - if (gMultiProcessBrowser) - return; - var browser = this.getBrowserForTab(aTab); - var docURIObject = browser.contentDocument.documentURIObject; + var documentURI = browser.documentURI; var icon = null; - if (browser.contentDocument instanceof ImageDocument) { + + if (browser.imageDocument) { if (Services.prefs.getBoolPref("browser.chrome.site_icons")) { let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size"); - try { - let req = browser.contentDocument.imageRequest; - if (req && - req.image && - req.image.width <= sz && - req.image.height <= sz) - icon = browser.currentURI; - } catch (e) { } + if (browser.imageDocument.width <= sz && + browser.imageDocument.height <= sz) { + icon = browser.currentURI; + } } } // Use documentURIObject in the check for shouldLoadFavIcon so that we // do the right thing with about:-style error pages. Bug 453442 - else if (this.shouldLoadFavIcon(docURIObject)) { - let url = docURIObject.prePath + "/favicon.ico"; + else if (this.shouldLoadFavIcon(documentURI)) { + let url = documentURI.prePath + "/favicon.ico"; if (!this.isFailedIcon(url)) icon = url; } this.setIcon(aTab, icon); ]]> </body> </method>
--- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -217,17 +217,16 @@ MOCHITEST_BROWSER_FILES = \ browser_minimize.js \ browser_offlineQuotaNotification.js \ browser_overflowScroll.js \ browser_page_style_menu.js \ browser_pageInfo_plugins.js \ browser_pageInfo.js \ browser_pinnedTabs.js \ browser_plainTextLinks.js \ - browser_pluginCrashCommentAndURL.js \ browser_pluginnotification.js \ browser_pluginplaypreview.js \ browser_pluginplaypreview2.js \ browser_plugins_added_dynamically.js \ browser_popupUI.js \ browser_private_browsing_window.js \ browser_private_no_prompt.js \ browser_relatedTabs.js \ @@ -316,17 +315,16 @@ MOCHITEST_BROWSER_FILES = \ 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 \ test_bug839103.html \ test_wyciwyg_copying.html \ @@ -354,9 +352,16 @@ MOCHITEST_BROWSER_FILES += \ endif ifdef MOZ_DATA_REPORTING MOCHITEST_BROWSER_FILES += \ browser_datareporting_notification.js \ $(NULL) endif +ifdef MOZ_CRASHREPORTER +MOCHITEST_BROWSER_FILES += \ + browser_pluginCrashCommentAndURL.js \ + pluginCrashCommentAndURL.html \ + $(NULL) +endif + include $(topsrcdir)/config/rules.mk
--- a/browser/base/content/test/browser_locationBarCommand.js +++ b/browser/base/content/test/browser_locationBarCommand.js @@ -27,28 +27,30 @@ saveURL = function() { runShiftLeftClickTest(); } function runAltLeftClickTest() { info("Running test: Alt left click"); triggerCommand(true, { altKey: true }); } function runShiftLeftClickTest() { - let listener = new WindowListener(getBrowserURL(), function(aWindow) { + let listener = new BrowserWindowListener(getBrowserURL(), function(aWindow) { Services.wm.removeListener(listener); addPageShowListener(aWindow.gBrowser.selectedBrowser, function() { executeSoon(function () { info("URL should be loaded in a new window"); is(gURLBar.value, "", "Urlbar reverted to original value"); is(gFocusManager.focusedElement, null, "There should be no focused element"); is(gFocusManager.focusedWindow, aWindow.gBrowser.contentWindow, "Content window should be focused"); is(aWindow.gURLBar.value, TEST_VALUE, "New URL is loaded in new window"); aWindow.close(); - runNextTest(); + + // Continue testing when the original window has focus again. + whenWindowActivated(window, runNextTest); }); }, "http://example.com/"); }); Services.wm.addListener(listener); info("Running test: Shift left click"); triggerCommand(true, { shiftKey: true }); } @@ -170,32 +172,42 @@ function addPageShowListener(browser, cb info("pageshow: " + browser.currentURI.spec); if (expectedURL && browser.currentURI.spec != expectedURL) return; // ignore pageshows for non-expected URLs browser.removeEventListener("pageshow", pageShowListener, false); cb(); }); } -function WindowListener(aURL, aCallback) { +function whenWindowActivated(win, cb) { + if (Services.focus.activeWindow == win) { + executeSoon(cb); + return; + } + + win.addEventListener("activate", function onActivate() { + win.removeEventListener("activate", onActivate); + executeSoon(cb); + }); +} + +function BrowserWindowListener(aURL, aCallback) { this.callback = aCallback; this.url = aURL; } -WindowListener.prototype = { +BrowserWindowListener.prototype = { onOpenWindow: function(aXULWindow) { - var domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + let cb = () => this.callback(domwindow); + let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindow); - var self = this; - domwindow.addEventListener("load", function() { - domwindow.removeEventListener("load", arguments.callee, false); - if (domwindow.document.location.href != self.url) - return; + let numWait = 2; + function maybeRunCallback() { + if (--numWait == 0) + cb(); + } - // Allow other window load listeners to execute before passing to callback - executeSoon(function() { - self.callback(domwindow); - }); - }, false); + whenWindowActivated(domwindow, maybeRunCallback); + whenDelayedStartupFinished(domwindow, maybeRunCallback); }, onCloseWindow: function(aXULWindow) {}, onWindowTitleChange: function(aXULWindow, aNewTitle) {} }
--- a/browser/base/content/test/browser_save_link-perwindowpb.js +++ b/browser/base/content/test/browser_save_link-perwindowpb.js @@ -72,21 +72,26 @@ function triggerSave(aWindow, aCallback) function test() { waitForExplicitFinish(); var windowsToClose = []; var gNumSet = 0; function testOnWindow(options, callback) { var win = OpenBrowserWindow(options); - win.addEventListener("load", function onLoad() { - win.removeEventListener("load", onLoad, false); - windowsToClose.push(win); - executeSoon(function() callback(win)); - }, false); + whenDelayedStartupFinished(win, () => callback(win)); + } + + function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + executeSoon(aCallback); + } + }, "browser-delayed-startup-finished", false); } mockTransferRegisterer.register(); registerCleanupFunction(function () { mockTransferRegisterer.unregister(); MockFilePicker.cleanup(); windowsToClose.forEach(function(win) {
--- a/browser/base/content/test/browser_tab_dragdrop.js +++ b/browser/base/content/test/browser_tab_dragdrop.js @@ -56,19 +56,17 @@ function test() var t = tabs[1]; var b = gBrowser.getBrowserForTab(t); gBrowser.selectedTab = t; b.addEventListener("load", function() { b.removeEventListener("load", arguments.callee, true); executeSoon(function () { var win = gBrowser.replaceTabWithWindow(t); - win.addEventListener("load", function () { - win.removeEventListener("load", arguments.callee, true); - + whenDelayedStartupFinished(win, function () { // Verify that the original window now only has the initial tab left in it. is(gBrowser.tabs[0], tabs[0], "tab0"); is(gBrowser.getBrowserForTab(gBrowser.tabs[0]).contentWindow.location, "about:blank", "tab0 uri"); executeSoon(function () { win.gBrowser.addEventListener("pageshow", function () { win.gBrowser.removeEventListener("pageshow", arguments.callee, false); executeSoon(function () { @@ -77,17 +75,17 @@ function test() var doc = b.docShell.contentViewer.DOMDocument.wrappedJSObject; clickTest(doc, win); win.close(); finish(); }); }, false); win.gBrowser.goBack(); }); - }, true); + }); }); }, true); b.loadURI("about:blank"); } var loads = 0; function waitForLoad(event, tab, listenerContainer) { @@ -106,15 +104,15 @@ function test() var listenerContainer = { listener: null } listenerContainer.listener = function (event) { return f(event, arg, listenerContainer); }; return listenerContainer.listener; } for (var i = 1; i < tabs.length; ++i) { gBrowser.getBrowserForTab(tabs[i]).addEventListener("load", fn(waitForLoad,i), true); } - setLocation(1, "data:text/html,<title>tab1</title><body>tab1<iframe>"); - setLocation(2, "data:text/plain,tab2"); - setLocation(3, "data:text/html,<title>tab3</title><body>tab3<iframe>"); - setLocation(4, "data:text/html,<body onload='clicks=0' onclick='++clicks'>"+embed); + setLocation(1, "data:text/html;charset=utf-8,<title>tab1</title><body>tab1<iframe>"); + setLocation(2, "data:text/plain;charset=utf-8,tab2"); + setLocation(3, "data:text/html;charset=utf-8,<title>tab3</title><body>tab3<iframe>"); + setLocation(4, "data:text/html;charset=utf-8,<body onload='clicks=0' onclick='++clicks'>"+embed); gBrowser.selectedTab = tabs[3]; }
--- a/browser/base/content/test/social/browser_addons.js +++ b/browser/base/content/test/social/browser_addons.js @@ -286,43 +286,41 @@ var tests = { // point should be upgraded let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" addTab(activationURL, function(tab) { let doc = tab.linkedBrowser.contentDocument; let installFrom = doc.nodePrincipal.origin; Services.prefs.setCharPref("social.whitelist", installFrom); Social.installProvider(doc, manifest2, function(addonManifest) { SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { + is(provider.manifest.version, 1, "manifest version is 1"); Social.enabled = true; - checkSocialUI(); - is(Social.provider.manifest.version, 1, "manifest version is 1") - // watch for the provider-update and tell the worker to update + + // watch for the provider-update and test the new version SocialService.registerProviderListener(function providerListener(topic, data) { if (topic != "provider-update") return; SocialService.unregisterProviderListener(providerListener); - observeProviderSet(function() { - Services.prefs.clearUserPref("social.whitelist"); - executeSoon(function() { - is(Social.provider.manifest.version, 2, "manifest version is 2"); - Social.uninstallProvider(addonManifest.origin); - gBrowser.removeTab(tab); - next(); - }) - }); + Services.prefs.clearUserPref("social.whitelist"); + let provider = Social._getProviderFromOrigin(addonManifest.origin); + is(provider.manifest.version, 2, "manifest version is 2"); + Social.uninstallProvider(addonManifest.origin); + gBrowser.removeTab(tab); + next(); }); - let port = Social.provider.getWorkerPort(); - port.postMessage({topic: "worker.update", data: true}); + + let port = provider.getWorkerPort(); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + ok(true, "got the sidebar message from provider 1"); + port.postMessage({topic: "worker.update", data: true}); + break; + } + }; + port.postMessage({topic: "test-init"}); + }); }); }); } } - - -function observeProviderSet(cb) { - Services.obs.addObserver(function providerSet(subject, topic, data) { - Services.obs.removeObserver(providerSet, "social:provider-set"); - info("social:provider-set observer was notified"); - // executeSoon to let the browser UI observers run first - executeSoon(cb); - }, "social:provider-set", false); -} \ No newline at end of file
--- a/browser/base/content/test/social/browser_social_toolbar.js +++ b/browser/base/content/test/social/browser_social_toolbar.js @@ -119,28 +119,32 @@ var tests = { Social.provider.setAmbientNotification(ambience3); try { Social.provider.setAmbientNotification(ambience4); } catch(e) {} let numIcons = Object.keys(Social.provider.ambientNotificationIcons).length; ok(numIcons == 3, "prevent adding more than 3 ambient notification icons"); - let statusIcon = document.getElementById("social-provider-button").nextSibling; + let mButton = document.getElementById("social-mark-button"); + let pButton = document.getElementById("social-provider-button"); waitForCondition(function() { - statusIcon = document.getElementById("social-provider-button").nextSibling; - return !!statusIcon; + // wait for a new button to be inserted inbetween the provider and mark + // button + return pButton.nextSibling != mButton; }, function () { + let statusIcon = pButton.nextSibling; let badge = statusIcon.getAttribute("badge"); is(badge, "42", "status value is correct"); // If there is a counter, the aria-label should reflect it. is(statusIcon.getAttribute("aria-label"), "Test Ambient 1 \u2046 (42)"); ambience.counter = 0; Social.provider.setAmbientNotification(ambience); + statusIcon = pButton.nextSibling; badge = statusIcon.getAttribute("badge"); is(badge, "", "status value is correct"); // If there is no counter, the aria-label should be the same as the label is(statusIcon.getAttribute("aria-label"), "Test Ambient 1 \u2046"); // The menu bar isn't as easy to instrument on Mac. if (navigator.platform.contains("Mac")) { next();
index 6aba01cd3ef128d834a9c3c978c431ab4c5bb3d0..e7dede528826f2ac090a5133dd6318a2fe67c73d GIT binary patch literal 2345 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 P00000NkvXXu0mjfS~_EQ
index 4ae88040c1ec0045f4579706987516d4bd467769..c019a8393b0064f8b79ecd666b790fdf798d5011 GIT binary patch literal 993957 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�SmTBAai&%r|aYTL<VDxCQK1fhXS)7lQ50+8- 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 z##�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