Merge mozilla-central to b2g-inbound

This commit is contained in:
Carsten "Tomcat" Book 2015-06-26 14:10:14 +02:00
commit 93acb4823c
739 changed files with 11844 additions and 5566 deletions

View File

@ -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,

View File

@ -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

View File

@ -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++;
}

View File

@ -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()
{

View File

@ -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)

View File

@ -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.

View File

@ -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);

View File

@ -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=".">

View File

@ -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]

View 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>

View File

@ -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]

View 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>

View File

@ -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

View File

@ -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.

View File

@ -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) {

View File

@ -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 {

View File

@ -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));

View File

@ -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]

View File

@ -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);
});

View File

@ -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();
},

View File

@ -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"/>

View File

@ -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>

View File

@ -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 = "";

View File

@ -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

View File

@ -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() {

View File

@ -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() {

View File

@ -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
},

View File

@ -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
},

View File

@ -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"},

View File

@ -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">

View File

@ -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})

View File

@ -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} />

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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)
}),
/**

View File

@ -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
},

View File

@ -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
},

View File

@ -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) {

View File

@ -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);
}

View File

@ -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 });

View File

@ -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);

View File

@ -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);

View File

@ -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
},

View File

@ -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
},

View File

@ -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)

View File

@ -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});
},

View File

@ -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} />;
},

View File

@ -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}),

View File

@ -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} />

View File

@ -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;

View File

@ -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;

View File

@ -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();
});
});

View File

@ -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"

View File

@ -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"
}));
});

View File

@ -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"
}
};

View File

@ -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})

View File

@ -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&nbsp;
<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}/>

View File

@ -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

View File

@ -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"/>

View File

@ -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;

View File

@ -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">

View File

@ -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
]);
}

View File

@ -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
]);
}

View File

@ -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
]));
}

View File

@ -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);
}

View File

@ -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
]);
}

View File

@ -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) {

View File

@ -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

View File

@ -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"/>

View File

@ -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
]);
}

View File

@ -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
]);
}

View File

@ -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
]));
}

View File

@ -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);
}

View File

@ -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
]);
}

View File

@ -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) {

View File

@ -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"));

View File

@ -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.
*/

View File

@ -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) {

View File

@ -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);
}
};

View File

@ -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]

View File

@ -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);
});

View File

@ -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.
*/

View File

@ -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]);

View File

@ -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]

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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");

View File

@ -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]

View File

@ -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.

View File

@ -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 {

View File

@ -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;
}

View File

@ -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();
}
}
};

View File

@ -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.");
}
}

View File

@ -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

View File

@ -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;
}
});

View File

@ -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();

View File

@ -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