mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge mozilla-central to b2g-inbound
This commit is contained in:
commit
93acb4823c
@ -98,6 +98,16 @@ static nsRoleMapEntry sWAIRoleMaps[] =
|
||||
kNoReqStates
|
||||
// eARIAPressed is auto applied on any button
|
||||
},
|
||||
{ // cell
|
||||
&nsGkAtoms::cell,
|
||||
roles::CELL,
|
||||
kUseMapRole,
|
||||
eNoValue,
|
||||
eNoAction,
|
||||
eNoLiveAttr,
|
||||
eTableCell,
|
||||
kNoReqStates
|
||||
},
|
||||
{ // checkbox
|
||||
&nsGkAtoms::checkbox,
|
||||
roles::CHECKBUTTON,
|
||||
@ -631,6 +641,17 @@ static nsRoleMapEntry sWAIRoleMaps[] =
|
||||
kNoReqStates,
|
||||
eARIASelectable
|
||||
},
|
||||
{ // table
|
||||
&nsGkAtoms::table,
|
||||
roles::TABLE,
|
||||
kUseMapRole,
|
||||
eNoValue,
|
||||
eNoAction,
|
||||
eNoLiveAttr,
|
||||
eTable,
|
||||
kNoReqStates,
|
||||
eARIASelectable
|
||||
},
|
||||
{ // tablist
|
||||
&nsGkAtoms::tablist,
|
||||
roles::PAGETABLIST,
|
||||
|
@ -48,8 +48,9 @@ uint32_t
|
||||
filters::GetCell(Accessible* aAccessible)
|
||||
{
|
||||
a11y::role role = aAccessible->Role();
|
||||
return role == roles::GRID_CELL || role == roles::ROWHEADER ||
|
||||
role == roles::COLUMNHEADER ? eMatch : eSkipSubtree;
|
||||
return role == roles::CELL || role == roles::GRID_CELL ||
|
||||
role == roles::ROWHEADER || role == roles::COLUMNHEADER ?
|
||||
eMatch : eSkipSubtree;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
|
@ -68,7 +68,7 @@ ARIAGridAccessible::RowCount()
|
||||
|
||||
Accessible*
|
||||
ARIAGridAccessible::CellAt(uint32_t aRowIndex, uint32_t aColumnIndex)
|
||||
{
|
||||
{
|
||||
Accessible* row = GetRowAt(aRowIndex);
|
||||
if (!row)
|
||||
return nullptr;
|
||||
@ -79,6 +79,9 @@ ARIAGridAccessible::CellAt(uint32_t aRowIndex, uint32_t aColumnIndex)
|
||||
bool
|
||||
ARIAGridAccessible::IsColSelected(uint32_t aColIdx)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return false;
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
Accessible* row = rowIter.Next();
|
||||
if (!row)
|
||||
@ -98,6 +101,9 @@ ARIAGridAccessible::IsColSelected(uint32_t aColIdx)
|
||||
bool
|
||||
ARIAGridAccessible::IsRowSelected(uint32_t aRowIdx)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return false;
|
||||
|
||||
Accessible* row = GetRowAt(aRowIdx);
|
||||
if(!row)
|
||||
return false;
|
||||
@ -117,6 +123,9 @@ ARIAGridAccessible::IsRowSelected(uint32_t aRowIdx)
|
||||
bool
|
||||
ARIAGridAccessible::IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return false;
|
||||
|
||||
Accessible* row = GetRowAt(aRowIdx);
|
||||
if(!row)
|
||||
return false;
|
||||
@ -133,6 +142,9 @@ ARIAGridAccessible::IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx)
|
||||
uint32_t
|
||||
ARIAGridAccessible::SelectedCellCount()
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return 0;
|
||||
|
||||
uint32_t count = 0, colCount = ColCount();
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
@ -159,6 +171,9 @@ ARIAGridAccessible::SelectedCellCount()
|
||||
uint32_t
|
||||
ARIAGridAccessible::SelectedColCount()
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return 0;
|
||||
|
||||
uint32_t colCount = ColCount();
|
||||
if (!colCount)
|
||||
return 0;
|
||||
@ -193,6 +208,9 @@ ARIAGridAccessible::SelectedColCount()
|
||||
uint32_t
|
||||
ARIAGridAccessible::SelectedRowCount()
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return 0;
|
||||
|
||||
uint32_t count = 0;
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
@ -227,6 +245,9 @@ ARIAGridAccessible::SelectedRowCount()
|
||||
void
|
||||
ARIAGridAccessible::SelectedCells(nsTArray<Accessible*>* aCells)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
|
||||
Accessible* row = nullptr;
|
||||
@ -251,6 +272,9 @@ ARIAGridAccessible::SelectedCells(nsTArray<Accessible*>* aCells)
|
||||
void
|
||||
ARIAGridAccessible::SelectedCellIndices(nsTArray<uint32_t>* aCells)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
uint32_t colCount = ColCount();
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
@ -275,6 +299,9 @@ ARIAGridAccessible::SelectedCellIndices(nsTArray<uint32_t>* aCells)
|
||||
void
|
||||
ARIAGridAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
uint32_t colCount = ColCount();
|
||||
if (!colCount)
|
||||
return;
|
||||
@ -309,6 +336,9 @@ ARIAGridAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols)
|
||||
void
|
||||
ARIAGridAccessible::SelectedRowIndices(nsTArray<uint32_t>* aRows)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
Accessible* row = nullptr;
|
||||
for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) {
|
||||
@ -338,6 +368,9 @@ ARIAGridAccessible::SelectedRowIndices(nsTArray<uint32_t>* aRows)
|
||||
void
|
||||
ARIAGridAccessible::SelectRow(uint32_t aRowIdx)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
|
||||
Accessible* row = nullptr;
|
||||
@ -350,6 +383,9 @@ ARIAGridAccessible::SelectRow(uint32_t aRowIdx)
|
||||
void
|
||||
ARIAGridAccessible::SelectCol(uint32_t aColIdx)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
|
||||
Accessible* row = nullptr;
|
||||
@ -368,8 +404,10 @@ ARIAGridAccessible::SelectCol(uint32_t aColIdx)
|
||||
void
|
||||
ARIAGridAccessible::UnselectRow(uint32_t aRowIdx)
|
||||
{
|
||||
Accessible* row = GetRowAt(aRowIdx);
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
Accessible* row = GetRowAt(aRowIdx);
|
||||
if (row)
|
||||
SetARIASelected(row, false);
|
||||
}
|
||||
@ -377,6 +415,9 @@ ARIAGridAccessible::UnselectRow(uint32_t aRowIdx)
|
||||
void
|
||||
ARIAGridAccessible::UnselectCol(uint32_t aColIdx)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return;
|
||||
|
||||
AccIterator rowIter(this, filters::GetRow);
|
||||
|
||||
Accessible* row = nullptr;
|
||||
@ -421,6 +462,9 @@ nsresult
|
||||
ARIAGridAccessible::SetARIASelected(Accessible* aAccessible,
|
||||
bool aIsSelected, bool aNotify)
|
||||
{
|
||||
if (IsARIARole(nsGkAtoms::table))
|
||||
return NS_OK;
|
||||
|
||||
nsIContent *content = aAccessible->GetContent();
|
||||
NS_ENSURE_STATE(content);
|
||||
|
||||
@ -524,8 +568,8 @@ ARIAGridCellAccessible::ColIdx() const
|
||||
for (int32_t idx = 0; idx < indexInRow; idx++) {
|
||||
Accessible* cell = row->GetChildAt(idx);
|
||||
roles::Role role = cell->Role();
|
||||
if (role == roles::GRID_CELL || role == roles::ROWHEADER ||
|
||||
role == roles::COLUMNHEADER)
|
||||
if (role == roles::CELL || role == roles::GRID_CELL ||
|
||||
role == roles::ROWHEADER || role == roles::COLUMNHEADER)
|
||||
colIdx++;
|
||||
}
|
||||
|
||||
@ -593,8 +637,8 @@ ARIAGridCellAccessible::NativeAttributes()
|
||||
colIdx = colCount;
|
||||
|
||||
roles::Role role = child->Role();
|
||||
if (role == roles::GRID_CELL || role == roles::ROWHEADER ||
|
||||
role == roles::COLUMNHEADER)
|
||||
if (role == roles::CELL || role == roles::GRID_CELL ||
|
||||
role == roles::ROWHEADER || role == roles::COLUMNHEADER)
|
||||
colCount++;
|
||||
}
|
||||
|
||||
|
@ -985,6 +985,9 @@ HyperTextAccessible::NativeAttributes()
|
||||
nsIAtom*
|
||||
HyperTextAccessible::LandmarkRole() const
|
||||
{
|
||||
if (!HasOwnContent())
|
||||
return nullptr;
|
||||
|
||||
// For the html landmark elements we expose them like we do ARIA landmarks to
|
||||
// make AT navigation schemes "just work".
|
||||
if (mContent->IsHTMLElement(nsGkAtoms::nav)) {
|
||||
@ -1752,6 +1755,42 @@ HyperTextAccessible::RemoveChild(Accessible* aAccessible)
|
||||
return Accessible::RemoveChild(aAccessible);
|
||||
}
|
||||
|
||||
Relation
|
||||
HyperTextAccessible::RelationByType(RelationType aType)
|
||||
{
|
||||
Relation rel = Accessible::RelationByType(aType);
|
||||
|
||||
switch (aType) {
|
||||
case RelationType::NODE_CHILD_OF:
|
||||
if (mContent->IsMathMLElement()) {
|
||||
Accessible* parent = Parent();
|
||||
if (parent) {
|
||||
nsIContent* parentContent = parent->GetContent();
|
||||
if (parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
|
||||
// Add a relation pointing to the parent <mroot>.
|
||||
rel.AppendTarget(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RelationType::NODE_PARENT_OF:
|
||||
if (mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
|
||||
Accessible* base = GetChildAt(0);
|
||||
Accessible* index = GetChildAt(1);
|
||||
if (base && index) {
|
||||
// Append the <mroot> children in the order index, base.
|
||||
rel.AppendTarget(index);
|
||||
rel.AppendTarget(base);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return rel;
|
||||
}
|
||||
|
||||
void
|
||||
HyperTextAccessible::CacheChildren()
|
||||
{
|
||||
|
@ -62,6 +62,7 @@ public:
|
||||
|
||||
virtual void InvalidateChildren() override;
|
||||
virtual bool RemoveChild(Accessible* aAccessible) override;
|
||||
virtual Relation RelationByType(RelationType aType) override;
|
||||
|
||||
// HyperTextAccessible (static helper method)
|
||||
|
||||
|
@ -124,6 +124,9 @@ static const uintptr_t IS_PROXY = 1;
|
||||
- (void)valueDidChange;
|
||||
- (void)selectedTextDidChange;
|
||||
|
||||
// internal method to retrieve a child at a given index.
|
||||
- (id)childAt:(uint32_t)i;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// invalidates and removes all our children from our cached array.
|
||||
|
@ -27,6 +27,23 @@
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::a11y;
|
||||
|
||||
#define NSAccessibilityMathRootRadicandAttribute @"AXMathRootRadicand"
|
||||
#define NSAccessibilityMathRootIndexAttribute @"AXMathRootIndex"
|
||||
#define NSAccessibilityMathFractionNumeratorAttribute @"AXMathFractionNumerator"
|
||||
#define NSAccessibilityMathFractionDenominatorAttribute @"AXMathFractionDenominator"
|
||||
#define NSAccessibilityMathBaseAttribute @"AXMathBase"
|
||||
#define NSAccessibilityMathSubscriptAttribute @"AXMathSubscript"
|
||||
#define NSAccessibilityMathSuperscriptAttribute @"AXMathSuperscript"
|
||||
#define NSAccessibilityMathUnderAttribute @"AXMathUnder"
|
||||
#define NSAccessibilityMathOverAttribute @"AXMathOver"
|
||||
// XXX WebKit also defines the following attributes.
|
||||
// See bugs 1176970, 1176973 and 1176983.
|
||||
// - NSAccessibilityMathFencedOpenAttribute @"AXMathFencedOpen"
|
||||
// - NSAccessibilityMathFencedCloseAttribute @"AXMathFencedClose"
|
||||
// - NSAccessibilityMathLineThicknessAttribute @"AXMathLineThickness"
|
||||
// - NSAccessibilityMathPrescriptsAttribute @"AXMathPrescripts"
|
||||
// - NSAccessibilityMathPostscriptsAttribute @"AXMathPostscripts"
|
||||
|
||||
// returns the passed in object if it is not ignored. if it's ignored, will return
|
||||
// the first unignored ancestor.
|
||||
static inline id
|
||||
@ -121,6 +138,52 @@ GetClosestInterestingAccessible(id anObject)
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO);
|
||||
}
|
||||
|
||||
- (NSArray*)additionalAccessibilityAttributeNames
|
||||
{
|
||||
NSMutableArray* additional = [NSMutableArray array];
|
||||
switch (mRole) {
|
||||
case roles::MATHML_ROOT:
|
||||
[additional addObject:NSAccessibilityMathRootIndexAttribute];
|
||||
[additional addObject:NSAccessibilityMathRootRadicandAttribute];
|
||||
break;
|
||||
case roles::MATHML_SQUARE_ROOT:
|
||||
[additional addObject:NSAccessibilityMathRootRadicandAttribute];
|
||||
break;
|
||||
case roles::MATHML_FRACTION:
|
||||
[additional addObject:NSAccessibilityMathFractionNumeratorAttribute];
|
||||
[additional addObject:NSAccessibilityMathFractionDenominatorAttribute];
|
||||
// XXX bug 1176973
|
||||
// WebKit also defines NSAccessibilityMathLineThicknessAttribute
|
||||
break;
|
||||
case roles::MATHML_SUB:
|
||||
case roles::MATHML_SUP:
|
||||
case roles::MATHML_SUB_SUP:
|
||||
[additional addObject:NSAccessibilityMathBaseAttribute];
|
||||
[additional addObject:NSAccessibilityMathSubscriptAttribute];
|
||||
[additional addObject:NSAccessibilityMathSuperscriptAttribute];
|
||||
break;
|
||||
case roles::MATHML_UNDER:
|
||||
case roles::MATHML_OVER:
|
||||
case roles::MATHML_UNDER_OVER:
|
||||
[additional addObject:NSAccessibilityMathBaseAttribute];
|
||||
[additional addObject:NSAccessibilityMathUnderAttribute];
|
||||
[additional addObject:NSAccessibilityMathOverAttribute];
|
||||
break;
|
||||
// XXX bug 1176983
|
||||
// roles::MATHML_MULTISCRIPTS should also have the following attributes:
|
||||
// - NSAccessibilityMathPrescriptsAttribute
|
||||
// - NSAccessibilityMathPostscriptsAttribute
|
||||
// XXX bug 1176970
|
||||
// roles::MATHML_FENCED should also have the following attributes:
|
||||
// - NSAccessibilityMathFencedOpenAttribute
|
||||
// - NSAccessibilityMathFencedCloseAttribute
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return additional;
|
||||
}
|
||||
|
||||
- (NSArray*)accessibilityAttributeNames
|
||||
{
|
||||
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
|
||||
@ -155,7 +218,27 @@ GetClosestInterestingAccessible(id anObject)
|
||||
nil];
|
||||
}
|
||||
|
||||
return generalAttributes;
|
||||
NSArray* objectAttributes = generalAttributes;
|
||||
NSArray* additionalAttributes = [self additionalAccessibilityAttributeNames];
|
||||
if ([additionalAttributes count])
|
||||
objectAttributes = [objectAttributes arrayByAddingObjectsFromArray:additionalAttributes];
|
||||
|
||||
return objectAttributes;
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
|
||||
}
|
||||
|
||||
- (id)childAt:(uint32_t)i
|
||||
{
|
||||
NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
|
||||
|
||||
AccessibleWrap* accWrap = [self getGeckoAccessible];
|
||||
if (accWrap) {
|
||||
Accessible* acc = accWrap->GetChildAt(i);
|
||||
return acc ? GetNativeFromGeckoAccessible(acc) : nil;
|
||||
}
|
||||
|
||||
return nil;
|
||||
|
||||
NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
|
||||
}
|
||||
@ -214,6 +297,93 @@ GetClosestInterestingAccessible(id anObject)
|
||||
if ([attribute isEqualToString:NSAccessibilityHelpAttribute])
|
||||
return [self help];
|
||||
|
||||
switch (mRole) {
|
||||
case roles::MATHML_ROOT:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathRootRadicandAttribute])
|
||||
return [self childAt:0];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathRootIndexAttribute])
|
||||
return [self childAt:1];
|
||||
break;
|
||||
case roles::MATHML_SQUARE_ROOT:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathRootRadicandAttribute])
|
||||
return [self childAt:0];
|
||||
break;
|
||||
case roles::MATHML_FRACTION:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathFractionNumeratorAttribute])
|
||||
return [self childAt:0];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathFractionDenominatorAttribute])
|
||||
return [self childAt:1];
|
||||
// XXX bug 1176973
|
||||
// WebKit also defines NSAccessibilityMathLineThicknessAttribute
|
||||
break;
|
||||
case roles::MATHML_SUB:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathBaseAttribute])
|
||||
return [self childAt:0];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathSubscriptAttribute])
|
||||
return [self childAt:1];
|
||||
#ifdef DEBUG
|
||||
if ([attribute isEqualToString:NSAccessibilityMathSuperscriptAttribute])
|
||||
return nil;
|
||||
#endif
|
||||
break;
|
||||
case roles::MATHML_SUP:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathBaseAttribute])
|
||||
return [self childAt:0];
|
||||
#ifdef DEBUG
|
||||
if ([attribute isEqualToString:NSAccessibilityMathSubscriptAttribute])
|
||||
return nil;
|
||||
#endif
|
||||
if ([attribute isEqualToString:NSAccessibilityMathSuperscriptAttribute])
|
||||
return [self childAt:1];
|
||||
break;
|
||||
case roles::MATHML_SUB_SUP:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathBaseAttribute])
|
||||
return [self childAt:0];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathSubscriptAttribute])
|
||||
return [self childAt:1];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathSuperscriptAttribute])
|
||||
return [self childAt:2];
|
||||
break;
|
||||
case roles::MATHML_UNDER:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathBaseAttribute])
|
||||
return [self childAt:0];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathUnderAttribute])
|
||||
return [self childAt:1];
|
||||
#ifdef DEBUG
|
||||
if ([attribute isEqualToString:NSAccessibilityMathOverAttribute])
|
||||
return nil;
|
||||
#endif
|
||||
break;
|
||||
case roles::MATHML_OVER:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathBaseAttribute])
|
||||
return [self childAt:0];
|
||||
#ifdef DEBUG
|
||||
if ([attribute isEqualToString:NSAccessibilityMathUnderAttribute])
|
||||
return nil;
|
||||
#endif
|
||||
if ([attribute isEqualToString:NSAccessibilityMathOverAttribute])
|
||||
return [self childAt:1];
|
||||
break;
|
||||
case roles::MATHML_UNDER_OVER:
|
||||
if ([attribute isEqualToString:NSAccessibilityMathBaseAttribute])
|
||||
return [self childAt:0];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathUnderAttribute])
|
||||
return [self childAt:1];
|
||||
if ([attribute isEqualToString:NSAccessibilityMathOverAttribute])
|
||||
return [self childAt:2];
|
||||
break;
|
||||
// XXX bug 1176983
|
||||
// roles::MATHML_MULTISCRIPTS should also have the following attributes:
|
||||
// - NSAccessibilityMathPrescriptsAttribute
|
||||
// - NSAccessibilityMathPostscriptsAttribute
|
||||
// XXX bug 1176970
|
||||
// roles::MATHML_FENCED should also have the following attributes:
|
||||
// - NSAccessibilityMathFencedOpenAttribute
|
||||
// - NSAccessibilityMathFencedCloseAttribute
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
NSLog (@"!!! %@ can't respond to attribute %@", self, attribute);
|
||||
#endif
|
||||
@ -510,7 +680,8 @@ GetClosestInterestingAccessible(id anObject)
|
||||
return @"AXMathFraction";
|
||||
|
||||
case roles::MATHML_FENCED:
|
||||
// XXX This should be AXMathFence, but doing so without implementing the
|
||||
// XXX bug 1176970
|
||||
// This should be AXMathFence, but doing so without implementing the
|
||||
// whole fence interface seems to make VoiceOver crash, so we present it
|
||||
// as a row for now.
|
||||
return @"AXMathRow";
|
||||
@ -557,6 +728,8 @@ GetClosestInterestingAccessible(id anObject)
|
||||
// NS_MATHML_OPERATOR_SEPARATOR bits of nsOperatorFlags, but currently they
|
||||
// are only available from the MathML layout code. Hence we just fallback
|
||||
// to subrole AXMathOperator for now.
|
||||
// XXX bug 1175747 WebKit also creates anonymous operators for <mfenced>
|
||||
// which have subroles AXMathSeparatorOperator and AXMathFenceOperator.
|
||||
case roles::MATHML_OPERATOR:
|
||||
return @"AXMathOperator";
|
||||
|
||||
@ -607,10 +780,12 @@ struct RoleDescrComparator
|
||||
|
||||
NSString* subrole = [self subrole];
|
||||
|
||||
size_t idx = 0;
|
||||
if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
|
||||
RoleDescrComparator(subrole), &idx)) {
|
||||
return utils::LocalizedString(sRoleDescrMap[idx].description);
|
||||
if (subrole) {
|
||||
size_t idx = 0;
|
||||
if (BinarySearchIf(sRoleDescrMap, 0, ArrayLength(sRoleDescrMap),
|
||||
RoleDescrComparator(subrole), &idx)) {
|
||||
return utils::LocalizedString(sRoleDescrMap[idx].description);
|
||||
}
|
||||
}
|
||||
|
||||
return NSAccessibilityRoleDescription([self role], subrole);
|
||||
|
@ -125,6 +125,19 @@
|
||||
|
||||
obj = {
|
||||
role: ROLE_MATHML_ROOT,
|
||||
relations: {
|
||||
RELATION_NODE_PARENT_OF: ["mroot_index", "mroot_base"]
|
||||
},
|
||||
children: [
|
||||
{
|
||||
role: ROLE_MATHML_IDENTIFIER,
|
||||
relations: { RELATION_NODE_CHILD_OF: "mroot" }
|
||||
},
|
||||
{
|
||||
role: ROLE_MATHML_NUMBER,
|
||||
relations: { RELATION_NODE_CHILD_OF: "mroot" }
|
||||
}
|
||||
]
|
||||
};
|
||||
testElm("mroot", obj);
|
||||
|
||||
@ -386,8 +399,8 @@
|
||||
<mn>2</mn>
|
||||
</mfrac>
|
||||
<mroot id="mroot">
|
||||
<mi>x</mi>
|
||||
<mn>5</mn>
|
||||
<mi id="mroot_base">x</mi>
|
||||
<mn id="mroot_index">5</mn>
|
||||
</mroot>
|
||||
<mspace width="1em"/>
|
||||
<mfenced id="mfenced" close="[" open="]" separators=".">
|
||||
|
@ -1,6 +1,7 @@
|
||||
[DEFAULT]
|
||||
|
||||
[test_headers_ariagrid.html]
|
||||
[test_headers_ariatable.html]
|
||||
[test_headers_listbox.xul]
|
||||
[test_headers_table.html]
|
||||
[test_headers_tree.xul]
|
||||
|
96
accessible/tests/mochitest/table/test_headers_ariatable.html
Normal file
96
accessible/tests/mochitest/table/test_headers_ariatable.html
Normal file
@ -0,0 +1,96 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en">
|
||||
<html>
|
||||
<head>
|
||||
<title>Table header information cells for ARIA table</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
|
||||
<script type="application/javascript"
|
||||
src="../common.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="../table.js"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
|
||||
function doTest()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// column and row headers from markup
|
||||
|
||||
headerInfoMap = [
|
||||
{
|
||||
cell: "table_dc_1",
|
||||
rowHeaderCells: [ "table_rh_1" ],
|
||||
columnHeaderCells: [ "table_ch_2" ]
|
||||
},
|
||||
{
|
||||
cell: "table_dc_2",
|
||||
rowHeaderCells: [ "table_rh_1" ],
|
||||
columnHeaderCells: [ "table_ch_3" ]
|
||||
},
|
||||
{
|
||||
cell: "table_dc_3",
|
||||
rowHeaderCells: [ "table_rh_2" ],
|
||||
columnHeaderCells: [ "table_ch_2" ]
|
||||
},
|
||||
{
|
||||
cell: "table_dc_4",
|
||||
rowHeaderCells: [ "table_rh_2" ],
|
||||
columnHeaderCells: [ "table_ch_3" ]
|
||||
},
|
||||
{
|
||||
cell: "table_rh_1",
|
||||
rowHeaderCells: [],
|
||||
columnHeaderCells: [ "table_ch_1" ]
|
||||
},
|
||||
{
|
||||
cell: "table_rh_2",
|
||||
rowHeaderCells: [],
|
||||
columnHeaderCells: [ "table_ch_1" ]
|
||||
}
|
||||
];
|
||||
|
||||
testHeaderCells(headerInfoMap);
|
||||
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
addA11yLoadEvent(doTest);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a target="_blank"
|
||||
title="support ARIA table and cell roles"
|
||||
href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173364">Bug 1173364</a>
|
||||
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<div role="table">
|
||||
<div role="row">
|
||||
<span id="table_ch_1" role="columnheader">col_1</span>
|
||||
<span id="table_ch_2" role="columnheader">col_2</span>
|
||||
<span id="table_ch_3" role="columnheader">col_3</span>
|
||||
</div>
|
||||
<div role="row">
|
||||
<span id="table_rh_1" role="rowheader">row_1</span>
|
||||
<span id="table_dc_1" role="cell">cell1</span>
|
||||
<span id="table_dc_2" role="cell">cell2</span>
|
||||
</div>
|
||||
<div role="row">
|
||||
<span id="table_rh_2" role="rowheader">row_2</span>
|
||||
<span id="table_dc_3" role="cell">cell3</span>
|
||||
<span id="table_dc_4" role="cell">cell4</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -11,6 +11,7 @@ skip-if = true # Bug 561508
|
||||
[test_aria_list.html]
|
||||
[test_aria_menu.html]
|
||||
[test_aria_presentation.html]
|
||||
[test_aria_table.html]
|
||||
[test_brokencontext.html]
|
||||
[test_button.xul]
|
||||
[test_canvas.html]
|
||||
|
63
accessible/tests/mochitest/tree/test_aria_table.html
Normal file
63
accessible/tests/mochitest/tree/test_aria_table.html
Normal file
@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ARIA table tests</title>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
|
||||
|
||||
<script type="application/javascript"
|
||||
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
|
||||
<script type="application/javascript"
|
||||
src="../common.js"></script>
|
||||
<script type="application/javascript"
|
||||
src="../role.js"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
function doTest()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// table having rowgroups
|
||||
|
||||
var accTree =
|
||||
{ TABLE: [
|
||||
{ GROUPING: [
|
||||
{ ROW: [
|
||||
{ CELL: [
|
||||
{ TEXT_LEAF: [ ] }
|
||||
] }
|
||||
] }
|
||||
] },
|
||||
] };
|
||||
|
||||
testAccessibleTree("table", accTree);
|
||||
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
addA11yLoadEvent(doTest);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a target="_blank"
|
||||
title="support ARIA table and cell roles"
|
||||
href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173364">
|
||||
Bug 1173364
|
||||
</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none"></div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<div id="table" role="table">
|
||||
<div role="rowgroup">
|
||||
<div role="row">
|
||||
<div role="cell">cell</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -4,9 +4,9 @@
|
||||
|
||||
# Integrates the xpcshell test runner with mach.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import mozpack.path as mozpath
|
||||
|
||||
|
@ -340,6 +340,7 @@ pref("browser.urlbar.restrict.bookmark", "*");
|
||||
pref("browser.urlbar.restrict.tag", "+");
|
||||
pref("browser.urlbar.restrict.openpage", "%");
|
||||
pref("browser.urlbar.restrict.typed", "~");
|
||||
pref("browser.urlbar.restrict.searches", "$");
|
||||
pref("browser.urlbar.match.title", "#");
|
||||
pref("browser.urlbar.match.url", "@");
|
||||
|
||||
@ -348,7 +349,11 @@ pref("browser.urlbar.match.url", "@");
|
||||
pref("browser.urlbar.suggest.history", true);
|
||||
pref("browser.urlbar.suggest.bookmark", true);
|
||||
pref("browser.urlbar.suggest.openpage", true);
|
||||
#ifdef NIGHTLY_BUILD
|
||||
pref("browser.urlbar.suggest.searches", true);
|
||||
#else
|
||||
pref("browser.urlbar.suggest.searches", false);
|
||||
#endif
|
||||
|
||||
// Restrictions to current suggestions can also be applied (intersection).
|
||||
// Typed suggestion works only if history is set to true.
|
||||
|
@ -477,7 +477,6 @@ var FullScreen = {
|
||||
}
|
||||
|
||||
// Track whether mouse is near the toolbox
|
||||
this._isChromeCollapsed = false;
|
||||
if (trackMouse && !this.useLionFullScreen) {
|
||||
let rect = gBrowser.mPanelContainer.getBoundingClientRect();
|
||||
this._mouseTargetRect = {
|
||||
@ -488,6 +487,8 @@ var FullScreen = {
|
||||
};
|
||||
MousePosTracker.addListener(this);
|
||||
}
|
||||
|
||||
this._isChromeCollapsed = false;
|
||||
},
|
||||
|
||||
hideNavToolbox: function (aAnimate = false) {
|
||||
|
@ -290,13 +290,10 @@ toolbar[customizing] > .overflow-button {
|
||||
|
||||
%endif
|
||||
|
||||
#main-window[inDOMFullscreen] #navigator-toolbox,
|
||||
#main-window[inDOMFullscreen] #fullscr-toggler,
|
||||
#main-window[inDOMFullscreen] #sidebar-box,
|
||||
#main-window[inDOMFullscreen] #sidebar-splitter {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
#main-window[inFullscreen][inDOMFullscreen] #navigator-toolbox,
|
||||
#main-window[inFullscreen][inDOMFullscreen] #fullscr-toggler,
|
||||
#main-window[inFullscreen][inDOMFullscreen] #sidebar-box,
|
||||
#main-window[inFullscreen][inDOMFullscreen] #sidebar-splitter,
|
||||
#main-window[inFullscreen]:not([OSXLionFullscreen]) toolbar:not([fullscreentoolbar=true]),
|
||||
#main-window[inFullscreen] #global-notificationbox,
|
||||
#main-window[inFullscreen] #high-priority-global-notificationbox {
|
||||
|
@ -2571,6 +2571,7 @@ let gMenuButtonUpdateBadge = {
|
||||
}
|
||||
PanelUI.menuButton.classList.add("badged-button");
|
||||
Services.obs.addObserver(this, "update-staged", false);
|
||||
Services.obs.addObserver(this, "update-downloaded", false);
|
||||
}
|
||||
},
|
||||
|
||||
@ -2579,6 +2580,7 @@ let gMenuButtonUpdateBadge = {
|
||||
this.timer.cancel();
|
||||
if (this.enabled) {
|
||||
Services.obs.removeObserver(this, "update-staged");
|
||||
Services.obs.removeObserver(this, "update-downloaded");
|
||||
PanelUI.panel.removeEventListener("popupshowing", this, true);
|
||||
this.enabled = false;
|
||||
}
|
||||
@ -2602,72 +2604,55 @@ let gMenuButtonUpdateBadge = {
|
||||
},
|
||||
|
||||
observe: function (subject, topic, status) {
|
||||
const STATE_DOWNLOADING = "downloading";
|
||||
const STATE_PENDING = "pending";
|
||||
const STATE_PENDING_SVC = "pending-service";
|
||||
const STATE_APPLIED = "applied";
|
||||
const STATE_APPLIED_SVC = "applied-service";
|
||||
const STATE_FAILED = "failed";
|
||||
|
||||
let updateButton = document.getElementById("PanelUI-update-status");
|
||||
|
||||
let updateButtonText;
|
||||
let stringId;
|
||||
|
||||
// Update the UI when the background updater is finished.
|
||||
switch (status) {
|
||||
case STATE_APPLIED:
|
||||
case STATE_APPLIED_SVC:
|
||||
case STATE_PENDING:
|
||||
case STATE_PENDING_SVC:
|
||||
if (this.timer) {
|
||||
return;
|
||||
}
|
||||
// Give the user badgeWaitTime seconds to react before prompting.
|
||||
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
|
||||
this.timer.TYPE_ONE_SHOT);
|
||||
// The timer callback will call uninit() when it completes.
|
||||
break;
|
||||
case STATE_FAILED:
|
||||
// Background update has failed, let's show the UI responsible for
|
||||
// prompting the user to update manually.
|
||||
PanelUI.menuButton.setAttribute("update-status", "failed");
|
||||
PanelUI.menuButton.setAttribute("badge", "!");
|
||||
|
||||
stringId = "appmenu.updateFailed.description";
|
||||
updateButtonText = gNavigatorBundle.getString(stringId);
|
||||
|
||||
updateButton.setAttribute("label", updateButtonText);
|
||||
updateButton.setAttribute("update-status", "failed");
|
||||
updateButton.hidden = false;
|
||||
|
||||
PanelUI.panel.addEventListener("popupshowing", this, true);
|
||||
|
||||
this.uninit();
|
||||
break;
|
||||
if (status == "failed") {
|
||||
// Background update has failed, let's show the UI responsible for
|
||||
// prompting the user to update manually.
|
||||
this.displayBadge(false);
|
||||
this.uninit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Give the user badgeWaitTime seconds to react before prompting.
|
||||
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
|
||||
this.timer.TYPE_ONE_SHOT);
|
||||
// The timer callback will call uninit() when it completes.
|
||||
},
|
||||
|
||||
notify: function () {
|
||||
// If the update is successfully applied, or if the updater has fallen back
|
||||
// to non-staged updates, add a badge to the hamburger menu to indicate an
|
||||
// update will be applied once the browser restarts.
|
||||
PanelUI.menuButton.setAttribute("update-status", "succeeded");
|
||||
this.displayBadge(true);
|
||||
this.uninit();
|
||||
},
|
||||
|
||||
let brandBundle = document.getElementById("bundle_brand");
|
||||
let brandShortName = brandBundle.getString("brandShortName");
|
||||
stringId = "appmenu.restartNeeded.description";
|
||||
updateButtonText = gNavigatorBundle.getFormattedString(stringId,
|
||||
[brandShortName]);
|
||||
displayBadge: function (succeeded) {
|
||||
let status = succeeded ? "succeeded" : "failed";
|
||||
PanelUI.menuButton.setAttribute("update-status", status);
|
||||
if (!succeeded) {
|
||||
PanelUI.menuButton.setAttribute("badge", "!");
|
||||
}
|
||||
|
||||
let stringId;
|
||||
let updateButtonText;
|
||||
if (succeeded) {
|
||||
let brandBundle = document.getElementById("bundle_brand");
|
||||
let brandShortName = brandBundle.getString("brandShortName");
|
||||
stringId = "appmenu.restartNeeded.description";
|
||||
updateButtonText = gNavigatorBundle.getFormattedString(stringId,
|
||||
[brandShortName]);
|
||||
} else {
|
||||
stringId = "appmenu.updateFailed.description";
|
||||
updateButtonText = gNavigatorBundle.getString(stringId);
|
||||
}
|
||||
|
||||
let updateButton = document.getElementById("PanelUI-update-status");
|
||||
updateButton.setAttribute("label", updateButtonText);
|
||||
updateButton.setAttribute("update-status", "succeeded");
|
||||
updateButton.setAttribute("update-status", status);
|
||||
updateButton.hidden = false;
|
||||
|
||||
PanelUI.panel.addEventListener("popupshowing", this, true);
|
||||
this.uninit();
|
||||
},
|
||||
|
||||
handleEvent: function(e) {
|
||||
@ -3147,9 +3132,12 @@ var PrintPreviewListener = {
|
||||
|
||||
getPrintPreviewBrowser: function () {
|
||||
if (!this._printPreviewTab) {
|
||||
let browser = gBrowser.selectedTab.linkedBrowser;
|
||||
let forceNotRemote = gMultiProcessBrowser && !browser.isRemoteBrowser;
|
||||
this._tabBeforePrintPreview = gBrowser.selectedTab;
|
||||
this._printPreviewTab = gBrowser.loadOneTab("about:blank",
|
||||
{ inBackground: false });
|
||||
{ inBackground: false,
|
||||
forceNotRemote });
|
||||
gBrowser.selectedTab = this._printPreviewTab;
|
||||
}
|
||||
return gBrowser.getBrowserForTab(this._printPreviewTab);
|
||||
@ -6605,11 +6593,6 @@ var gIdentityHandler = {
|
||||
delete this._identityBox;
|
||||
return this._identityBox = document.getElementById("identity-box");
|
||||
},
|
||||
get _identityPopupContentBox () {
|
||||
delete this._identityPopupContentBox;
|
||||
return this._identityPopupContentBox =
|
||||
document.getElementById("identity-popup-content-box");
|
||||
},
|
||||
get _identityPopupContentHost () {
|
||||
delete this._identityPopupContentHost;
|
||||
return this._identityPopupContentHost =
|
||||
@ -6630,6 +6613,16 @@ var gIdentityHandler = {
|
||||
return this._identityPopupContentVerif =
|
||||
document.getElementById("identity-popup-content-verifier");
|
||||
},
|
||||
get _identityPopupSecurityContent () {
|
||||
delete this._identityPopupSecurityContent;
|
||||
return this._identityPopupSecurityContent =
|
||||
document.getElementById("identity-popup-security-content");
|
||||
},
|
||||
get _identityPopupSecurityView () {
|
||||
delete this._identityPopupSecurityView;
|
||||
return this._identityPopupSecurityView =
|
||||
document.getElementById("identity-popup-securityView");
|
||||
},
|
||||
get _identityIconLabel () {
|
||||
delete this._identityIconLabel;
|
||||
return this._identityIconLabel = document.getElementById("identity-icon-label");
|
||||
@ -6685,6 +6678,11 @@ var gIdentityHandler = {
|
||||
this._identityPopup.hidePopup();
|
||||
},
|
||||
|
||||
showSubView(name, anchor) {
|
||||
let view = document.getElementById("identity-popup-multiView");
|
||||
view.showSubView(`identity-popup-${name}View`, anchor);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to parse out the important parts of _lastStatus (of the SSL cert in
|
||||
* particular) for use in constructing identity UI strings
|
||||
@ -6859,8 +6857,10 @@ var gIdentityHandler = {
|
||||
this.setIdentityMessages(newMode);
|
||||
|
||||
// Update the popup too, if it's open
|
||||
if (this._identityPopup.state == "open")
|
||||
if (this._identityPopup.state == "open") {
|
||||
this.setPopupMessages(newMode);
|
||||
this.updateSitePermissions();
|
||||
}
|
||||
|
||||
this._mode = newMode;
|
||||
},
|
||||
@ -6945,7 +6945,8 @@ var gIdentityHandler = {
|
||||
setPopupMessages : function(newMode) {
|
||||
|
||||
this._identityPopup.className = newMode;
|
||||
this._identityPopupContentBox.className = newMode;
|
||||
this._identityPopupSecurityView.className = newMode;
|
||||
this._identityPopupSecurityContent.className = newMode;
|
||||
|
||||
// Initialize the optional strings to empty values
|
||||
let supplemental = "";
|
||||
@ -6953,16 +6954,11 @@ var gIdentityHandler = {
|
||||
let host = "";
|
||||
let owner = "";
|
||||
|
||||
if (newMode == this.IDENTITY_MODE_CHROMEUI) {
|
||||
let brandBundle = document.getElementById("bundle_brand");
|
||||
host = brandBundle.getString("brandFullName");
|
||||
} else {
|
||||
try {
|
||||
host = this.getEffectiveHost();
|
||||
} catch (e) {
|
||||
// Some URIs might have no hosts.
|
||||
host = this._lastUri.specIgnoringRef;
|
||||
}
|
||||
try {
|
||||
host = this.getEffectiveHost();
|
||||
} catch (e) {
|
||||
// Some URIs might have no hosts.
|
||||
host = this._lastUri.specIgnoringRef;
|
||||
}
|
||||
|
||||
switch (newMode) {
|
||||
@ -6999,10 +6995,13 @@ var gIdentityHandler = {
|
||||
// Push the appropriate strings out to the UI. Need to use |value| for the
|
||||
// host as it's a <label> that will be cropped if too long. Using
|
||||
// |textContent| would simply wrap the value.
|
||||
this._identityPopupContentHost.value = host;
|
||||
this._identityPopupContentHost.setAttribute("value", host);
|
||||
this._identityPopupContentOwner.textContent = owner;
|
||||
this._identityPopupContentSupp.textContent = supplemental;
|
||||
this._identityPopupContentVerif.textContent = verifier;
|
||||
|
||||
// Hide subviews when updating panel information.
|
||||
document.getElementById("identity-popup-multiView").showMainView();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -7127,6 +7126,7 @@ var gIdentityHandler = {
|
||||
|
||||
let label = document.createElement("label");
|
||||
label.setAttribute("flex", "1");
|
||||
label.setAttribute("class", "identity-popup-permission-label");
|
||||
label.setAttribute("control", menulist.getAttribute("id"));
|
||||
label.setAttribute("value", SitePermissions.getPermissionLabel(aPermission));
|
||||
|
||||
|
@ -131,6 +131,7 @@ skip-if = e10s # Bug 1093153 - no about:home support yet
|
||||
[browser_alltabslistener.js]
|
||||
[browser_autocomplete_a11y_label.js]
|
||||
skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir (works on its own)
|
||||
[browser_autocomplete_cursor.js]
|
||||
[browser_autocomplete_enter_race.js]
|
||||
[browser_autocomplete_no_title.js]
|
||||
[browser_autocomplete_autoselect.js]
|
||||
|
@ -0,0 +1,25 @@
|
||||
add_task(function*() {
|
||||
// This test is only relevant if UnifiedComplete is enabled.
|
||||
let ucpref = Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete");
|
||||
Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", ucpref);
|
||||
});
|
||||
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla", {animate: false});
|
||||
yield promiseTabLoaded(tab);
|
||||
yield promiseAutocompleteResultPopup("www.mozilla.org");
|
||||
|
||||
gURLBar.selectTextRange(4, 4);
|
||||
|
||||
is(gURLBar.popup.state, "open", "Popup should be open");
|
||||
is(gURLBar.popup.richlistbox.selectedIndex, 0, "Should have selected something");
|
||||
|
||||
EventUtils.synthesizeKey("VK_RIGHT", {});
|
||||
yield promisePopupHidden(gURLBar.popup);
|
||||
|
||||
is(gURLBar.selectionStart, 5, "Should have moved the cursor");
|
||||
is(gURLBar.selectionEnd, 5, "And not selected anything");
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
@ -402,17 +402,22 @@ var gAllTests = [
|
||||
// left to clear, the checkbox will be disabled.
|
||||
var cb = this.win.document.querySelectorAll(
|
||||
"#itemList > [preference='privacy.cpd.formdata']");
|
||||
ok(cb.length == 1 && cb[0].disabled && !cb[0].checked,
|
||||
"There is no formdata history, checkbox should be disabled and be " +
|
||||
"cleared to reduce user confusion (bug 497664).");
|
||||
|
||||
var cb = this.win.document.querySelectorAll(
|
||||
"#itemList > [preference='privacy.cpd.history']");
|
||||
ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
|
||||
"There is no history, but history checkbox should always be enabled " +
|
||||
"and will be checked from previous preference.");
|
||||
// Wait until the checkbox is disabled. This is done asynchronously
|
||||
// from Sanitizer.init() as FormHistory.count() is a purely async API.
|
||||
promiseWaitForCondition(() => cb[0].disabled).then(() => {
|
||||
ok(cb.length == 1 && cb[0].disabled && !cb[0].checked,
|
||||
"There is no formdata history, checkbox should be disabled and be " +
|
||||
"cleared to reduce user confusion (bug 497664).");
|
||||
|
||||
this.acceptDialog();
|
||||
cb = this.win.document.querySelectorAll(
|
||||
"#itemList > [preference='privacy.cpd.history']");
|
||||
ok(cb.length == 1 && !cb[0].disabled && cb[0].checked,
|
||||
"There is no history, but history checkbox should always be enabled " +
|
||||
"and will be checked from previous preference.");
|
||||
|
||||
this.acceptDialog();
|
||||
});
|
||||
}
|
||||
wh.open();
|
||||
},
|
||||
|
@ -184,6 +184,22 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="onKeyPress">
|
||||
<parameter name="aEvent"/>
|
||||
<body><![CDATA[
|
||||
switch (aEvent.keyCode) {
|
||||
case KeyEvent.DOM_VK_LEFT:
|
||||
case KeyEvent.DOM_VK_RIGHT:
|
||||
case KeyEvent.DOM_VK_HOME:
|
||||
this.popup.hidePopup();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
return this.handleKeyPress(aEvent);
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<field name="_mayTrimURLs">true</field>
|
||||
<method name="trimValue">
|
||||
<parameter name="aURL"/>
|
||||
|
@ -10,35 +10,74 @@
|
||||
gIdentityHandler.onPopupShown(event);"
|
||||
orient="vertical"
|
||||
level="top">
|
||||
<hbox id="identity-popup-container" align="top">
|
||||
<image id="identity-popup-icon"/>
|
||||
<vbox id="identity-popup-content-box">
|
||||
<label id="identity-popup-content-host"
|
||||
class="identity-popup-description"
|
||||
crop="end"/>
|
||||
<label id="identity-popup-connection-secure"
|
||||
class="identity-popup-label"
|
||||
value="&identity.connectionSecure;"/>
|
||||
<label id="identity-popup-connection-not-secure"
|
||||
class="identity-popup-label"
|
||||
value="&identity.connectionNotSecure;"/>
|
||||
<description id="identity-popup-content-owner"
|
||||
class="identity-popup-description"/>
|
||||
<description id="identity-popup-content-supplemental"
|
||||
class="identity-popup-description"/>
|
||||
<description id="identity-popup-content-verifier"
|
||||
class="identity-popup-description"/>
|
||||
<vbox id="identity-popup-permissions">
|
||||
<label class="identity-popup-label header"
|
||||
value="&identity.permissions;"/>
|
||||
<vbox id="identity-popup-permission-list" class="indent"/>
|
||||
|
||||
<broadcasterset>
|
||||
<broadcaster id="identity-popup-content-host" value=""/>
|
||||
</broadcasterset>
|
||||
|
||||
<panelmultiview id="identity-popup-multiView"
|
||||
mainViewId="identity-popup-mainView">
|
||||
<panelview id="identity-popup-mainView" flex="1">
|
||||
|
||||
<!-- Security Section -->
|
||||
<hbox class="identity-popup-section">
|
||||
<vbox id="identity-popup-security-content" flex="1">
|
||||
<label class="identity-popup-headline" crop="end">
|
||||
<observes element="identity-popup-content-host" attribute="value"/>
|
||||
</label>
|
||||
<label class="identity-popup-connection-secure identity-popup-text"
|
||||
value="&identity.connectionSecure;"/>
|
||||
<label class="identity-popup-connection-not-secure identity-popup-text"
|
||||
value="&identity.connectionNotSecure;"/>
|
||||
<label class="identity-popup-connection-internal identity-popup-text"
|
||||
value="&identity.connectionInternal;"/>
|
||||
</vbox>
|
||||
<button class="identity-popup-expander"
|
||||
oncommand="gIdentityHandler.showSubView('security', this)"/>
|
||||
</hbox>
|
||||
|
||||
<!-- Permissions Section -->
|
||||
<hbox id="identity-popup-permissions" class="identity-popup-section">
|
||||
<vbox id="identity-popup-permissions-content" flex="1">
|
||||
<label class="identity-popup-text identity-popup-headline"
|
||||
value="&identity.permissions;"/>
|
||||
<vbox id="identity-popup-permission-list"/>
|
||||
</vbox>
|
||||
</hbox>
|
||||
|
||||
<spacer flex="1"/>
|
||||
|
||||
<!-- More Information Button -->
|
||||
<hbox id="identity-popup-button-container" align="center">
|
||||
<button id="identity-popup-more-info-button" flex="1"
|
||||
label="&identity.moreInfoLinkText2;"
|
||||
oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
|
||||
</hbox>
|
||||
</panelview>
|
||||
|
||||
<!-- Security SubView -->
|
||||
<panelview id="identity-popup-securityView" flex="1">
|
||||
<vbox id="identity-popup-securityView-header">
|
||||
<label class="identity-popup-headline" crop="end">
|
||||
<observes element="identity-popup-content-host" attribute="value"/>
|
||||
</label>
|
||||
<label class="identity-popup-connection-secure identity-popup-text"
|
||||
value="&identity.connectionSecure;"/>
|
||||
<label class="identity-popup-connection-not-secure identity-popup-text"
|
||||
value="&identity.connectionNotSecure;"/>
|
||||
<label class="identity-popup-connection-internal identity-popup-text"
|
||||
value="&identity.connectionInternal;"/>
|
||||
</vbox>
|
||||
</vbox>
|
||||
</hbox>
|
||||
<!-- Footer button to open security page info -->
|
||||
<hbox id="identity-popup-button-container" align="center">
|
||||
<button id="identity-popup-more-info-button" flex="1"
|
||||
label="&identity.moreInfoLinkText2;"
|
||||
oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
|
||||
</hbox>
|
||||
|
||||
<description id="identity-popup-securityView-connection"
|
||||
class="identity-popup-text">&identity.connectionVerified;</description>
|
||||
|
||||
<description id="identity-popup-content-owner"
|
||||
class="identity-popup-text"/>
|
||||
<description id="identity-popup-content-supplemental"
|
||||
class="identity-popup-text"/>
|
||||
<description id="identity-popup-content-verifier"
|
||||
class="identity-popup-text"/>
|
||||
</panelview>
|
||||
</panelmultiview>
|
||||
</panel>
|
||||
|
@ -163,13 +163,7 @@
|
||||
|
||||
this._subViewObserver.disconnect();
|
||||
|
||||
this._transitioning = true;
|
||||
|
||||
this._viewContainer.addEventListener("transitionend", function trans() {
|
||||
this._viewContainer.removeEventListener("transitionend", trans);
|
||||
this._transitioning = false;
|
||||
}.bind(this));
|
||||
this._viewContainer.style.height = this._mainViewHeight + "px";
|
||||
this._setViewContainerHeight(this._mainViewHeight);
|
||||
|
||||
this.setAttribute("viewtype", "main");
|
||||
}
|
||||
@ -211,14 +205,8 @@
|
||||
|
||||
this._mainViewHeight = this._viewStack.clientHeight;
|
||||
|
||||
this._transitioning = true;
|
||||
this._viewContainer.addEventListener("transitionend", function trans() {
|
||||
this._viewContainer.removeEventListener("transitionend", trans);
|
||||
this._transitioning = false;
|
||||
}.bind(this));
|
||||
|
||||
let newHeight = this._heightOfSubview(viewNode, this._subViews);
|
||||
this._viewContainer.style.height = newHeight + "px";
|
||||
this._setViewContainerHeight(newHeight);
|
||||
|
||||
this._subViewObserver.observe(viewNode, {
|
||||
attributes: true,
|
||||
@ -229,6 +217,22 @@
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_setViewContainerHeight">
|
||||
<parameter name="aHeight"/>
|
||||
<body><![CDATA[
|
||||
let container = this._viewContainer;
|
||||
this._transitioning = true;
|
||||
|
||||
let onTransitionEnd = () => {
|
||||
container.removeEventListener("transitionend", onTransitionEnd);
|
||||
this._transitioning = false;
|
||||
};
|
||||
|
||||
container.addEventListener("transitionend", onTransitionEnd);
|
||||
container.style.height = `${aHeight}px`;
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="_shiftMainView">
|
||||
<parameter name="aAnchor"/>
|
||||
<body><![CDATA[
|
||||
@ -240,15 +244,24 @@
|
||||
let mainViewRect = this._mainViewContainer.getBoundingClientRect();
|
||||
let center = aAnchor.clientWidth / 2;
|
||||
let direction = aAnchor.ownerDocument.defaultView.getComputedStyle(aAnchor, null).direction;
|
||||
let edge, target;
|
||||
let edge;
|
||||
if (direction == "ltr") {
|
||||
edge = anchorRect.left - mainViewRect.left;
|
||||
target = "-" + (edge + center);
|
||||
} else {
|
||||
edge = mainViewRect.right - anchorRect.right;
|
||||
target = edge + center;
|
||||
}
|
||||
this._mainViewContainer.style.transform = "translateX(" + target + "px)";
|
||||
|
||||
// If the anchor is an element on the far end of the mainView we
|
||||
// don't want to shift the mainView too far, we would reveal empty
|
||||
// space otherwise.
|
||||
let cstyle = window.getComputedStyle(document.documentElement, null);
|
||||
let exitSubViewGutterWidth =
|
||||
cstyle.getPropertyValue("--panel-ui-exit-subview-gutter-width");
|
||||
let maxShift = mainViewRect.width - parseInt(exitSubViewGutterWidth);
|
||||
let target = Math.min(maxShift, edge + center);
|
||||
|
||||
let neg = direction == "ltr" ? "-" : "";
|
||||
this._mainViewContainer.style.transform = `translateX(${neg}${target}px)`;
|
||||
aAnchor.setAttribute("panel-multiview-anchor", true);
|
||||
} else {
|
||||
this._mainViewContainer.style.transform = "";
|
||||
|
@ -62,7 +62,6 @@
|
||||
// <https://github.com/yannickcr/eslint-plugin-react#list-of-supported-rules>
|
||||
"react/jsx-quotes": [2, "double", "avoid-escape"],
|
||||
"react/jsx-no-undef": 2,
|
||||
// Need to fix instances where this is failing.
|
||||
"react/jsx-sort-props": 2,
|
||||
"react/jsx-sort-prop-types": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
@ -71,8 +70,7 @@
|
||||
"react/no-did-mount-set-state": 0,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-unknown-property": 2,
|
||||
// Need to fix instances where this is currently failing
|
||||
"react/prop-types": 0,
|
||||
"react/prop-types": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/wrap-multilines": 2,
|
||||
// Not worth it: React is defined globally
|
||||
|
@ -146,6 +146,8 @@ loop.contacts = (function(_, mozL10n) {
|
||||
|
||||
const ContactDropdown = React.createClass({displayName: "ContactDropdown",
|
||||
propTypes: {
|
||||
// If the contact is blocked or not.
|
||||
blocked: React.PropTypes.bool.isRequired,
|
||||
canEdit: React.PropTypes.bool,
|
||||
handleAction: React.PropTypes.func.isRequired
|
||||
},
|
||||
@ -334,7 +336,9 @@ loop.contacts = (function(_, mozL10n) {
|
||||
|
||||
propTypes: {
|
||||
notifications: React.PropTypes.instanceOf(
|
||||
loop.shared.models.NotificationCollection).isRequired
|
||||
loop.shared.models.NotificationCollection).isRequired,
|
||||
// Callback to handle entry to the add/edit contact form.
|
||||
startForm: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
/**
|
||||
@ -624,7 +628,9 @@ loop.contacts = (function(_, mozL10n) {
|
||||
mixins: [React.addons.LinkedStateMixin],
|
||||
|
||||
propTypes: {
|
||||
mode: React.PropTypes.string
|
||||
mode: React.PropTypes.string,
|
||||
// Callback used to change the selected tab - it is passed the tab name.
|
||||
selectTab: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -146,6 +146,8 @@ loop.contacts = (function(_, mozL10n) {
|
||||
|
||||
const ContactDropdown = React.createClass({
|
||||
propTypes: {
|
||||
// If the contact is blocked or not.
|
||||
blocked: React.PropTypes.bool.isRequired,
|
||||
canEdit: React.PropTypes.bool,
|
||||
handleAction: React.PropTypes.func.isRequired
|
||||
},
|
||||
@ -334,7 +336,9 @@ loop.contacts = (function(_, mozL10n) {
|
||||
|
||||
propTypes: {
|
||||
notifications: React.PropTypes.instanceOf(
|
||||
loop.shared.models.NotificationCollection).isRequired
|
||||
loop.shared.models.NotificationCollection).isRequired,
|
||||
// Callback to handle entry to the add/edit contact form.
|
||||
startForm: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
/**
|
||||
@ -624,7 +628,9 @@ loop.contacts = (function(_, mozL10n) {
|
||||
mixins: [React.addons.LinkedStateMixin],
|
||||
|
||||
propTypes: {
|
||||
mode: React.PropTypes.string
|
||||
mode: React.PropTypes.string,
|
||||
// Callback used to change the selected tab - it is passed the tab name.
|
||||
selectTab: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -115,6 +115,10 @@ loop.conversationViews = (function(mozL10n) {
|
||||
*/
|
||||
var ConversationDetailView = React.createClass({displayName: "ConversationDetailView",
|
||||
propTypes: {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.arrayOf(React.PropTypes.element)
|
||||
]).isRequired,
|
||||
contact: React.PropTypes.object
|
||||
},
|
||||
|
||||
|
@ -115,6 +115,10 @@ loop.conversationViews = (function(mozL10n) {
|
||||
*/
|
||||
var ConversationDetailView = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.arrayOf(React.PropTypes.element)
|
||||
]).isRequired,
|
||||
contact: React.PropTypes.object
|
||||
},
|
||||
|
||||
|
@ -20,6 +20,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
var TabView = React.createClass({displayName: "TabView",
|
||||
propTypes: {
|
||||
buttonsHidden: React.PropTypes.array,
|
||||
children: React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
mozLoop: React.PropTypes.object,
|
||||
// The selectedTab prop is used by the UI showcase.
|
||||
selectedTab: React.PropTypes.string
|
||||
@ -468,6 +469,10 @@ loop.panel = (function(_, mozL10n) {
|
||||
* FxA user identity (guest/authenticated) component.
|
||||
*/
|
||||
var UserIdentity = React.createClass({displayName: "UserIdentity",
|
||||
propTypes: {
|
||||
displayName: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("p", {className: "user-identity"},
|
||||
|
@ -20,6 +20,7 @@ loop.panel = (function(_, mozL10n) {
|
||||
var TabView = React.createClass({
|
||||
propTypes: {
|
||||
buttonsHidden: React.PropTypes.array,
|
||||
children: React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
mozLoop: React.PropTypes.object,
|
||||
// The selectedTab prop is used by the UI showcase.
|
||||
selectedTab: React.PropTypes.string
|
||||
@ -468,6 +469,10 @@ loop.panel = (function(_, mozL10n) {
|
||||
* FxA user identity (guest/authenticated) component.
|
||||
*/
|
||||
var UserIdentity = React.createClass({
|
||||
propTypes: {
|
||||
displayName: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<p className="user-identity">
|
||||
|
@ -278,6 +278,8 @@ loop.roomViews = (function(mozL10n) {
|
||||
mixins: [React.addons.LinkedStateMixin],
|
||||
|
||||
propTypes: {
|
||||
// Only used for tests.
|
||||
availableContext: React.PropTypes.object,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
editMode: React.PropTypes.bool,
|
||||
error: React.PropTypes.object,
|
||||
@ -802,7 +804,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
roomData: roomData,
|
||||
savingContext: this.state.savingContext,
|
||||
show: !shouldRenderInvitationOverlay && shouldRenderContextView}),
|
||||
React.createElement(sharedViews.TextChatView, {
|
||||
React.createElement(sharedViews.chat.TextChatView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
showAlways: false,
|
||||
showRoomName: false})
|
||||
|
@ -278,6 +278,8 @@ loop.roomViews = (function(mozL10n) {
|
||||
mixins: [React.addons.LinkedStateMixin],
|
||||
|
||||
propTypes: {
|
||||
// Only used for tests.
|
||||
availableContext: React.PropTypes.object,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
editMode: React.PropTypes.bool,
|
||||
error: React.PropTypes.object,
|
||||
@ -802,7 +804,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
roomData={roomData}
|
||||
savingContext={this.state.savingContext}
|
||||
show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
|
||||
<sharedViews.TextChatView
|
||||
<sharedViews.chat.TextChatView
|
||||
dispatcher={this.props.dispatcher}
|
||||
showAlways={false}
|
||||
showRoomName={false} />
|
||||
|
@ -1476,7 +1476,6 @@ html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
|
||||
flex: 1 1 auto;
|
||||
max-height: 120px;
|
||||
min-height: 60px;
|
||||
padding: .7em .5em 0;
|
||||
}
|
||||
|
||||
.text-chat-box {
|
||||
@ -1491,24 +1490,65 @@ html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
|
||||
}
|
||||
|
||||
.text-chat-entry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: .5em;
|
||||
text-align: end;
|
||||
margin-bottom: 1.5em;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-content: stretch;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.text-chat-entry > p {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #0095dd;
|
||||
border-radius: 10000px;
|
||||
padding: .5em 1em;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
/* Drop the default margins from the 'p' element. */
|
||||
margin: 0;
|
||||
/* inline-block stops the elements taking 100% of the text-chat-view width */
|
||||
display: inline-block;
|
||||
/* Split really long strings with no spaces appropriately, whilst limiting the
|
||||
width to 100%. */
|
||||
max-width: 100%;
|
||||
padding: .7em;
|
||||
/* leave some room for the chat bubble arrow */
|
||||
max-width: 80%;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #2ea4ff;
|
||||
background: #fff;
|
||||
word-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
flex: 0 1 auto;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
.text-chat-entry.sent > p,
|
||||
.text-chat-entry.received > p {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.text-chat-entry.sent > p {
|
||||
border-radius: 15px;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.text-chat-entry.received > p {
|
||||
border-radius: 15px;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .text-chat-entry.sent > p {
|
||||
border-radius: 15px;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .text-chat-entry.received > p {
|
||||
border-radius: 15px;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.text-chat-entry.received > p {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.text-chat-entry.sent > p {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.text-chat-entry.received {
|
||||
@ -1519,8 +1559,114 @@ html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
|
||||
border-color: #d8d8d8;
|
||||
}
|
||||
|
||||
.text-chat-entry.special > p {
|
||||
border: none;
|
||||
/* Text chat entry timestamp */
|
||||
.text-chat-entry-timestamp {
|
||||
margin: 0 .2em;
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
font-size: .8em;
|
||||
order: 0;
|
||||
flex: 0 1 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Sent text chat entries should be on the right */
|
||||
.text-chat-entry.sent {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.received > .text-chat-entry-timestamp {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.sent > .text-chat-entry-timestamp {
|
||||
order: 0;
|
||||
}
|
||||
|
||||
/* Pseudo element used to cover part between chat bubble and chat arrow. */
|
||||
.text-chat-entry > p:after {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.text-chat-entry.sent > p:after {
|
||||
right: -2px;
|
||||
bottom: 0;
|
||||
width: 15px;
|
||||
height: 9px;
|
||||
border-top-left-radius: 15px;
|
||||
border-top-right-radius: 22px;
|
||||
}
|
||||
|
||||
.text-chat-entry.received > p:after {
|
||||
top: 0;
|
||||
left: -2px;
|
||||
width: 15px;
|
||||
height: 9px;
|
||||
border-bottom-left-radius: 22px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .text-chat-entry.sent > p:after {
|
||||
/* Reset */
|
||||
right: auto;
|
||||
left: -1px;
|
||||
bottom: 0;
|
||||
width: 15px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .text-chat-entry.received > p:after {
|
||||
/* Reset */
|
||||
left: auto;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
width: 15px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
/* Text chat entry arrow */
|
||||
.text-chat-arrow {
|
||||
width: 18px;
|
||||
background-repeat: no-repeat;
|
||||
flex: 0 1 auto;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.text-chat-entry.sent .text-chat-arrow {
|
||||
margin-bottom: -1px;
|
||||
margin-left: -11px;
|
||||
height: 10px;
|
||||
background-image: url("../img/chatbubble-arrow-right.svg");
|
||||
order: 2;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.text-chat-entry.received .text-chat-arrow {
|
||||
margin-left: 0;
|
||||
margin-right: -9px;
|
||||
height: 10px;
|
||||
background-image: url("../img/chatbubble-arrow-left.svg");
|
||||
order: 0;
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .text-chat-arrow {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
html[dir="rtl"] .text-chat-entry.sent .text-chat-arrow {
|
||||
/* Reset margin. */
|
||||
margin-left: 0;
|
||||
margin-right: -11px;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
|
||||
/* Reset margin. */
|
||||
margin-right: 0;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.text-chat-entry.special.room-name {
|
||||
@ -1532,6 +1678,14 @@ html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.text-chat-entry.special.room-name p {
|
||||
background: #E8F6FE;
|
||||
}
|
||||
|
||||
.text-chat-entry.special > p {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.text-chat-box {
|
||||
margin: auto;
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg width="20" height="8" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<title>chatbubble-arrow</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<g transform="rotate(180 6.2844319343566895,3.8364052772521973) " id="svg_1" fill="none">
|
||||
<path id="svg_2" fill="#d8d8d8" d="m12.061934,7.656905l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967001 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.201999,5.664001 8.377999,6.637 8.439999,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352001"/>
|
||||
</g>
|
||||
<line id="svg_13" y2="0.529488" x2="13.851821" y1="0.529488" x1="17.916953" stroke="#d8d8d8" fill="none"/>
|
||||
<line id="svg_26" y2="0.529488" x2="9.79687" y1="0.529488" x1="13.862002" stroke="#d8d8d8" fill="none"/>
|
||||
<line id="svg_27" y2="0.529488" x2="15.908413" y1="0.529488" x1="19.973545" stroke="#d8d8d8" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 969 B |
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg width="20" height="9" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<title>chatbubble-arrow</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<g id="svg_1" fill="none">
|
||||
<path id="svg_2" fill="#2EA4FF" d="m19.505243,8.972466l-9.299002,0l0,-1l6.088001,0c-2.110002,-0.967 -4.742001,-2.818 -6.088001,-6.278l0.932,-0.363c2.202,5.664 8.377999,6.637 8.44,6.646c0.259001,0.039 0.444,0.27 0.426001,0.531c-0.019001,0.262 -0.237,0.464 -0.498999,0.464l-12.072001,-0.352"/>
|
||||
</g>
|
||||
<line id="svg_13" y2="8.474788" x2="6.200791" y1="8.474788" x1="10.265923" stroke="#22a4ff" fill="none"/>
|
||||
<line id="svg_26" y2="8.474788" x2="2.14584" y1="8.474788" x1="6.210972" stroke="#22a4ff" fill="none"/>
|
||||
<line id="svg_27" y2="8.474788" x2="0.000501" y1="8.474788" x1="4.065633" stroke="#22a4ff" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 886 B |
@ -177,7 +177,8 @@ loop.shared.actions = (function() {
|
||||
*/
|
||||
SendTextChatMessage: Action.define("sendTextChatMessage", {
|
||||
contentType: String,
|
||||
message: String
|
||||
message: String,
|
||||
sentTimestamp: String
|
||||
}),
|
||||
|
||||
/**
|
||||
@ -185,7 +186,9 @@ loop.shared.actions = (function() {
|
||||
*/
|
||||
ReceivedTextChatMessage: Action.define("receivedTextChatMessage", {
|
||||
contentType: String,
|
||||
message: String
|
||||
message: String,
|
||||
receivedTimestamp: String
|
||||
// sentTimestamp: String (optional)
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -23,7 +23,7 @@ loop.shared.views.FeedbackView = (function(l10n) {
|
||||
*/
|
||||
var FeedbackLayout = React.createClass({displayName: "FeedbackLayout",
|
||||
propTypes: {
|
||||
children: React.PropTypes.component.isRequired,
|
||||
children: React.PropTypes.element,
|
||||
reset: React.PropTypes.func, // if not specified, no Back btn is shown
|
||||
title: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
@ -23,7 +23,7 @@ loop.shared.views.FeedbackView = (function(l10n) {
|
||||
*/
|
||||
var FeedbackLayout = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.component.isRequired,
|
||||
children: React.PropTypes.element,
|
||||
reset: React.PropTypes.func, // if not specified, no Back btn is shown
|
||||
title: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
@ -605,7 +605,9 @@ loop.shared.mixins = (function() {
|
||||
/**
|
||||
* Starts playing an audio file, stopping any audio that is already in progress.
|
||||
*
|
||||
* @param {String} name The filename to play (excluding the extension).
|
||||
* @param {String} name The filename to play (excluding the extension).
|
||||
* @param {Object} options A list of options for the sound:
|
||||
* - {Boolean} loop Whether or not to loop the sound.
|
||||
*/
|
||||
play: function(name, options) {
|
||||
if (this._isLoopDesktop() && rootObject.navigator.mozLoop.doNotDisturb) {
|
||||
|
@ -698,8 +698,12 @@ loop.OTSdkDriver = (function() {
|
||||
channel.on({
|
||||
message: function(ev) {
|
||||
try {
|
||||
var message = JSON.parse(ev.data);
|
||||
/* Append the timestamp. This is the time that gets shown. */
|
||||
message.receivedTimestamp = (new Date()).toISOString();
|
||||
|
||||
this.dispatcher.dispatch(
|
||||
new sharedActions.ReceivedTextChatMessage(JSON.parse(ev.data)));
|
||||
new sharedActions.ReceivedTextChatMessage(message));
|
||||
} catch (ex) {
|
||||
console.error("Failed to process incoming chat message", ex);
|
||||
}
|
||||
|
@ -96,7 +96,9 @@ loop.store.TextChatStore = (function() {
|
||||
type: type,
|
||||
contentType: messageData.contentType,
|
||||
message: messageData.message,
|
||||
extraData: messageData.extraData
|
||||
extraData: messageData.extraData,
|
||||
sentTimestamp: messageData.sentTimestamp,
|
||||
receivedTimestamp: messageData.receivedTimestamp
|
||||
};
|
||||
var newList = this._storeState.messageList.concat(message);
|
||||
this.setStoreState({ messageList: newList });
|
||||
|
@ -5,8 +5,9 @@
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.views = loop.shared.views || {};
|
||||
loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
loop.shared.views.chat = (function(mozL10n) {
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedViews = loop.shared.views;
|
||||
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
|
||||
var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
|
||||
@ -20,20 +21,44 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
propTypes: {
|
||||
contentType: React.PropTypes.string.isRequired,
|
||||
message: React.PropTypes.string.isRequired,
|
||||
showTimestamp: React.PropTypes.bool.isRequired,
|
||||
timestamp: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
/**
|
||||
* Pretty print timestamp. From time in milliseconds to HH:MM
|
||||
* (or L10N equivalent).
|
||||
*
|
||||
*/
|
||||
_renderTimestamp: function() {
|
||||
var date = new Date(this.props.timestamp);
|
||||
var language = mozL10n.language ? mozL10n.language.code
|
||||
: mozL10n.getLanguage();
|
||||
|
||||
return (
|
||||
React.createElement("span", {className: "text-chat-entry-timestamp"},
|
||||
date.toLocaleTimeString(language,
|
||||
{hour: "numeric", minute: "numeric",
|
||||
hour12: false})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var classes = React.addons.classSet({
|
||||
"text-chat-entry": true,
|
||||
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
"sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
|
||||
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
|
||||
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: classes},
|
||||
React.createElement("p", null, this.props.message)
|
||||
React.createElement("p", null, this.props.message),
|
||||
React.createElement("span", {className: "text-chat-arrow"}),
|
||||
this.props.showTimestamp ? this._renderTimestamp() : null
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -61,13 +86,26 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
* component only updates when the message list is changed.
|
||||
*/
|
||||
var TextChatEntriesView = React.createClass({displayName: "TextChatEntriesView",
|
||||
mixins: [React.addons.PureRenderMixin],
|
||||
mixins: [
|
||||
React.addons.PureRenderMixin,
|
||||
sharedMixins.AudioMixin
|
||||
],
|
||||
|
||||
statics: {
|
||||
ONE_MINUTE: 60
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
messageList: React.PropTypes.array.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
receivedMessageCount: 0
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUpdate: function() {
|
||||
var node = this.getDOMNode();
|
||||
if (!node) {
|
||||
@ -77,6 +115,18 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
var receivedMessageCount = nextProps.messageList.filter(function(message) {
|
||||
return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
|
||||
}).length;
|
||||
|
||||
// If the number of received messages has increased, we play a sound.
|
||||
if (receivedMessageCount > this.state.receivedMessageCount) {
|
||||
this.play("message");
|
||||
this.setState({receivedMessageCount: receivedMessageCount});
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.shouldScroll) {
|
||||
// This ensures the paint is complete.
|
||||
@ -92,6 +142,9 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
/* Keep track of the last printed timestamp. */
|
||||
var lastTimestamp = 0;
|
||||
|
||||
if (!this.props.messageList.length) {
|
||||
return null;
|
||||
}
|
||||
@ -119,23 +172,80 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
)
|
||||
);
|
||||
default:
|
||||
console.error("Unsupported contentType", entry.contentType);
|
||||
console.error("Unsupported contentType",
|
||||
entry.contentType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* For SENT messages there is no received timestamp. */
|
||||
var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
|
||||
|
||||
var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
|
||||
var shouldShowTimestamp = this._shouldShowTimestamp(i,
|
||||
timeDiff);
|
||||
|
||||
if (shouldShowTimestamp) {
|
||||
lastTimestamp = timestamp;
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement(TextChatEntry, {
|
||||
contentType: entry.contentType,
|
||||
key: i,
|
||||
message: entry.message,
|
||||
type: entry.type})
|
||||
);
|
||||
React.createElement(TextChatEntry, {contentType: entry.contentType,
|
||||
key: i,
|
||||
message: entry.message,
|
||||
showTimestamp: shouldShowTimestamp,
|
||||
timestamp: timestamp,
|
||||
type: entry.type})
|
||||
);
|
||||
}, this)
|
||||
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Decide to show timestamp or not on a message.
|
||||
* If the time difference between two consecutive messages is bigger than
|
||||
* one minute or if message types are different.
|
||||
*
|
||||
* @param {number} idx Index of message in the messageList.
|
||||
* @param {boolean} timeDiff If difference between consecutive messages is
|
||||
* bigger than one minute.
|
||||
*/
|
||||
_shouldShowTimestamp: function(idx, timeDiff) {
|
||||
if (!idx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* If consecutive messages are from different senders */
|
||||
if (this.props.messageList[idx].type !==
|
||||
this.props.messageList[idx - 1].type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return timeDiff;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if difference between the two timestamp arguments
|
||||
* is bigger that 60 (1 minute)
|
||||
*
|
||||
* Timestamps are using ISO8601 format.
|
||||
*
|
||||
* @param {string} currTime Timestamp of message yet to be rendered.
|
||||
* @param {string} prevTime Last timestamp printed in the chat view.
|
||||
*/
|
||||
_isOneMinDelta: function(currTime, prevTime) {
|
||||
var date1 = new Date(currTime);
|
||||
var date2 = new Date(prevTime);
|
||||
var delta = date1 - date2;
|
||||
|
||||
if (delta / 1000 >= this.constructor.ONE_MINUTE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -192,7 +302,8 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: this.state.messageDetail
|
||||
message: this.state.messageDetail,
|
||||
sentTimestamp: (new Date()).toISOString()
|
||||
}));
|
||||
|
||||
// Reset the form to empty, ready for the next message.
|
||||
@ -285,5 +396,9 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
return TextChatView;
|
||||
return {
|
||||
TextChatEntriesView: TextChatEntriesView,
|
||||
TextChatEntry: TextChatEntry,
|
||||
TextChatView: TextChatView
|
||||
};
|
||||
})(navigator.mozL10n || document.mozL10n);
|
||||
|
@ -5,8 +5,9 @@
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.views = loop.shared.views || {};
|
||||
loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
loop.shared.views.chat = (function(mozL10n) {
|
||||
var sharedActions = loop.shared.actions;
|
||||
var sharedMixins = loop.shared.mixins;
|
||||
var sharedViews = loop.shared.views;
|
||||
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
|
||||
var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
|
||||
@ -20,13 +21,35 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
propTypes: {
|
||||
contentType: React.PropTypes.string.isRequired,
|
||||
message: React.PropTypes.string.isRequired,
|
||||
showTimestamp: React.PropTypes.bool.isRequired,
|
||||
timestamp: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
/**
|
||||
* Pretty print timestamp. From time in milliseconds to HH:MM
|
||||
* (or L10N equivalent).
|
||||
*
|
||||
*/
|
||||
_renderTimestamp: function() {
|
||||
var date = new Date(this.props.timestamp);
|
||||
var language = mozL10n.language ? mozL10n.language.code
|
||||
: mozL10n.getLanguage();
|
||||
|
||||
return (
|
||||
<span className="text-chat-entry-timestamp">
|
||||
{date.toLocaleTimeString(language,
|
||||
{hour: "numeric", minute: "numeric",
|
||||
hour12: false})}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var classes = React.addons.classSet({
|
||||
"text-chat-entry": true,
|
||||
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
"sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
|
||||
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
|
||||
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
|
||||
});
|
||||
@ -34,6 +57,8 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<p>{this.props.message}</p>
|
||||
<span className="text-chat-arrow" />
|
||||
{this.props.showTimestamp ? this._renderTimestamp() : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -61,13 +86,26 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
* component only updates when the message list is changed.
|
||||
*/
|
||||
var TextChatEntriesView = React.createClass({
|
||||
mixins: [React.addons.PureRenderMixin],
|
||||
mixins: [
|
||||
React.addons.PureRenderMixin,
|
||||
sharedMixins.AudioMixin
|
||||
],
|
||||
|
||||
statics: {
|
||||
ONE_MINUTE: 60
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
messageList: React.PropTypes.array.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
receivedMessageCount: 0
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUpdate: function() {
|
||||
var node = this.getDOMNode();
|
||||
if (!node) {
|
||||
@ -77,6 +115,18 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
var receivedMessageCount = nextProps.messageList.filter(function(message) {
|
||||
return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
|
||||
}).length;
|
||||
|
||||
// If the number of received messages has increased, we play a sound.
|
||||
if (receivedMessageCount > this.state.receivedMessageCount) {
|
||||
this.play("message");
|
||||
this.setState({receivedMessageCount: receivedMessageCount});
|
||||
}
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.shouldScroll) {
|
||||
// This ensures the paint is complete.
|
||||
@ -92,6 +142,9 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
/* Keep track of the last printed timestamp. */
|
||||
var lastTimestamp = 0;
|
||||
|
||||
if (!this.props.messageList.length) {
|
||||
return null;
|
||||
}
|
||||
@ -119,23 +172,80 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
console.error("Unsupported contentType", entry.contentType);
|
||||
console.error("Unsupported contentType",
|
||||
entry.contentType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* For SENT messages there is no received timestamp. */
|
||||
var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
|
||||
|
||||
var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
|
||||
var shouldShowTimestamp = this._shouldShowTimestamp(i,
|
||||
timeDiff);
|
||||
|
||||
if (shouldShowTimestamp) {
|
||||
lastTimestamp = timestamp;
|
||||
}
|
||||
|
||||
return (
|
||||
<TextChatEntry
|
||||
contentType={entry.contentType}
|
||||
key={i}
|
||||
message={entry.message}
|
||||
type={entry.type} />
|
||||
);
|
||||
<TextChatEntry contentType={entry.contentType}
|
||||
key={i}
|
||||
message={entry.message}
|
||||
showTimestamp={shouldShowTimestamp}
|
||||
timestamp={timestamp}
|
||||
type={entry.type} />
|
||||
);
|
||||
}, this)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Decide to show timestamp or not on a message.
|
||||
* If the time difference between two consecutive messages is bigger than
|
||||
* one minute or if message types are different.
|
||||
*
|
||||
* @param {number} idx Index of message in the messageList.
|
||||
* @param {boolean} timeDiff If difference between consecutive messages is
|
||||
* bigger than one minute.
|
||||
*/
|
||||
_shouldShowTimestamp: function(idx, timeDiff) {
|
||||
if (!idx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* If consecutive messages are from different senders */
|
||||
if (this.props.messageList[idx].type !==
|
||||
this.props.messageList[idx - 1].type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return timeDiff;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if difference between the two timestamp arguments
|
||||
* is bigger that 60 (1 minute)
|
||||
*
|
||||
* Timestamps are using ISO8601 format.
|
||||
*
|
||||
* @param {string} currTime Timestamp of message yet to be rendered.
|
||||
* @param {string} prevTime Last timestamp printed in the chat view.
|
||||
*/
|
||||
_isOneMinDelta: function(currTime, prevTime) {
|
||||
var date1 = new Date(currTime);
|
||||
var date2 = new Date(prevTime);
|
||||
var delta = date1 - date2;
|
||||
|
||||
if (delta / 1000 >= this.constructor.ONE_MINUTE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -192,7 +302,8 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
|
||||
this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: this.state.messageDetail
|
||||
message: this.state.messageDetail,
|
||||
sentTimestamp: (new Date()).toISOString()
|
||||
}));
|
||||
|
||||
// Reset the form to empty, ready for the next message.
|
||||
@ -285,5 +396,9 @@ loop.shared.views.TextChatView = (function(mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
return TextChatView;
|
||||
return {
|
||||
TextChatEntriesView: TextChatEntriesView,
|
||||
TextChatEntry: TextChatEntry,
|
||||
TextChatView: TextChatView
|
||||
};
|
||||
})(navigator.mozL10n || document.mozL10n);
|
||||
|
@ -262,6 +262,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
audio: React.PropTypes.object,
|
||||
initiate: React.PropTypes.bool,
|
||||
isDesktop: React.PropTypes.bool,
|
||||
model: React.PropTypes.object.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
video: React.PropTypes.object
|
||||
},
|
||||
@ -557,6 +558,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
propTypes: {
|
||||
additionalClass: React.PropTypes.string,
|
||||
caption: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.element,
|
||||
disabled: React.PropTypes.bool,
|
||||
htmlId: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
@ -589,8 +591,12 @@ loop.shared.views = (function(_, l10n) {
|
||||
});
|
||||
|
||||
var ButtonGroup = React.createClass({displayName: "ButtonGroup",
|
||||
PropTypes: {
|
||||
additionalClass: React.PropTypes.string
|
||||
propTypes: {
|
||||
additionalClass: React.PropTypes.string,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.arrayOf(React.PropTypes.element)
|
||||
])
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -614,7 +620,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
});
|
||||
|
||||
var Checkbox = React.createClass({displayName: "Checkbox",
|
||||
PropTypes: {
|
||||
propTypes: {
|
||||
additionalClass: React.PropTypes.string,
|
||||
checked: React.PropTypes.bool,
|
||||
disabled: React.PropTypes.bool,
|
||||
@ -734,7 +740,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
var ContextUrlView = React.createClass({displayName: "ContextUrlView",
|
||||
mixins: [React.addons.PureRenderMixin],
|
||||
|
||||
PropTypes: {
|
||||
propTypes: {
|
||||
allowClick: React.PropTypes.bool.isRequired,
|
||||
description: React.PropTypes.string.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
|
||||
@ -813,12 +819,12 @@ loop.shared.views = (function(_, l10n) {
|
||||
// to use the pure render mixin here.
|
||||
mixins: [React.addons.PureRenderMixin],
|
||||
|
||||
PropTypes: {
|
||||
propTypes: {
|
||||
displayAvatar: React.PropTypes.bool.isRequired,
|
||||
isLoading: React.PropTypes.bool.isRequired,
|
||||
mediaType: React.PropTypes.string.isRequired,
|
||||
posterUrl: React.PropTypes.string,
|
||||
// Expecting "local" or "remote".
|
||||
mediaType: React.PropTypes.string.isRequired,
|
||||
srcVideoObject: React.PropTypes.object
|
||||
},
|
||||
|
||||
|
@ -262,6 +262,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
audio: React.PropTypes.object,
|
||||
initiate: React.PropTypes.bool,
|
||||
isDesktop: React.PropTypes.bool,
|
||||
model: React.PropTypes.object.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
video: React.PropTypes.object
|
||||
},
|
||||
@ -557,6 +558,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
propTypes: {
|
||||
additionalClass: React.PropTypes.string,
|
||||
caption: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.element,
|
||||
disabled: React.PropTypes.bool,
|
||||
htmlId: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
@ -589,8 +591,12 @@ loop.shared.views = (function(_, l10n) {
|
||||
});
|
||||
|
||||
var ButtonGroup = React.createClass({
|
||||
PropTypes: {
|
||||
additionalClass: React.PropTypes.string
|
||||
propTypes: {
|
||||
additionalClass: React.PropTypes.string,
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.arrayOf(React.PropTypes.element)
|
||||
])
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -614,7 +620,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
});
|
||||
|
||||
var Checkbox = React.createClass({
|
||||
PropTypes: {
|
||||
propTypes: {
|
||||
additionalClass: React.PropTypes.string,
|
||||
checked: React.PropTypes.bool,
|
||||
disabled: React.PropTypes.bool,
|
||||
@ -734,7 +740,7 @@ loop.shared.views = (function(_, l10n) {
|
||||
var ContextUrlView = React.createClass({
|
||||
mixins: [React.addons.PureRenderMixin],
|
||||
|
||||
PropTypes: {
|
||||
propTypes: {
|
||||
allowClick: React.PropTypes.bool.isRequired,
|
||||
description: React.PropTypes.string.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
|
||||
@ -813,12 +819,12 @@ loop.shared.views = (function(_, l10n) {
|
||||
// to use the pure render mixin here.
|
||||
mixins: [React.addons.PureRenderMixin],
|
||||
|
||||
PropTypes: {
|
||||
propTypes: {
|
||||
displayAvatar: React.PropTypes.bool.isRequired,
|
||||
isLoading: React.PropTypes.bool.isRequired,
|
||||
mediaType: React.PropTypes.string.isRequired,
|
||||
posterUrl: React.PropTypes.string,
|
||||
// Expecting "local" or "remote".
|
||||
mediaType: React.PropTypes.string.isRequired,
|
||||
srcVideoObject: React.PropTypes.object
|
||||
},
|
||||
|
||||
|
BIN
browser/components/loop/content/shared/sounds/message.ogg
Normal file
BIN
browser/components/loop/content/shared/sounds/message.ogg
Normal file
Binary file not shown.
@ -40,6 +40,8 @@ browser.jar:
|
||||
# one?
|
||||
content/browser/loop/shared/img/spinner.png (content/shared/img/spinner.png)
|
||||
content/browser/loop/shared/img/spinner@2x.png (content/shared/img/spinner@2x.png)
|
||||
content/browser/loop/shared/img/chatbubble-arrow-left.svg (content/shared/img/chatbubble-arrow-left.svg)
|
||||
content/browser/loop/shared/img/chatbubble-arrow-right.svg (content/shared/img/chatbubble-arrow-right.svg)
|
||||
content/browser/loop/shared/img/audio-inverse-14x14.png (content/shared/img/audio-inverse-14x14.png)
|
||||
content/browser/loop/shared/img/audio-inverse-14x14@2x.png (content/shared/img/audio-inverse-14x14@2x.png)
|
||||
content/browser/loop/shared/img/facemute-14x14.png (content/shared/img/facemute-14x14.png)
|
||||
@ -112,6 +114,7 @@ browser.jar:
|
||||
content/browser/loop/shared/sounds/room-joined-in.ogg (content/shared/sounds/room-joined-in.ogg)
|
||||
content/browser/loop/shared/sounds/room-left.ogg (content/shared/sounds/room-left.ogg)
|
||||
content/browser/loop/shared/sounds/failure.ogg (content/shared/sounds/failure.ogg)
|
||||
content/browser/loop/shared/sounds/message.ogg (content/shared/sounds/message.ogg)
|
||||
|
||||
# Partner SDK assets
|
||||
content/browser/loop/libs/sdk.js (content/shared/libs/sdk.js)
|
||||
|
@ -15,6 +15,11 @@ loop.fxOSMarketplaceViews = (function() {
|
||||
* client.
|
||||
*/
|
||||
var FxOSHiddenMarketplaceView = React.createClass({displayName: "FxOSHiddenMarketplaceView",
|
||||
propTypes: {
|
||||
marketplaceSrc: React.PropTypes.string,
|
||||
onMarketplaceMessage: React.PropTypes.func
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return React.createElement("iframe", {hidden: true, id: "marketplace", src: this.props.marketplaceSrc});
|
||||
},
|
||||
|
@ -15,6 +15,11 @@ loop.fxOSMarketplaceViews = (function() {
|
||||
* client.
|
||||
*/
|
||||
var FxOSHiddenMarketplaceView = React.createClass({
|
||||
propTypes: {
|
||||
marketplaceSrc: React.PropTypes.string,
|
||||
onMarketplaceMessage: React.PropTypes.func
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <iframe hidden id="marketplace" src={this.props.marketplaceSrc} />;
|
||||
},
|
||||
|
@ -20,7 +20,11 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
]).isRequired,
|
||||
isFirefox: React.PropTypes.bool.isRequired
|
||||
failureReason: React.PropTypes.string,
|
||||
isFirefox: React.PropTypes.bool.isRequired,
|
||||
joinRoom: React.PropTypes.func.isRequired,
|
||||
roomState: React.PropTypes.string.isRequired,
|
||||
roomUsed: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
onFeedbackSent: function() {
|
||||
@ -247,6 +251,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
// The poster URLs are for UI-showcase testing and development
|
||||
localPosterUrl: React.PropTypes.string,
|
||||
remotePosterUrl: React.PropTypes.string,
|
||||
roomState: React.PropTypes.string,
|
||||
screenSharePosterUrl: React.PropTypes.string
|
||||
},
|
||||
|
||||
@ -456,7 +461,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
posterUrl: this.props.screenSharePosterUrl,
|
||||
srcVideoObject: this.state.screenShareVideoObject})
|
||||
),
|
||||
React.createElement(sharedViews.TextChatView, {
|
||||
React.createElement(sharedViews.chat.TextChatView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
showAlways: true,
|
||||
showRoomName: true}),
|
||||
|
@ -20,7 +20,11 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
]).isRequired,
|
||||
isFirefox: React.PropTypes.bool.isRequired
|
||||
failureReason: React.PropTypes.string,
|
||||
isFirefox: React.PropTypes.bool.isRequired,
|
||||
joinRoom: React.PropTypes.func.isRequired,
|
||||
roomState: React.PropTypes.string.isRequired,
|
||||
roomUsed: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
onFeedbackSent: function() {
|
||||
@ -247,6 +251,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
// The poster URLs are for UI-showcase testing and development
|
||||
localPosterUrl: React.PropTypes.string,
|
||||
remotePosterUrl: React.PropTypes.string,
|
||||
roomState: React.PropTypes.string,
|
||||
screenSharePosterUrl: React.PropTypes.string
|
||||
},
|
||||
|
||||
@ -456,7 +461,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
posterUrl={this.props.screenSharePosterUrl}
|
||||
srcVideoObject={this.state.screenShareVideoObject} />
|
||||
</div>
|
||||
<sharedViews.TextChatView
|
||||
<sharedViews.chat.TextChatView
|
||||
dispatcher={this.props.dispatcher}
|
||||
showAlways={true}
|
||||
showRoomName={true} />
|
||||
|
@ -212,6 +212,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
});
|
||||
|
||||
var ConversationHeader = React.createClass({displayName: "ConversationHeader",
|
||||
propTypes: {
|
||||
urlCreationDateString: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
var conversationUrl = location.href;
|
||||
|
@ -212,6 +212,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
});
|
||||
|
||||
var ConversationHeader = React.createClass({
|
||||
propTypes: {
|
||||
urlCreationDateString: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
var conversationUrl = location.href;
|
||||
|
@ -1341,6 +1341,9 @@ describe("loop.OTSdkDriver", function () {
|
||||
|
||||
it("should dispatch `ReceivedTextChatMessage` when a text message is received", function() {
|
||||
var fakeChannel = _.extend({}, Backbone.Events);
|
||||
var data = '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT +
|
||||
'","message":"Are you there?","receivedTimestamp": "2015-06-25T00:29:14.197Z"}';
|
||||
var clock = sinon.useFakeTimers();
|
||||
|
||||
subscriber._.getDataChannel.callsArgWith(2, null, fakeChannel);
|
||||
|
||||
@ -1348,15 +1351,19 @@ describe("loop.OTSdkDriver", function () {
|
||||
|
||||
// Now send the message.
|
||||
fakeChannel.trigger("message", {
|
||||
data: '{"contentType":"' + CHAT_CONTENT_TYPES.TEXT + '","message":"Are you there?"}'
|
||||
data: data
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Are you there?"
|
||||
message: "Are you there?",
|
||||
receivedTimestamp: "1970-01-01T00:00:00.000Z"
|
||||
}));
|
||||
|
||||
/* Restore the time. */
|
||||
clock.restore();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -70,14 +70,19 @@ describe("loop.store.TextChatStore", function () {
|
||||
|
||||
store.receivedTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: message
|
||||
message: message,
|
||||
extraData: undefined,
|
||||
sentTimestamp: "2015-06-24T23:58:53.848Z",
|
||||
receivedTimestamp: "1970-01-01T00:00:00.000Z"
|
||||
});
|
||||
|
||||
expect(store.getStoreState("messageList")).eql([{
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: message,
|
||||
extraData: undefined
|
||||
extraData: undefined,
|
||||
sentTimestamp: "2015-06-24T23:58:53.848Z",
|
||||
receivedTimestamp: "1970-01-01T00:00:00.000Z"
|
||||
}]);
|
||||
});
|
||||
|
||||
@ -118,7 +123,9 @@ describe("loop.store.TextChatStore", function () {
|
||||
it("should add the message to the list", function() {
|
||||
var messageData = {
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "It's awesome!"
|
||||
message: "It's awesome!",
|
||||
sentTimestamp: "2015-06-24T23:58:53.848Z",
|
||||
receivedTimestamp: "2015-06-24T23:58:53.848Z"
|
||||
};
|
||||
|
||||
store.sendTextChatMessage(messageData);
|
||||
@ -127,7 +134,9 @@ describe("loop.store.TextChatStore", function () {
|
||||
type: CHAT_MESSAGE_TYPES.SENT,
|
||||
contentType: messageData.contentType,
|
||||
message: messageData.message,
|
||||
extraData: undefined
|
||||
extraData: undefined,
|
||||
sentTimestamp: "2015-06-24T23:58:53.848Z",
|
||||
receivedTimestamp: "2015-06-24T23:58:53.848Z"
|
||||
}]);
|
||||
});
|
||||
|
||||
@ -155,7 +164,9 @@ describe("loop.store.TextChatStore", function () {
|
||||
type: CHAT_MESSAGE_TYPES.SPECIAL,
|
||||
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
|
||||
message: "Let's share!",
|
||||
extraData: undefined
|
||||
extraData: undefined,
|
||||
sentTimestamp: undefined,
|
||||
receivedTimestamp: undefined
|
||||
}]);
|
||||
});
|
||||
|
||||
@ -176,11 +187,15 @@ describe("loop.store.TextChatStore", function () {
|
||||
type: CHAT_MESSAGE_TYPES.SPECIAL,
|
||||
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
|
||||
message: "Let's share!",
|
||||
extraData: undefined
|
||||
extraData: undefined,
|
||||
sentTimestamp: undefined,
|
||||
receivedTimestamp: undefined
|
||||
}, {
|
||||
type: CHAT_MESSAGE_TYPES.SPECIAL,
|
||||
contentType: CHAT_CONTENT_TYPES.CONTEXT,
|
||||
message: "A wonderful event",
|
||||
sentTimestamp: undefined,
|
||||
receivedTimestamp: undefined,
|
||||
extraData: {
|
||||
location: "http://wonderful.invalid",
|
||||
thumbnail: "fake"
|
||||
|
@ -11,11 +11,11 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
|
||||
var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
|
||||
|
||||
var dispatcher, fakeSdkDriver, sandbox, store;
|
||||
var dispatcher, fakeSdkDriver, sandbox, store, fakeClock;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox.useFakeTimers();
|
||||
fakeClock = sandbox.useFakeTimers();
|
||||
|
||||
dispatcher = new loop.Dispatcher();
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
@ -37,6 +37,213 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("TextChatEntriesView", function() {
|
||||
var view;
|
||||
|
||||
function mountTestComponent(extraProps) {
|
||||
var basicProps = {
|
||||
dispatcher: dispatcher,
|
||||
messageList: []
|
||||
};
|
||||
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.shared.views.chat.TextChatEntriesView,
|
||||
_.extend(basicProps, extraProps)));
|
||||
}
|
||||
|
||||
it("should render message entries when message were sent/ received", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!"
|
||||
}, {
|
||||
type: CHAT_MESSAGE_TYPES.SENT,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Is it me you're looking for?"
|
||||
}]
|
||||
});
|
||||
|
||||
var node = view.getDOMNode();
|
||||
expect(node).to.not.eql(null);
|
||||
|
||||
var entries = node.querySelectorAll(".text-chat-entry");
|
||||
expect(entries.length).to.eql(2);
|
||||
expect(entries[0].classList.contains("received")).to.eql(true);
|
||||
expect(entries[1].classList.contains("received")).to.not.eql(true);
|
||||
});
|
||||
|
||||
it("should play a sound when a message is received", function() {
|
||||
view = mountTestComponent();
|
||||
sandbox.stub(view, "play");
|
||||
|
||||
view.setProps({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!"
|
||||
}]
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(view.play);
|
||||
sinon.assert.calledWithExactly(view.play, "message");
|
||||
});
|
||||
|
||||
it("should not play a sound when a special message is displayed", function() {
|
||||
view = mountTestComponent();
|
||||
sandbox.stub(view, "play");
|
||||
|
||||
view.setProps({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.SPECIAL,
|
||||
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
|
||||
message: "Hello!"
|
||||
}]
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(view.play);
|
||||
});
|
||||
|
||||
it("should not play a sound when a message is sent", function() {
|
||||
view = mountTestComponent();
|
||||
sandbox.stub(view, "play");
|
||||
|
||||
view.setProps({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.SENT,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!"
|
||||
}]
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(view.play);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextChatEntry", function() {
|
||||
var view;
|
||||
|
||||
function mountTestComponent(extraProps) {
|
||||
var props = _.extend({
|
||||
dispatcher: dispatcher
|
||||
}, extraProps);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.shared.views.chat.TextChatEntry, props));
|
||||
}
|
||||
|
||||
it("should not render a timestamp", function() {
|
||||
view = mountTestComponent({
|
||||
showTimestamp: false,
|
||||
timestamp: "2015-06-23T22:48:39.738Z"
|
||||
});
|
||||
var node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelector(".text-chat-entry-timestamp")).to.eql(null);
|
||||
});
|
||||
|
||||
it("should render a timestamp", function() {
|
||||
view = mountTestComponent({
|
||||
showTimestamp: true,
|
||||
timestamp: "2015-06-23T22:48:39.738Z"
|
||||
});
|
||||
var node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelector(".text-chat-entry-timestamp")).to.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextChatEntriesView", function() {
|
||||
var view, node;
|
||||
|
||||
function mountTestComponent(extraProps) {
|
||||
var props = _.extend({
|
||||
dispatcher: dispatcher
|
||||
}, extraProps);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.shared.views.chat.TextChatEntriesView, props));
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
store.setStoreState({ textChatEnabled: true });
|
||||
});
|
||||
|
||||
it("should show timestamps if there are different senders", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!"
|
||||
}, {
|
||||
type: CHAT_MESSAGE_TYPES.SENT,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Is it me you're looking for?"
|
||||
}]
|
||||
});
|
||||
node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
|
||||
.to.eql(2);
|
||||
});
|
||||
|
||||
it("should show timestamps if they are 1 minute apart (SENT)", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.SENT,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!",
|
||||
sentTimestamp: "2015-06-25T17:53:55.357Z"
|
||||
}, {
|
||||
type: CHAT_MESSAGE_TYPES.SENT,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Is it me you're looking for?",
|
||||
sentTimestamp: "2015-06-25T17:54:55.357Z"
|
||||
}]
|
||||
});
|
||||
node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
|
||||
.to.eql(2);
|
||||
});
|
||||
|
||||
it("should show timestamps if they are 1 minute apart (RECV)", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!",
|
||||
receivedTimestamp: "2015-06-25T17:53:55.357Z"
|
||||
}, {
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Is it me you're looking for?",
|
||||
receivedTimestamp: "2015-06-25T17:54:55.357Z"
|
||||
}]
|
||||
});
|
||||
node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
|
||||
.to.eql(2);
|
||||
});
|
||||
|
||||
it("should not show timestamps from msgs sent in the same minute", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!"
|
||||
}, {
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Is it me you're looking for?"
|
||||
}]
|
||||
});
|
||||
node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
|
||||
.to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TextChatView", function() {
|
||||
var view;
|
||||
|
||||
@ -45,13 +252,42 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
dispatcher: dispatcher
|
||||
}, extraProps);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.shared.views.TextChatView, props));
|
||||
React.createElement(loop.shared.views.chat.TextChatView, props));
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
store.setStoreState({ textChatEnabled: true });
|
||||
});
|
||||
|
||||
it("should show timestamps from msgs sent more than 1 min apart", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
store.sendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!",
|
||||
sentTimestamp: "1970-01-01T00:02:00.000Z",
|
||||
receivedTimestamp: "1970-01-01T00:02:00.000Z"
|
||||
});
|
||||
|
||||
store.sendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Is it me you're looking for?",
|
||||
sentTimestamp: "1970-01-01T00:03:00.000Z",
|
||||
receivedTimestamp: "1970-01-01T00:03:00.000Z"
|
||||
});
|
||||
store.sendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Is it me you're looking for?",
|
||||
sentTimestamp: "1970-01-01T00:02:00.000Z",
|
||||
receivedTimestamp: "1970-01-01T00:02:00.000Z"
|
||||
});
|
||||
|
||||
var node = view.getDOMNode();
|
||||
|
||||
expect(node.querySelectorAll(".text-chat-entry-timestamp").length)
|
||||
.to.eql(2);
|
||||
});
|
||||
|
||||
it("should not display anything if no messages and text chat not enabled and showAlways is false", function() {
|
||||
store.setStoreState({ textChatEnabled: false });
|
||||
|
||||
@ -110,6 +346,31 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
expect(entries[1].classList.contains("received")).to.not.eql(true);
|
||||
});
|
||||
|
||||
it("should add `sent` CSS class selector to msg of type SENT", function() {
|
||||
var node = mountTestComponent().getDOMNode();
|
||||
|
||||
store.sendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Foo",
|
||||
timestamp: 0
|
||||
});
|
||||
|
||||
expect(node.querySelector(".sent")).to.not.eql(null);
|
||||
});
|
||||
|
||||
it("should add `received` CSS class selector to msg of type RECEIVED",
|
||||
function() {
|
||||
var node = mountTestComponent().getDOMNode();
|
||||
|
||||
store.receivedTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Foo",
|
||||
timestamp: 0
|
||||
});
|
||||
|
||||
expect(node.querySelector(".received")).to.not.eql(null);
|
||||
});
|
||||
|
||||
it("should render a room name special entry", function() {
|
||||
view = mountTestComponent({
|
||||
showRoomName: true
|
||||
@ -171,7 +432,8 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.SendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!"
|
||||
message: "Hello!",
|
||||
sentTimestamp: "1970-01-01T00:00:00.000Z"
|
||||
}));
|
||||
});
|
||||
|
||||
|
@ -22,5 +22,10 @@ navigator.mozL10n = document.mozL10n = {
|
||||
}).replace(/_/g, " "); // and convert _ chars to spaces
|
||||
|
||||
return "" + readableStringId + (vars ? ";" + JSON.stringify(vars) : "");
|
||||
},
|
||||
|
||||
/* For timestamp formatting reasons. */
|
||||
language: {
|
||||
code: "en-US"
|
||||
}
|
||||
};
|
||||
|
@ -34,7 +34,7 @@
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
var Checkbox = loop.shared.views.Checkbox;
|
||||
var TextChatView = loop.shared.views.TextChatView;
|
||||
var TextChatView = loop.shared.views.chat.TextChatView;
|
||||
|
||||
// Store constants
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
@ -87,7 +87,7 @@
|
||||
var mockSDK = _.extend({
|
||||
sendTextChatMessage: function(message) {
|
||||
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
|
||||
message: message
|
||||
message: message.message
|
||||
}));
|
||||
}
|
||||
}, Backbone.Events);
|
||||
@ -309,30 +309,46 @@
|
||||
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Rheet!"
|
||||
message: "Rheet!",
|
||||
sentTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hi there"
|
||||
message: "Hi there",
|
||||
receivedTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello",
|
||||
receivedTimestamp: "2015-06-23T23:24:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Check out this menu from DNA Pizza:" +
|
||||
" http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
|
||||
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%"
|
||||
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
|
||||
"linewrappingissuesifthecssiswrong"
|
||||
"linewrappingissuesifthecssiswrong",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "That avocado monkey-brains pie sounds tasty!"
|
||||
message: "That avocado monkey-brains pie sounds tasty!",
|
||||
receivedTimestamp: "2015-06-23T22:25:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "What time should we meet?"
|
||||
message: "What time should we meet?",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Cool",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
@ -381,6 +397,11 @@
|
||||
});
|
||||
|
||||
var SVGIcon = React.createClass({displayName: "SVGIcon",
|
||||
propTypes: {
|
||||
shapeId: React.PropTypes.string.isRequired,
|
||||
size: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var sizeUnit = this.props.size.split("x");
|
||||
return (
|
||||
@ -393,6 +414,10 @@
|
||||
});
|
||||
|
||||
var SVGIcons = React.createClass({displayName: "SVGIcons",
|
||||
propTypes: {
|
||||
size: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
shapes: {
|
||||
"10x10": ["close", "close-active", "close-disabled", "dropdown",
|
||||
"dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
|
||||
@ -435,10 +460,12 @@
|
||||
|
||||
var FramedExample = React.createClass({displayName: "FramedExample",
|
||||
propTypes: {
|
||||
children: React.PropTypes.element,
|
||||
cssClass: React.PropTypes.string,
|
||||
dashed: React.PropTypes.bool,
|
||||
height: React.PropTypes.number,
|
||||
onContentsRendered: React.PropTypes.func,
|
||||
summary: React.PropTypes.string.isRequired,
|
||||
width: React.PropTypes.number
|
||||
},
|
||||
|
||||
@ -478,6 +505,16 @@
|
||||
});
|
||||
|
||||
var Example = React.createClass({displayName: "Example",
|
||||
propTypes: {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.arrayOf(React.PropTypes.element)
|
||||
]).isRequired,
|
||||
dashed: React.PropTypes.bool,
|
||||
style: React.PropTypes.object,
|
||||
summary: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
makeId: function(prefix) {
|
||||
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
|
||||
},
|
||||
@ -500,6 +537,15 @@
|
||||
});
|
||||
|
||||
var Section = React.createClass({displayName: "Section",
|
||||
propTypes: {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
]).isRequired,
|
||||
className: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("section", {className: this.props.className, id: this.props.name},
|
||||
@ -511,6 +557,10 @@
|
||||
});
|
||||
|
||||
var ShowCase = React.createClass({displayName: "ShowCase",
|
||||
propTypes: {
|
||||
children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
// We assume for now that rtl is the only query parameter.
|
||||
//
|
||||
@ -572,10 +622,10 @@
|
||||
React.createElement("p", {className: "note"},
|
||||
React.createElement("strong", null, "Note:"), " 332px wide."
|
||||
),
|
||||
React.createElement(Example, {dashed: "true", style: {width: "332px"}, summary: "Re-sign-in view"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Re-sign-in view"},
|
||||
React.createElement(SignInRequestView, {mozLoop: mockMozLoopRooms})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true", style: {width: "332px"}, summary: "Room list tab"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Room list tab"},
|
||||
React.createElement(PanelView, {client: mockClient,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: mockMozLoopRooms,
|
||||
@ -584,7 +634,7 @@
|
||||
selectedTab: "rooms",
|
||||
userProfile: {email: "test@example.com"}})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true", style: {width: "332px"}, summary: "Contact list tab"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Contact list tab"},
|
||||
React.createElement(PanelView, {client: mockClient,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: mockMozLoopRooms,
|
||||
@ -593,14 +643,14 @@
|
||||
selectedTab: "contacts",
|
||||
userProfile: {email: "test@example.com"}})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true", style: {width: "332px"}, summary: "Error Notification"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Error Notification"},
|
||||
React.createElement(PanelView, {client: mockClient,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop,
|
||||
notifications: errNotifications,
|
||||
roomStore: roomStore})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true", style: {width: "332px"}, summary: "Error Notification - authenticated"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Error Notification - authenticated"},
|
||||
React.createElement(PanelView, {client: mockClient,
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: navigator.mozLoop,
|
||||
@ -608,7 +658,7 @@
|
||||
roomStore: roomStore,
|
||||
userProfile: {email: "test@example.com"}})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true", style: {width: "332px"}, summary: "Contact import success"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Contact import success"},
|
||||
React.createElement(PanelView, {dispatcher: dispatcher,
|
||||
mozLoop: mockMozLoopRooms,
|
||||
notifications: new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}]),
|
||||
@ -616,7 +666,7 @@
|
||||
selectedTab: "contacts",
|
||||
userProfile: {email: "test@example.com"}})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true", style: {width: "332px"}, summary: "Contact import error"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "332px"}, summary: "Contact import error"},
|
||||
React.createElement(PanelView, {dispatcher: dispatcher,
|
||||
mozLoop: mockMozLoopRooms,
|
||||
notifications: new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}]),
|
||||
@ -627,7 +677,7 @@
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "AcceptCallView"},
|
||||
React.createElement(Example, {dashed: "true", style: {width: "300px", height: "272px"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"},
|
||||
summary: "Default / incoming video call"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO,
|
||||
@ -637,7 +687,7 @@
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Example, {dashed: "true", style: {width: "300px", height: "272px"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"},
|
||||
summary: "Default / incoming audio only call"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_ONLY,
|
||||
@ -649,7 +699,7 @@
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "AcceptCallView-ActiveState"},
|
||||
React.createElement(Example, {dashed: "true", style: {width: "300px", height: "272px"},
|
||||
React.createElement(Example, {dashed: true, style: {width: "300px", height: "272px"},
|
||||
summary: "Default"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO,
|
||||
@ -708,7 +758,7 @@
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "PendingConversationView (Desktop)"},
|
||||
React.createElement(Example, {dashed: "true",
|
||||
React.createElement(Example, {dashed: true,
|
||||
style: {width: "300px", height: "272px"},
|
||||
summary: "Connecting"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
@ -720,7 +770,7 @@
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "CallFailedView"},
|
||||
React.createElement(Example, {dashed: "true",
|
||||
React.createElement(Example, {dashed: true,
|
||||
style: {width: "300px", height: "272px"},
|
||||
summary: "Call Failed - Incoming"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
@ -729,7 +779,7 @@
|
||||
store: conversationStore})
|
||||
)
|
||||
),
|
||||
React.createElement(Example, {dashed: "true",
|
||||
React.createElement(Example, {dashed: true,
|
||||
style: {width: "300px", height: "272px"},
|
||||
summary: "Call Failed - Outgoing"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
@ -738,7 +788,7 @@
|
||||
store: conversationStore})
|
||||
)
|
||||
),
|
||||
React.createElement(Example, {dashed: "true",
|
||||
React.createElement(Example, {dashed: true,
|
||||
style: {width: "300px", height: "272px"},
|
||||
summary: "Call Failed — with call URL error"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
@ -815,17 +865,17 @@
|
||||
React.createElement("strong", null, "Note:"), " For the useable demo, you can access submitted data at ",
|
||||
React.createElement("a", {href: "https://input.allizom.org/"}, "input.allizom.org"), "."
|
||||
),
|
||||
React.createElement(Example, {dashed: "true",
|
||||
React.createElement(Example, {dashed: true,
|
||||
style: {width: "300px", height: "272px"},
|
||||
summary: "Default (useable demo)"},
|
||||
React.createElement(FeedbackView, {feedbackStore: feedbackStore})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true",
|
||||
React.createElement(Example, {dashed: true,
|
||||
style: {width: "300px", height: "272px"},
|
||||
summary: "Detailed form"},
|
||||
React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.DETAILS, feedbackStore: feedbackStore})
|
||||
),
|
||||
React.createElement(Example, {dashed: "true",
|
||||
React.createElement(Example, {dashed: true,
|
||||
style: {width: "300px", height: "272px"},
|
||||
summary: "Thank you!"},
|
||||
React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.SENT, feedbackStore: feedbackStore})
|
||||
|
@ -34,7 +34,7 @@
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
var Checkbox = loop.shared.views.Checkbox;
|
||||
var TextChatView = loop.shared.views.TextChatView;
|
||||
var TextChatView = loop.shared.views.chat.TextChatView;
|
||||
|
||||
// Store constants
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
@ -87,7 +87,7 @@
|
||||
var mockSDK = _.extend({
|
||||
sendTextChatMessage: function(message) {
|
||||
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
|
||||
message: message
|
||||
message: message.message
|
||||
}));
|
||||
}
|
||||
}, Backbone.Events);
|
||||
@ -309,30 +309,46 @@
|
||||
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Rheet!"
|
||||
message: "Rheet!",
|
||||
sentTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hi there"
|
||||
message: "Hi there",
|
||||
receivedTimestamp: "2015-06-23T22:21:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello",
|
||||
receivedTimestamp: "2015-06-23T23:24:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Check out this menu from DNA Pizza:" +
|
||||
" http://example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/" +
|
||||
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%"
|
||||
"%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Nowforareallylongwordwithoutspacesorpunctuationwhichshouldcause" +
|
||||
"linewrappingissuesifthecssiswrong"
|
||||
"linewrappingissuesifthecssiswrong",
|
||||
sentTimestamp: "2015-06-23T22:23:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.ReceivedTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "That avocado monkey-brains pie sounds tasty!"
|
||||
message: "That avocado monkey-brains pie sounds tasty!",
|
||||
receivedTimestamp: "2015-06-23T22:25:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "What time should we meet?"
|
||||
message: "What time should we meet?",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
||||
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Cool",
|
||||
sentTimestamp: "2015-06-23T22:27:45.590Z"
|
||||
}));
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
@ -381,6 +397,11 @@
|
||||
});
|
||||
|
||||
var SVGIcon = React.createClass({
|
||||
propTypes: {
|
||||
shapeId: React.PropTypes.string.isRequired,
|
||||
size: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var sizeUnit = this.props.size.split("x");
|
||||
return (
|
||||
@ -393,6 +414,10 @@
|
||||
});
|
||||
|
||||
var SVGIcons = React.createClass({
|
||||
propTypes: {
|
||||
size: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
shapes: {
|
||||
"10x10": ["close", "close-active", "close-disabled", "dropdown",
|
||||
"dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
|
||||
@ -435,10 +460,12 @@
|
||||
|
||||
var FramedExample = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.element,
|
||||
cssClass: React.PropTypes.string,
|
||||
dashed: React.PropTypes.bool,
|
||||
height: React.PropTypes.number,
|
||||
onContentsRendered: React.PropTypes.func,
|
||||
summary: React.PropTypes.string.isRequired,
|
||||
width: React.PropTypes.number
|
||||
},
|
||||
|
||||
@ -478,6 +505,16 @@
|
||||
});
|
||||
|
||||
var Example = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.arrayOf(React.PropTypes.element)
|
||||
]).isRequired,
|
||||
dashed: React.PropTypes.bool,
|
||||
style: React.PropTypes.object,
|
||||
summary: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
makeId: function(prefix) {
|
||||
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
|
||||
},
|
||||
@ -500,6 +537,15 @@
|
||||
});
|
||||
|
||||
var Section = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
React.PropTypes.element
|
||||
]).isRequired,
|
||||
className: React.PropTypes.string,
|
||||
name: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<section className={this.props.className} id={this.props.name}>
|
||||
@ -511,6 +557,10 @@
|
||||
});
|
||||
|
||||
var ShowCase = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
// We assume for now that rtl is the only query parameter.
|
||||
//
|
||||
@ -572,10 +622,10 @@
|
||||
<p className="note">
|
||||
<strong>Note:</strong> 332px wide.
|
||||
</p>
|
||||
<Example dashed="true" style={{width: "332px"}} summary="Re-sign-in view">
|
||||
<Example dashed={true} style={{width: "332px"}} summary="Re-sign-in view">
|
||||
<SignInRequestView mozLoop={mockMozLoopRooms} />
|
||||
</Example>
|
||||
<Example dashed="true" style={{width: "332px"}} summary="Room list tab">
|
||||
<Example dashed={true} style={{width: "332px"}} summary="Room list tab">
|
||||
<PanelView client={mockClient}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
@ -584,7 +634,7 @@
|
||||
selectedTab="rooms"
|
||||
userProfile={{email: "test@example.com"}} />
|
||||
</Example>
|
||||
<Example dashed="true" style={{width: "332px"}} summary="Contact list tab">
|
||||
<Example dashed={true} style={{width: "332px"}} summary="Contact list tab">
|
||||
<PanelView client={mockClient}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
@ -593,14 +643,14 @@
|
||||
selectedTab="contacts"
|
||||
userProfile={{email: "test@example.com"}} />
|
||||
</Example>
|
||||
<Example dashed="true" style={{width: "332px"}} summary="Error Notification">
|
||||
<Example dashed={true} style={{width: "332px"}} summary="Error Notification">
|
||||
<PanelView client={mockClient}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={navigator.mozLoop}
|
||||
notifications={errNotifications}
|
||||
roomStore={roomStore} />
|
||||
</Example>
|
||||
<Example dashed="true" style={{width: "332px"}} summary="Error Notification - authenticated">
|
||||
<Example dashed={true} style={{width: "332px"}} summary="Error Notification - authenticated">
|
||||
<PanelView client={mockClient}
|
||||
dispatcher={dispatcher}
|
||||
mozLoop={navigator.mozLoop}
|
||||
@ -608,7 +658,7 @@
|
||||
roomStore={roomStore}
|
||||
userProfile={{email: "test@example.com"}} />
|
||||
</Example>
|
||||
<Example dashed="true" style={{width: "332px"}} summary="Contact import success">
|
||||
<Example dashed={true} style={{width: "332px"}} summary="Contact import success">
|
||||
<PanelView dispatcher={dispatcher}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
notifications={new loop.shared.models.NotificationCollection([{level: "success", message: "Import success"}])}
|
||||
@ -616,7 +666,7 @@
|
||||
selectedTab="contacts"
|
||||
userProfile={{email: "test@example.com"}} />
|
||||
</Example>
|
||||
<Example dashed="true" style={{width: "332px"}} summary="Contact import error">
|
||||
<Example dashed={true} style={{width: "332px"}} summary="Contact import error">
|
||||
<PanelView dispatcher={dispatcher}
|
||||
mozLoop={mockMozLoopRooms}
|
||||
notifications={new loop.shared.models.NotificationCollection([{level: "error", message: "Import error"}])}
|
||||
@ -627,7 +677,7 @@
|
||||
</Section>
|
||||
|
||||
<Section name="AcceptCallView">
|
||||
<Example dashed="true" style={{width: "300px", height: "272px"}}
|
||||
<Example dashed={true} style={{width: "300px", height: "272px"}}
|
||||
summary="Default / incoming video call">
|
||||
<div className="fx-embedded">
|
||||
<AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
|
||||
@ -637,7 +687,7 @@
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example dashed="true" style={{width: "300px", height: "272px"}}
|
||||
<Example dashed={true} style={{width: "300px", height: "272px"}}
|
||||
summary="Default / incoming audio only call">
|
||||
<div className="fx-embedded">
|
||||
<AcceptCallView callType={CALL_TYPES.AUDIO_ONLY}
|
||||
@ -649,7 +699,7 @@
|
||||
</Section>
|
||||
|
||||
<Section name="AcceptCallView-ActiveState">
|
||||
<Example dashed="true" style={{width: "300px", height: "272px"}}
|
||||
<Example dashed={true} style={{width: "300px", height: "272px"}}
|
||||
summary="Default">
|
||||
<div className="fx-embedded" >
|
||||
<AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
|
||||
@ -708,7 +758,7 @@
|
||||
</Section>
|
||||
|
||||
<Section name="PendingConversationView (Desktop)">
|
||||
<Example dashed="true"
|
||||
<Example dashed={true}
|
||||
style={{width: "300px", height: "272px"}}
|
||||
summary="Connecting">
|
||||
<div className="fx-embedded">
|
||||
@ -720,7 +770,7 @@
|
||||
</Section>
|
||||
|
||||
<Section name="CallFailedView">
|
||||
<Example dashed="true"
|
||||
<Example dashed={true}
|
||||
style={{width: "300px", height: "272px"}}
|
||||
summary="Call Failed - Incoming">
|
||||
<div className="fx-embedded">
|
||||
@ -729,7 +779,7 @@
|
||||
store={conversationStore} />
|
||||
</div>
|
||||
</Example>
|
||||
<Example dashed="true"
|
||||
<Example dashed={true}
|
||||
style={{width: "300px", height: "272px"}}
|
||||
summary="Call Failed - Outgoing">
|
||||
<div className="fx-embedded">
|
||||
@ -738,7 +788,7 @@
|
||||
store={conversationStore} />
|
||||
</div>
|
||||
</Example>
|
||||
<Example dashed="true"
|
||||
<Example dashed={true}
|
||||
style={{width: "300px", height: "272px"}}
|
||||
summary="Call Failed — with call URL error">
|
||||
<div className="fx-embedded">
|
||||
@ -815,17 +865,17 @@
|
||||
<strong>Note:</strong> For the useable demo, you can access submitted data at
|
||||
<a href="https://input.allizom.org/">input.allizom.org</a>.
|
||||
</p>
|
||||
<Example dashed="true"
|
||||
<Example dashed={true}
|
||||
style={{width: "300px", height: "272px"}}
|
||||
summary="Default (useable demo)">
|
||||
<FeedbackView feedbackStore={feedbackStore} />
|
||||
</Example>
|
||||
<Example dashed="true"
|
||||
<Example dashed={true}
|
||||
style={{width: "300px", height: "272px"}}
|
||||
summary="Detailed form">
|
||||
<FeedbackView feedbackState={FEEDBACK_STATES.DETAILS} feedbackStore={feedbackStore} />
|
||||
</Example>
|
||||
<Example dashed="true"
|
||||
<Example dashed={true}
|
||||
style={{width: "300px", height: "272px"}}
|
||||
summary="Thank you!">
|
||||
<FeedbackView feedbackState={FEEDBACK_STATES.SENT} feedbackStore={feedbackStore}/>
|
||||
|
@ -2,6 +2,8 @@
|
||||
* 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/. */
|
||||
|
||||
Components.utils.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
var gPrivacyPane = {
|
||||
|
||||
/**
|
||||
@ -103,6 +105,8 @@ var gPrivacyPane = {
|
||||
gPrivacyPane.showCookies);
|
||||
setEventListener("clearDataSettings", "command",
|
||||
gPrivacyPane.showClearPrivateDataSettings);
|
||||
|
||||
document.getElementById("searchesSuggestion").hidden = !AppConstants.NIGHTLY_BUILD;
|
||||
},
|
||||
|
||||
// HISTORY MODE
|
||||
|
@ -258,6 +258,7 @@
|
||||
accesskey="&locbar.openpage.accesskey;"
|
||||
preference="browser.urlbar.suggest.openpage"/>
|
||||
<checkbox id="searchesSuggestion" label="&locbar.searches.label;"
|
||||
hidden="true"
|
||||
onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
|
||||
accesskey="&locbar.searches.accesskey;"
|
||||
preference="browser.urlbar.suggest.searches"/>
|
||||
|
@ -129,6 +129,25 @@ let gSyncPane = {
|
||||
this._initProfileImageUI();
|
||||
},
|
||||
|
||||
_toggleComputerNameControls: function(editMode) {
|
||||
let textbox = document.getElementById("fxaSyncComputerName");
|
||||
textbox.className = editMode ? "" : "plain";
|
||||
textbox.disabled = !editMode;
|
||||
document.getElementById("fxaChangeDeviceName").hidden = editMode;
|
||||
document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode;
|
||||
document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode;
|
||||
},
|
||||
|
||||
_updateComputerNameValue: function(save) {
|
||||
let textbox = document.getElementById("fxaSyncComputerName");
|
||||
if (save) {
|
||||
Weave.Service.clientsEngine.localName = textbox.value;
|
||||
}
|
||||
else {
|
||||
textbox.value = Weave.Service.clientsEngine.localName;
|
||||
}
|
||||
},
|
||||
|
||||
_setupEventListeners: function() {
|
||||
function setEventListener(aId, aEventType, aCallback)
|
||||
{
|
||||
@ -161,6 +180,17 @@ let gSyncPane = {
|
||||
setEventListener("syncComputerName", "change", function (e) {
|
||||
gSyncUtils.changeName(e.target);
|
||||
});
|
||||
setEventListener("fxaChangeDeviceName", "click", function () {
|
||||
this._toggleComputerNameControls(true);
|
||||
});
|
||||
setEventListener("fxaCancelChangeDeviceName", "click", function () {
|
||||
this._toggleComputerNameControls(false);
|
||||
this._updateComputerNameValue(false);
|
||||
});
|
||||
setEventListener("fxaSaveChangeDeviceName", "click", function () {
|
||||
this._toggleComputerNameControls(false);
|
||||
this._updateComputerNameValue(true);
|
||||
});
|
||||
setEventListener("unlinkDevice", "click", function () {
|
||||
gSyncPane.startOver(true);
|
||||
return false;
|
||||
|
@ -314,14 +314,30 @@
|
||||
<spacer/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
<hbox align="center">
|
||||
<label accesskey="&syncDeviceName.accesskey;"
|
||||
control="syncComputerName">
|
||||
&syncDeviceName.label;
|
||||
</label>
|
||||
<textbox id="fxaSyncComputerName"
|
||||
flex="1"/>
|
||||
</hbox>
|
||||
<vbox>
|
||||
<caption>
|
||||
<label accesskey="&syncDeviceName.accesskey;"
|
||||
control="fxaSyncComputerName">
|
||||
&fxaSyncDeviceName.label;
|
||||
</label>
|
||||
</caption>
|
||||
<hbox id="fxaDeviceName">
|
||||
<hbox flex="1">
|
||||
<textbox id="fxaSyncComputerName" class="plain"
|
||||
disabled="true" flex="1"/>
|
||||
</hbox>
|
||||
<hbox>
|
||||
<button id="fxaChangeDeviceName"
|
||||
label="&changeSyncDeviceName.label;"/>
|
||||
<button id="fxaCancelChangeDeviceName"
|
||||
label="&cancelChangeSyncDeviceName.label;"
|
||||
hidden="true"/>
|
||||
<button id="fxaSaveChangeDeviceName"
|
||||
label="&saveChangeSyncDeviceName.label;"
|
||||
hidden="true"/>
|
||||
</hbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
<spacer flex="1"/>
|
||||
<vbox id="tosPP-small">
|
||||
<label id="tosPP-small-ToS" class="text-link">
|
||||
|
@ -19,8 +19,5 @@ function test() {
|
||||
test_dependent_cookie_elements,
|
||||
test_dependent_clearonclose_elements,
|
||||
test_dependent_prefs,
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
}
|
||||
|
@ -18,8 +18,5 @@ function test() {
|
||||
test_custom_retention("rememberForms", "remember"),
|
||||
test_custom_retention("rememberForms", "custom"),
|
||||
test_historymode_retention("remember", "remember"),
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
}
|
||||
|
@ -27,8 +27,5 @@ function test() {
|
||||
test_custom_retention("alwaysClear", "remember"),
|
||||
test_custom_retention("alwaysClear", "custom"),
|
||||
test_historymode_retention("remember", "remember"),
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]));
|
||||
}
|
||||
|
@ -12,15 +12,16 @@ function test() {
|
||||
}
|
||||
loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
|
||||
|
||||
run_test_subset([
|
||||
let tests = [
|
||||
test_locbar_suggestion_retention("history", true),
|
||||
test_locbar_suggestion_retention("bookmark", true),
|
||||
test_locbar_suggestion_retention("searches", true),
|
||||
test_locbar_suggestion_retention("openpage", false),
|
||||
test_locbar_suggestion_retention("history", true),
|
||||
test_locbar_suggestion_retention("history", false),
|
||||
];
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
if (AppConstants.NIGHTLY_BUILD)
|
||||
tests.push(test_locbar_suggestion_retention("searches", true)),
|
||||
|
||||
run_test_subset(tests);
|
||||
}
|
||||
|
@ -27,8 +27,5 @@ function test() {
|
||||
|
||||
// history mode should now be remember
|
||||
test_historymode_retention("remember", "remember"),
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
}
|
||||
|
@ -315,11 +315,18 @@ function test_locbar_suggestion_retention(suggestion, autocomplete) {
|
||||
};
|
||||
}
|
||||
|
||||
const gPrefCache = new Map();
|
||||
|
||||
function cache_preferences(win) {
|
||||
let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
|
||||
for (let pref of prefs)
|
||||
gPrefCache.set(pref.name, pref.value);
|
||||
}
|
||||
|
||||
function reset_preferences(win) {
|
||||
let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
|
||||
for (let i = 0; i < prefs.length; ++i)
|
||||
if (prefs[i].hasUserValue)
|
||||
prefs[i].reset();
|
||||
for (let pref of prefs)
|
||||
pref.value = gPrefCache.get(pref.name);
|
||||
}
|
||||
|
||||
let testRunner;
|
||||
@ -334,7 +341,7 @@ function run_test_subset(subset) {
|
||||
});
|
||||
|
||||
testRunner = {
|
||||
tests: subset,
|
||||
tests: [cache_preferences, ...subset, reset_preferences],
|
||||
counter: 0,
|
||||
runNext: function() {
|
||||
if (this.counter == this.tests.length) {
|
||||
|
@ -4,6 +4,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Components.utils.import("resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
var gPrivacyPane = {
|
||||
|
||||
@ -69,6 +70,8 @@ var gPrivacyPane = {
|
||||
this._initTrackingProtection();
|
||||
#endif
|
||||
this._initAutocomplete();
|
||||
|
||||
document.getElementById("searchesSuggestion").hidden = !AppConstants.NIGHTLY_BUILD;
|
||||
},
|
||||
|
||||
// HISTORY MODE
|
||||
|
@ -284,6 +284,7 @@
|
||||
accesskey="&locbar.openpage.accesskey;"
|
||||
preference="browser.urlbar.suggest.openpage"/>
|
||||
<checkbox id="searchesSuggestion" label="&locbar.searches.label;"
|
||||
hidden="true"
|
||||
onsyncfrompreference="return gPrivacyPane.writeSuggestionPref();"
|
||||
accesskey="&locbar.searches.accesskey;"
|
||||
preference="browser.urlbar.suggest.searches"/>
|
||||
|
@ -20,8 +20,5 @@ function test() {
|
||||
test_dependent_cookie_elements,
|
||||
test_dependent_clearonclose_elements,
|
||||
test_dependent_prefs,
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
}
|
||||
|
@ -19,8 +19,5 @@ function test() {
|
||||
test_custom_retention("rememberForms", "remember"),
|
||||
test_custom_retention("rememberForms", "custom"),
|
||||
test_historymode_retention("remember", "remember"),
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
}
|
||||
|
@ -28,8 +28,5 @@ function test() {
|
||||
test_custom_retention("alwaysClear", "remember"),
|
||||
test_custom_retention("alwaysClear", "custom"),
|
||||
test_historymode_retention("remember", "remember"),
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]));
|
||||
}
|
||||
|
@ -12,15 +12,16 @@ function test() {
|
||||
}
|
||||
loader.loadSubScript(rootDir + "privacypane_tests_perwindow.js", this);
|
||||
|
||||
run_test_subset([
|
||||
let tests = [
|
||||
test_locbar_suggestion_retention("history", true),
|
||||
test_locbar_suggestion_retention("bookmark", true),
|
||||
test_locbar_suggestion_retention("searches", true),
|
||||
test_locbar_suggestion_retention("openpage", false),
|
||||
test_locbar_suggestion_retention("history", true),
|
||||
test_locbar_suggestion_retention("history", false),
|
||||
];
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
if (AppConstants.NIGHTLY_BUILD)
|
||||
tests.push(test_locbar_suggestion_retention("searches", true)),
|
||||
|
||||
run_test_subset(tests);
|
||||
}
|
||||
|
@ -28,8 +28,5 @@ function test() {
|
||||
|
||||
// history mode should now be remember
|
||||
test_historymode_retention("remember", "remember"),
|
||||
|
||||
// reset all preferences to their default values once we're done
|
||||
reset_preferences
|
||||
]);
|
||||
}
|
||||
|
@ -321,11 +321,18 @@ function test_locbar_suggestion_retention(suggestion, autocomplete) {
|
||||
};
|
||||
}
|
||||
|
||||
const gPrefCache = new Map();
|
||||
|
||||
function cache_preferences(win) {
|
||||
let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
|
||||
for (let pref of prefs)
|
||||
gPrefCache.set(pref.name, pref.value);
|
||||
}
|
||||
|
||||
function reset_preferences(win) {
|
||||
let prefs = win.document.getElementsByTagName("preference");
|
||||
for (let i = 0; i < prefs.length; ++i)
|
||||
if (prefs[i].hasUserValue)
|
||||
prefs[i].reset();
|
||||
let prefs = win.document.querySelectorAll("#privacyPreferences > preference");
|
||||
for (let pref of prefs)
|
||||
pref.value = gPrefCache.get(pref.name);
|
||||
}
|
||||
|
||||
let testRunner;
|
||||
@ -336,7 +343,7 @@ function run_test_subset(subset) {
|
||||
waitForExplicitFinish();
|
||||
|
||||
testRunner = {
|
||||
tests: subset,
|
||||
tests: [cache_preferences, ...subset, reset_preferences],
|
||||
counter: 0,
|
||||
runNext: function() {
|
||||
if (this.counter == this.tests.length) {
|
||||
|
@ -344,6 +344,15 @@ HistoryListener.prototype = {
|
||||
// This will be called for a pending tab when loadURI(uri) is called where
|
||||
// the given |uri| only differs in the fragment.
|
||||
OnHistoryNewEntry(newURI) {
|
||||
let currentURI = this.webNavigation.currentURI;
|
||||
|
||||
// Ignore new SHistory entries with the same URI as those do not indicate
|
||||
// a navigation inside a document by changing the #hash part of the URL.
|
||||
// We usually hit this when purging session history for browsers.
|
||||
if (currentURI && (currentURI.spec == newURI.spec)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the tab's URL to what it's actually showing. Without this loadURI()
|
||||
// would use the current document and change the displayed URL only.
|
||||
this.webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
|
||||
|
@ -91,14 +91,6 @@ this.SessionSaver = Object.freeze({
|
||||
SessionSaverInternal.updateLastSaveTime();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the last save time to zero. This will cause us to
|
||||
* immediately save the next time runDelayed() is called.
|
||||
*/
|
||||
clearLastSaveTime: function () {
|
||||
SessionSaverInternal.clearLastSaveTime();
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancels all pending session saves.
|
||||
*/
|
||||
@ -161,14 +153,6 @@ let SessionSaverInternal = {
|
||||
this._lastSaveTime = Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the last save time to zero. This will cause us to
|
||||
* immediately save the next time runDelayed() is called.
|
||||
*/
|
||||
clearLastSaveTime: function () {
|
||||
this._lastSaveTime = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancels all pending session saves.
|
||||
*/
|
||||
|
@ -909,10 +909,13 @@ let SessionStoreInternal = {
|
||||
// perform additional initialization when the first window is loading
|
||||
if (RunState.isStopped) {
|
||||
RunState.setRunning();
|
||||
SessionSaver.updateLastSaveTime();
|
||||
|
||||
// restore a crashed session resp. resume the last session if requested
|
||||
if (aInitialState) {
|
||||
// Don't write to disk right after startup. Set the last time we wrote
|
||||
// to disk to NOW() to enforce a full interval before the next write.
|
||||
SessionSaver.updateLastSaveTime();
|
||||
|
||||
if (isPrivateWindow) {
|
||||
// We're starting with a single private window. Save the state we
|
||||
// actually wanted to restore so that we can do it later in case
|
||||
@ -937,9 +940,6 @@ let SessionStoreInternal = {
|
||||
else {
|
||||
// Nothing to restore, notify observers things are complete.
|
||||
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED, "");
|
||||
|
||||
// The next delayed save request should execute immediately.
|
||||
SessionSaver.clearLastSaveTime();
|
||||
}
|
||||
}
|
||||
// this window was opened by _openWindowWithState
|
||||
@ -1281,15 +1281,11 @@ let SessionStoreInternal = {
|
||||
if (RunState.isQuitting)
|
||||
return;
|
||||
LastSession.clear();
|
||||
|
||||
let openWindows = {};
|
||||
this._forEachBrowserWindow(function(aWindow) {
|
||||
Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
|
||||
delete aTab.linkedBrowser.__SS_data;
|
||||
if (aTab.linkedBrowser.__SS_restoreState)
|
||||
this._resetTabRestoringState(aTab);
|
||||
}, this);
|
||||
openWindows[aWindow.__SSi] = true;
|
||||
});
|
||||
// Collect open windows.
|
||||
this._forEachBrowserWindow(({__SSi: id}) => openWindows[id] = true);
|
||||
|
||||
// also clear all data about closed tabs and windows
|
||||
for (let ix in this._windows) {
|
||||
if (ix in openWindows) {
|
||||
|
@ -18,27 +18,29 @@ this.EXPORTED_SYMBOLS = ["TabStateCache"];
|
||||
*/
|
||||
this.TabStateCache = Object.freeze({
|
||||
/**
|
||||
* Retrieves cached data for a given |browser|.
|
||||
* Retrieves cached data for a given |tab| or associated |browser|.
|
||||
*
|
||||
* @param browser (xul:browser)
|
||||
* The browser to retrieve cached data for.
|
||||
* @param browserOrTab (xul:tab or xul:browser)
|
||||
* The tab or browser to retrieve cached data for.
|
||||
* @return (object)
|
||||
* The cached data stored for the given |browser|.
|
||||
* The cached data stored for the given |tab|
|
||||
* or associated |browser|.
|
||||
*/
|
||||
get: function (browser) {
|
||||
return TabStateCacheInternal.get(browser);
|
||||
get: function (browserOrTab) {
|
||||
return TabStateCacheInternal.get(browserOrTab);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates cached data for a given |browser|.
|
||||
* Updates cached data for a given |tab| or associated |browser|.
|
||||
*
|
||||
* @param browser (xul:browser)
|
||||
* The browser belonging to the given tab data.
|
||||
* @param browserOrTab (xul:tab or xul:browser)
|
||||
* The tab or browser belonging to the given tab data.
|
||||
* @param newData (object)
|
||||
* The new data to be stored for the given |browser|.
|
||||
* The new data to be stored for the given |tab|
|
||||
* or associated |browser|.
|
||||
*/
|
||||
update: function (browser, newData) {
|
||||
TabStateCacheInternal.update(browser, newData);
|
||||
update: function (browserOrTab, newData) {
|
||||
TabStateCacheInternal.update(browserOrTab, newData);
|
||||
}
|
||||
});
|
||||
|
||||
@ -46,27 +48,29 @@ let TabStateCacheInternal = {
|
||||
_data: new WeakMap(),
|
||||
|
||||
/**
|
||||
* Retrieves cached data for a given |browser|.
|
||||
* Retrieves cached data for a given |tab| or associated |browser|.
|
||||
*
|
||||
* @param browser (xul:browser)
|
||||
* The browser to retrieve cached data for.
|
||||
* @param browserOrTab (xul:tab or xul:browser)
|
||||
* The tab or browser to retrieve cached data for.
|
||||
* @return (object)
|
||||
* The cached data stored for the given |browser|.
|
||||
* The cached data stored for the given |tab|
|
||||
* or associated |browser|.
|
||||
*/
|
||||
get: function (browser) {
|
||||
return this._data.get(browser.permanentKey);
|
||||
get: function (browserOrTab) {
|
||||
return this._data.get(browserOrTab.permanentKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates cached data for a given |browser|.
|
||||
* Updates cached data for a given |tab| or associated |browser|.
|
||||
*
|
||||
* @param browser (xul:browser)
|
||||
* The browser belonging to the given tab data.
|
||||
* @param browserOrTab (xul:tab or xul:browser)
|
||||
* The tab or browser belonging to the given tab data.
|
||||
* @param newData (object)
|
||||
* The new data to be stored for the given |browser|.
|
||||
* The new data to be stored for the given |tab|
|
||||
* or associated |browser|.
|
||||
*/
|
||||
update: function (browser, newData) {
|
||||
let data = this._data.get(browser.permanentKey) || {};
|
||||
update: function (browserOrTab, newData) {
|
||||
let data = this._data.get(browserOrTab.permanentKey) || {};
|
||||
|
||||
for (let key of Object.keys(newData)) {
|
||||
let value = newData[key];
|
||||
@ -77,6 +81,6 @@ let TabStateCacheInternal = {
|
||||
}
|
||||
}
|
||||
|
||||
this._data.set(browser.permanentKey, data);
|
||||
this._data.set(browserOrTab.permanentKey, data);
|
||||
}
|
||||
};
|
||||
|
@ -96,6 +96,8 @@ skip-if = buildapp == 'mulet'
|
||||
[browser_pageStyle.js]
|
||||
[browser_pending_tabs.js]
|
||||
[browser_privatetabs.js]
|
||||
[browser_purge_shistory.js]
|
||||
skip-if = e10s
|
||||
[browser_replace_load.js]
|
||||
[browser_restore_redirect.js]
|
||||
[browser_scrollPositions.js]
|
||||
|
@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This test checks that pending tabs are treated like fully loaded tabs when
|
||||
* purging session history. Just like for fully loaded tabs we want to remove
|
||||
* every but the current shistory entry.
|
||||
*/
|
||||
|
||||
const TAB_STATE = {
|
||||
entries: [{url: "about:mozilla"}, {url: "about:robots"}],
|
||||
index: 1,
|
||||
};
|
||||
|
||||
function checkTabContents(browser) {
|
||||
return ContentTask.spawn(browser, null, function* () {
|
||||
let Ci = Components.interfaces;
|
||||
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
|
||||
return history && history.count == 1 && content.document.documentURI == "about:mozilla";
|
||||
});
|
||||
}
|
||||
|
||||
add_task(function* () {
|
||||
// Create a new tab.
|
||||
let tab = gBrowser.addTab("about:blank");
|
||||
let browser = tab.linkedBrowser;
|
||||
yield promiseBrowserLoaded(browser);
|
||||
yield promiseTabState(tab, TAB_STATE);
|
||||
|
||||
// Create another new tab.
|
||||
let tab2 = gBrowser.addTab("about:blank");
|
||||
let browser2 = tab2.linkedBrowser;
|
||||
yield promiseBrowserLoaded(browser2);
|
||||
|
||||
// The tab shouldn't be restored right away.
|
||||
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
|
||||
|
||||
// Prepare the tab state.
|
||||
let promise = promiseTabRestoring(tab2);
|
||||
ss.setTabState(tab2, JSON.stringify(TAB_STATE));
|
||||
ok(tab2.hasAttribute("pending"), "tab is pending");
|
||||
yield promise;
|
||||
|
||||
// Purge session history.
|
||||
Services.obs.notifyObservers(null, "browser:purge-session-history", "");
|
||||
ok((yield checkTabContents(browser)), "expected tab contents found");
|
||||
ok(tab2.hasAttribute("pending"), "tab is still pending");
|
||||
|
||||
// Kick off tab restoration.
|
||||
gBrowser.selectedTab = tab2;
|
||||
yield promiseTabRestored(tab2);
|
||||
ok((yield checkTabContents(browser2)), "expected tab contents found");
|
||||
ok(!tab2.hasAttribute("pending"), "tab is not pending anymore");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab2);
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
@ -31,36 +31,6 @@ add_task(function test_load_start() {
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that purging shistory invalidates.
|
||||
*/
|
||||
add_task(function test_purge() {
|
||||
// Create a new tab.
|
||||
let tab = gBrowser.addTab("about:mozilla");
|
||||
let browser = tab.linkedBrowser;
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
// Create a second shistory entry.
|
||||
browser.loadURI("about:robots");
|
||||
yield promiseBrowserLoaded(browser);
|
||||
|
||||
// Check that we now have two shistory entries.
|
||||
yield TabStateFlusher.flush(browser);
|
||||
let {entries} = JSON.parse(ss.getTabState(tab));
|
||||
is(entries.length, 2, "there are two shistory entries");
|
||||
|
||||
// Purge session history.
|
||||
yield sendMessage(browser, "ss-test:purgeSessionHistory");
|
||||
|
||||
// Check that we are left with a single shistory entry.
|
||||
yield TabStateFlusher.flush(browser);
|
||||
({entries} = JSON.parse(ss.getTabState(tab)));
|
||||
is(entries.length, 1, "there is one shistory entry");
|
||||
|
||||
// Cleanup.
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensure that anchor navigation invalidates shistory.
|
||||
*/
|
||||
|
@ -84,11 +84,6 @@ addMessageListener("ss-test:purgeDomainData", function ({data: domain}) {
|
||||
content.setTimeout(() => sendAsyncMessage("ss-test:purgeDomainData"));
|
||||
});
|
||||
|
||||
addMessageListener("ss-test:purgeSessionHistory", function () {
|
||||
Services.obs.notifyObservers(null, "browser:purge-session-history", "");
|
||||
content.setTimeout(() => sendAsyncMessage("ss-test:purgeSessionHistory"));
|
||||
});
|
||||
|
||||
addMessageListener("ss-test:getStyleSheets", function (msg) {
|
||||
let sheets = content.document.styleSheets;
|
||||
let titles = Array.map(sheets, ss => [ss.title, ss.disabled]);
|
||||
|
@ -87,6 +87,7 @@ support-files =
|
||||
doc_pretty-print-2.html
|
||||
doc_pretty-print-3.html
|
||||
doc_pretty-print-on-paused.html
|
||||
doc_promise-get-allocation-stack.html
|
||||
doc_promise.html
|
||||
doc_random-javascript.html
|
||||
doc_recursion-stack.html
|
||||
@ -346,7 +347,11 @@ skip-if = e10s && debug
|
||||
[browser_dbg_pretty-print-on-paused.js]
|
||||
skip-if = e10s && debug
|
||||
[browser_dbg_progress-listener-bug.js]
|
||||
skip-if = e10a && debug
|
||||
skip-if = e10s && debug
|
||||
[browser_dbg_promises-allocation-stack.js]
|
||||
skip-if = e10s && debug
|
||||
[browser_dbg_promises-chrome-allocation-stack.js]
|
||||
skip-if = e10s && debug
|
||||
[browser_dbg_reload-preferred-script-01.js]
|
||||
skip-if = e10s && debug
|
||||
[browser_dbg_reload-preferred-script-02.js]
|
||||
|
@ -0,0 +1,81 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Test that we can get a stack to a promise's allocation point.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const TAB_URL = EXAMPLE_URL + "doc_promise-get-allocation-stack.html";
|
||||
const { PromisesFront } = devtools.require("devtools/server/actors/promises");
|
||||
let events = devtools.require("sdk/event/core");
|
||||
|
||||
function test() {
|
||||
Task.spawn(function* () {
|
||||
DebuggerServer.init();
|
||||
DebuggerServer.addBrowserActors();
|
||||
|
||||
const [ tab,, panel ] = yield initDebugger(TAB_URL);
|
||||
|
||||
let client = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
yield connect(client);
|
||||
|
||||
let { tabs } = yield listTabs(client);
|
||||
let targetTab = findTab(tabs, TAB_URL);
|
||||
yield attachTab(client, targetTab);
|
||||
|
||||
yield testGetAllocationStack(client, targetTab, tab);
|
||||
|
||||
yield close(client);
|
||||
yield closeDebuggerAndFinish(panel);
|
||||
}).then(null, error => {
|
||||
ok(false, "Got an error: " + error.message + "\n" + error.stack);
|
||||
});
|
||||
}
|
||||
|
||||
function* testGetAllocationStack(client, form, tab) {
|
||||
let front = PromisesFront(client, form);
|
||||
|
||||
yield front.attach();
|
||||
yield front.listPromises();
|
||||
|
||||
// Get the grip for promise p
|
||||
let onNewPromise = new Promise(resolve => {
|
||||
events.on(front, "new-promises", promises => {
|
||||
for (let p of promises) {
|
||||
if (p.preview.ownProperties.name &&
|
||||
p.preview.ownProperties.name.value === "p") {
|
||||
resolve(p);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
callInTab(tab, "makePromises");
|
||||
|
||||
let grip = yield onNewPromise;
|
||||
ok(grip, "Found our promise p");
|
||||
|
||||
let objectClient = new ObjectClient(client, grip);
|
||||
ok(objectClient, "Got Object Client");
|
||||
|
||||
yield new Promise(resolve => {
|
||||
objectClient.getPromiseAllocationStack(response => {
|
||||
ok(response.allocationStack.length, "Got promise allocation stack.");
|
||||
|
||||
for (let stack of response.allocationStack) {
|
||||
is(stack.source.url, TAB_URL, "Got correct source URL.");
|
||||
is(stack.functionDisplayName, "makePromises",
|
||||
"Got correct function display name.");
|
||||
is(typeof stack.line, "number", "Expect stack line to be a number.");
|
||||
is(typeof stack.column, "number",
|
||||
"Expect stack column to be a number.");
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
yield front.detach();
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Test that we can get a stack to a promise's allocation point in the chrome
|
||||
* process.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const SOURCE_URL = "browser_dbg_promises-chrome-allocation-stack.js";
|
||||
const { PromisesFront } = devtools.require("devtools/server/actors/promises");
|
||||
let events = devtools.require("sdk/event/core");
|
||||
|
||||
const STACK_DATA = [
|
||||
{ functionDisplayName: "test/</<" },
|
||||
{ functionDisplayName: "testGetAllocationStack" },
|
||||
];
|
||||
|
||||
function test() {
|
||||
Task.spawn(function* () {
|
||||
requestLongerTimeout(10);
|
||||
|
||||
DebuggerServer.init();
|
||||
DebuggerServer.addBrowserActors();
|
||||
DebuggerServer.allowChromeProcess = true;
|
||||
|
||||
let client = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
yield connect(client);
|
||||
let chrome = yield client.getProcess();
|
||||
let [, tabClient] = yield attachTab(client, chrome.form);
|
||||
yield tabClient.attachThread();
|
||||
|
||||
yield testGetAllocationStack(client, chrome.form, () => {
|
||||
let p = new Promise(() => {});
|
||||
p.name = "p";
|
||||
let q = p.then();
|
||||
q.name = "q";
|
||||
let r = p.then(null, () => {});
|
||||
r.name = "r";
|
||||
});
|
||||
|
||||
yield close(client);
|
||||
finish();
|
||||
}).then(null, error => {
|
||||
ok(false, "Got an error: " + error.message + "\n" + error.stack);
|
||||
});
|
||||
}
|
||||
|
||||
function* testGetAllocationStack(client, form, makePromises) {
|
||||
let front = PromisesFront(client, form);
|
||||
|
||||
yield front.attach();
|
||||
yield front.listPromises();
|
||||
|
||||
// Get the grip for promise p
|
||||
let onNewPromise = new Promise(resolve => {
|
||||
events.on(front, "new-promises", promises => {
|
||||
for (let p of promises) {
|
||||
if (p.preview.ownProperties.name &&
|
||||
p.preview.ownProperties.name.value === "p") {
|
||||
resolve(p);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
makePromises();
|
||||
|
||||
let grip = yield onNewPromise;
|
||||
ok(grip, "Found our promise p");
|
||||
|
||||
let objectClient = new ObjectClient(client, grip);
|
||||
ok(objectClient, "Got Object Client");
|
||||
|
||||
yield new Promise(resolve => {
|
||||
objectClient.getPromiseAllocationStack(response => {
|
||||
ok(response.allocationStack.length, "Got promise allocation stack.");
|
||||
|
||||
for (let i = 0; i < STACK_DATA.length; i++) {
|
||||
let data = STACK_DATA[i];
|
||||
let stack = response.allocationStack[i];
|
||||
|
||||
ok(stack.source.url.startsWith("chrome:"), "Got a chrome source URL");
|
||||
ok(stack.source.url.endsWith(SOURCE_URL), "Got correct source URL.");
|
||||
is(stack.functionDisplayName, data.functionDisplayName,
|
||||
"Got correct function display name.");
|
||||
is(typeof stack.line, "number", "Expect stack line to be a number.");
|
||||
is(typeof stack.column, "number",
|
||||
"Expect stack column to be a number.");
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
yield front.detach();
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Promise test page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
function makePromises() {
|
||||
var p = new Promise(() => {});
|
||||
p.name = "p";
|
||||
var q = p.then();
|
||||
q.name = "q";
|
||||
var r = p.then(null, () => {});
|
||||
r.name = "r";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -20,7 +20,8 @@ let { require } = devtools;
|
||||
let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
|
||||
let { BrowserToolboxProcess } = Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", {});
|
||||
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
|
||||
let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
|
||||
let { DebuggerClient, ObjectClient } =
|
||||
Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
|
||||
let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
|
||||
let EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
const { promiseInvoke } = require("devtools/async-utils");
|
||||
|
@ -34,6 +34,7 @@ support-files =
|
||||
[browser_toolbox_hosts.js]
|
||||
[browser_toolbox_hosts_size.js]
|
||||
[browser_toolbox_minimize.js]
|
||||
skip-if = true # Bug 1177463 - Temporarily hide the minimize button
|
||||
[browser_toolbox_options.js]
|
||||
[browser_toolbox_options_disable_buttons.js]
|
||||
[browser_toolbox_options_disable_cache-01.js]
|
||||
|
@ -15,8 +15,6 @@ function* runEventPopupTests() {
|
||||
yield checkEventsForNode(selector, expected, inspector);
|
||||
}
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
|
||||
// Wait for promises to avoid leaks when running this as a single test.
|
||||
// We need to do this because we have opened a bunch of popups and don't them
|
||||
// to affect other test runs when they are GCd.
|
||||
|
@ -35,7 +35,6 @@ const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
|
||||
const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
|
||||
const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
|
||||
const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
|
||||
const DEFAULT_HTTP_VERSION = "HTTP/1.1";
|
||||
const REQUEST_TIME_DECIMALS = 2;
|
||||
const HEADERS_SIZE_DECIMALS = 3;
|
||||
const CONTENT_SIZE_DECIMALS = 2;
|
||||
@ -2641,7 +2640,7 @@ NetworkDetailsView.prototype = {
|
||||
$("#headers-summary-status").setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) {
|
||||
if (aData.httpVersion) {
|
||||
$("#headers-summary-version-value").setAttribute("value", aData.httpVersion);
|
||||
$("#headers-summary-version").removeAttribute("hidden");
|
||||
} else {
|
||||
|
@ -234,10 +234,11 @@ function InplaceEditor(aOptions, aEvent)
|
||||
this.elt.style.display = "none";
|
||||
this.elt.parentNode.insertBefore(this.input, this.elt);
|
||||
|
||||
this.input.focus();
|
||||
|
||||
if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
|
||||
this.input.select();
|
||||
}
|
||||
this.input.focus();
|
||||
|
||||
if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") {
|
||||
this._maybeSuggestCompletion(true);
|
||||
@ -959,7 +960,8 @@ InplaceEditor.prototype = {
|
||||
}
|
||||
if ((this.stopOnReturn &&
|
||||
aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) ||
|
||||
(this.stopOnTab && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB)) {
|
||||
(this.stopOnTab && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
|
||||
!aEvent.shiftKey)) {
|
||||
direction = null;
|
||||
}
|
||||
|
||||
|
@ -2710,7 +2710,8 @@ RuleEditor.prototype = {
|
||||
}
|
||||
|
||||
this.selectorText = createChild(this.selectorContainer, "span", {
|
||||
class: "ruleview-selector theme-fg-color3"
|
||||
class: "ruleview-selector theme-fg-color3",
|
||||
tabindex: this.isSelectorEditable ? "0" : "-1",
|
||||
});
|
||||
|
||||
if (this.isSelectorEditable) {
|
||||
@ -2722,9 +2723,6 @@ RuleEditor.prototype = {
|
||||
editableField({
|
||||
element: this.selectorText,
|
||||
done: this._onSelectorDone,
|
||||
stopOnShiftTab: true,
|
||||
stopOnTab: true,
|
||||
stopOnReturn: true
|
||||
});
|
||||
}
|
||||
|
||||
@ -3001,14 +2999,16 @@ RuleEditor.prototype = {
|
||||
* Ignores the change if the user pressed escape, otherwise
|
||||
* commits it.
|
||||
*
|
||||
* @param {string} aValue
|
||||
* @param {string} value
|
||||
* The value contained in the editor.
|
||||
* @param {boolean} aCommit
|
||||
* @param {boolean} commit
|
||||
* True if the change should be applied.
|
||||
* @param {number} direction
|
||||
* The move focus direction number.
|
||||
*/
|
||||
_onSelectorDone: function(aValue, aCommit) {
|
||||
if (!aCommit || this.isEditing || aValue === "" ||
|
||||
aValue === this.rule.selectorText) {
|
||||
_onSelectorDone: function(value, commit, direction) {
|
||||
if (!commit || this.isEditing || value === "" ||
|
||||
value === this.rule.selectorText) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3020,7 +3020,7 @@ RuleEditor.prototype = {
|
||||
|
||||
this.isEditing = true;
|
||||
|
||||
this.rule.domRule.modifySelector(element, aValue).then(response => {
|
||||
this.rule.domRule.modifySelector(element, value).then(response => {
|
||||
this.isEditing = false;
|
||||
|
||||
if (!supportsUnmatchedRules) {
|
||||
@ -3052,10 +3052,34 @@ RuleEditor.prototype = {
|
||||
ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
|
||||
ruleView.highlightedSelector);
|
||||
}
|
||||
|
||||
this._moveSelectorFocus(newRule, direction);
|
||||
}).then(null, err => {
|
||||
this.isEditing = false;
|
||||
promiseWarn(err);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle moving the focus change after pressing tab and return from the
|
||||
* selector inplace editor. The focused element after a tab or return keypress
|
||||
* is lost because the rule editor is replaced.
|
||||
*
|
||||
* @param {Rule} rule
|
||||
* The Rule object.
|
||||
* @param {number} direction
|
||||
* The move focus direction number.
|
||||
*/
|
||||
_moveSelectorFocus: function(rule, direction) {
|
||||
if (!direction || direction == Ci.nsIFocusManager.MOVEFOCUS_BACKWARD) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rule.textProps.length > 0) {
|
||||
rule.textProps[0].editor.nameSpan.click();
|
||||
} else {
|
||||
this.propertyList.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -7,49 +7,57 @@
|
||||
// Test selector value is correctly displayed when committing the inplace editor
|
||||
// with ENTER, ESC, SHIFT+TAB and TAB
|
||||
|
||||
let PAGE_CONTENT = [
|
||||
'<style type="text/css">',
|
||||
' #testid {',
|
||||
' text-align: center;',
|
||||
' }',
|
||||
'</style>',
|
||||
'<div id="testid" class="testclass">Styled Node</div>',
|
||||
let TEST_URI = [
|
||||
"<style type='text/css'>",
|
||||
" #testid1 {",
|
||||
" text-align: center;",
|
||||
" }",
|
||||
" #testid2 {",
|
||||
" text-align: center;",
|
||||
" }",
|
||||
" #testid3 {",
|
||||
" }",
|
||||
"</style>",
|
||||
"<div id='testid1'>Styled Node</div>",
|
||||
"<div id='testid2'>Styled Node</div>",
|
||||
"<div id='testid3'>Styled Node</div>",
|
||||
].join("\n");
|
||||
|
||||
const TEST_DATA = [
|
||||
{
|
||||
node: "#testid",
|
||||
node: "#testid1",
|
||||
value: ".testclass",
|
||||
commitKey: "VK_ESCAPE",
|
||||
modifiers: {},
|
||||
expected: "#testid"
|
||||
expected: "#testid1",
|
||||
|
||||
},
|
||||
{
|
||||
node: "#testid",
|
||||
value: ".testclass",
|
||||
node: "#testid1",
|
||||
value: ".testclass1",
|
||||
commitKey: "VK_RETURN",
|
||||
modifiers: {},
|
||||
expected: ".testclass"
|
||||
expected: ".testclass1"
|
||||
},
|
||||
{
|
||||
node: "#testid",
|
||||
value: ".testclass",
|
||||
node: "#testid2",
|
||||
value: ".testclass2",
|
||||
commitKey: "VK_TAB",
|
||||
modifiers: {},
|
||||
expected: ".testclass"
|
||||
expected: ".testclass2"
|
||||
},
|
||||
{
|
||||
node: "#testid",
|
||||
value: ".testclass",
|
||||
node: "#testid3",
|
||||
value: ".testclass3",
|
||||
commitKey: "VK_TAB",
|
||||
modifiers: {shiftKey: true},
|
||||
expected: ".testclass"
|
||||
expected: ".testclass3"
|
||||
}
|
||||
];
|
||||
|
||||
add_task(function*() {
|
||||
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT));
|
||||
let {toolbox, inspector, view} = yield openRuleView();
|
||||
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
|
||||
let { inspector, view } = yield openRuleView();
|
||||
|
||||
info("Iterating over the test data");
|
||||
for (let data of TEST_DATA) {
|
||||
@ -60,7 +68,8 @@ add_task(function*() {
|
||||
function* runTestData(inspector, view, data) {
|
||||
let {node, value, commitKey, modifiers, expected} = data;
|
||||
|
||||
info("Updating " + node + " to " + value + " and committing with " + commitKey + ". Expecting: " + expected);
|
||||
info("Updating " + node + " to " + value + " and committing with " +
|
||||
commitKey + ". Expecting: " + expected);
|
||||
|
||||
info("Selecting the test element");
|
||||
yield selectNode(node, inspector);
|
||||
@ -78,16 +87,32 @@ function* runTestData(inspector, view, data) {
|
||||
info("Entering the commit key " + commitKey + " " + modifiers);
|
||||
EventUtils.synthesizeKey(commitKey, modifiers);
|
||||
|
||||
let activeElement = view.doc.activeElement;
|
||||
|
||||
if (commitKey === "VK_ESCAPE") {
|
||||
is(idRuleEditor.rule.selectorText, expected,
|
||||
"Value is as expected: " + expected);
|
||||
is(idRuleEditor.isEditing, false, "Selector is not being edited.")
|
||||
} else {
|
||||
yield once(view, "ruleview-changed");
|
||||
ok(getRuleViewRule(view, expected),
|
||||
"Rule with " + name + " selector exists.");
|
||||
is(idRuleEditor.isEditing, false, "Selector is not being edited.");
|
||||
is(idRuleEditor.selectorText, activeElement,
|
||||
"Focus is on selector span.");
|
||||
return;
|
||||
}
|
||||
|
||||
info("Resetting page content");
|
||||
content.document.body.innerHTML = PAGE_CONTENT;
|
||||
yield once(view, "ruleview-changed");
|
||||
|
||||
ok(getRuleViewRule(view, expected),
|
||||
"Rule with " + expected + " selector exists.");
|
||||
|
||||
if (modifiers.shiftKey) {
|
||||
idRuleEditor = getRuleViewRuleEditor(view, 0);
|
||||
}
|
||||
|
||||
let rule = idRuleEditor.rule;
|
||||
if (rule.textProps.length > 0) {
|
||||
is(inplaceEditor(rule.textProps[0].editor.nameSpan).input, activeElement,
|
||||
"Focus is on the first property name span.");
|
||||
} else {
|
||||
is(inplaceEditor(idRuleEditor.newPropSpan).input, activeElement,
|
||||
"Focus is on the new property span.");
|
||||
}
|
||||
}
|
||||
|
@ -440,7 +440,7 @@ WebConsole.prototype = {
|
||||
// Attempt to access view source via a browser first, which may display it in
|
||||
// a tab, if enabled.
|
||||
let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
if (browserWin) {
|
||||
if (browserWin && browserWin.BrowserViewSourceOfDocument) {
|
||||
return browserWin.BrowserViewSourceOfDocument({
|
||||
URL: aSourceURL,
|
||||
lineNumber: aSourceLine
|
||||
|
@ -10,13 +10,18 @@
|
||||
* the according message is displayed in the web console.
|
||||
*/
|
||||
|
||||
const EXPECTED_RESULT = "Not supporting directive 'reflected-xss'. Directive and values will be ignored.";
|
||||
const TEST_FILE = "http://example.com/browser/browser/devtools/webconsole/test/" +
|
||||
"test_bug1045902_console_csp_ignore_reflected_xss_message.html";
|
||||
"use strict";
|
||||
|
||||
const EXPECTED_RESULT = "Not supporting directive 'reflected-xss'. Directive " +
|
||||
"and values will be ignored.";
|
||||
const TEST_FILE = "http://example.com/browser/browser/devtools/webconsole/" +
|
||||
"test/test_bug1045902_console_csp_ignore_reflected_xss_" +
|
||||
"message.html";
|
||||
|
||||
let hud = undefined;
|
||||
|
||||
let TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring reflected XSS (bug 1045902)";
|
||||
let TEST_URI = "data:text/html;charset=utf8,Web Console CSP ignoring " +
|
||||
"reflected XSS (bug 1045902)";
|
||||
|
||||
let test = asyncTest(function* () {
|
||||
let { browser } = yield loadTab(TEST_URI);
|
||||
@ -29,11 +34,10 @@ let test = asyncTest(function* () {
|
||||
hud = null;
|
||||
});
|
||||
|
||||
|
||||
function loadDocument(browser) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
hud.jsterm.clearOutput()
|
||||
hud.jsterm.clearOutput();
|
||||
browser.addEventListener("load", function onLoad() {
|
||||
browser.removeEventListener("load", onLoad, true);
|
||||
deferred.resolve();
|
||||
@ -44,15 +48,15 @@ function loadDocument(browser) {
|
||||
}
|
||||
|
||||
function testViolationMessage() {
|
||||
let deferred = promise.defer();
|
||||
let aOutputNode = hud.outputNode;
|
||||
|
||||
return waitForSuccess({
|
||||
name: "Confirming that CSP logs messages to the console when 'reflected-xss' directive is used!",
|
||||
name: "Confirming that CSP logs messages to the console when " +
|
||||
"'reflected-xss' directive is used!",
|
||||
validator: function() {
|
||||
console.log(hud.outputNode.textContent);
|
||||
console.log(aOutputNode.textContent);
|
||||
let success = false;
|
||||
success = hud.outputNode.textContent.indexOf(EXPECTED_RESULT) > -1;
|
||||
success = aOutputNode.textContent.indexOf(EXPECTED_RESULT) > -1;
|
||||
return success;
|
||||
}
|
||||
});
|
||||
|
@ -10,8 +10,10 @@
|
||||
"use strict";
|
||||
|
||||
let test = asyncTest(function* () {
|
||||
const TEST_URI1 = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html";
|
||||
const TEST_URI2 = "http://example.org/browser/browser/devtools/webconsole/test/test-console.html";
|
||||
const TEST_URI1 = "http://example.com/browser/browser/devtools/webconsole/" +
|
||||
"test/test-console.html";
|
||||
const TEST_URI2 = "http://example.org/browser/browser/devtools/webconsole/" +
|
||||
"test/test-console.html";
|
||||
|
||||
yield loadTab(TEST_URI1);
|
||||
let hud = yield openConsole();
|
||||
|
@ -55,7 +55,8 @@ let test = asyncTest(function* () {
|
||||
});
|
||||
|
||||
hud.jsterm.clearOutput();
|
||||
content.location.reload(); // Reloading will produce network logging
|
||||
// Reloading will produce network logging
|
||||
content.location.reload();
|
||||
|
||||
// Test that the "Copy Link Location" command is enabled and works
|
||||
// as expected for any network-related message.
|
||||
@ -80,10 +81,15 @@ let test = asyncTest(function* () {
|
||||
|
||||
let deferred = promise.defer();
|
||||
|
||||
waitForClipboard((aData) => { return aData.trim() == message.url; },
|
||||
() => { goDoCommand(COMMAND_NAME); },
|
||||
() => { deferred.resolve(null); },
|
||||
() => { deferred.reject(null); });
|
||||
waitForClipboard((aData) => {
|
||||
return aData.trim() == message.url;
|
||||
}, () => {
|
||||
goDoCommand(COMMAND_NAME);
|
||||
}, () => {
|
||||
deferred.resolve(null);
|
||||
}, () => {
|
||||
deferred.reject(null);
|
||||
});
|
||||
|
||||
yield deferred.promise;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user