Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
fa0a857d
Unverified
Commit
fa0a857d
authored
Sep 11, 2018
by
Jonah Williams
Committed by
GitHub
Sep 11, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add material tap target size and text contrast test to gallery (#21581)
parent
387948a2
Changes
3
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
685 additions
and
31 deletions
+685
-31
accessibility_test.dart
examples/flutter_gallery/test/accessibility_test.dart
+471
-0
accessibility.dart
packages/flutter_test/lib/src/accessibility.dart
+61
-26
accessibility_test.dart
packages/flutter_test/test/accessibility_test.dart
+153
-5
No files found.
examples/flutter_gallery/test/accessibility_test.dart
0 → 100644
View file @
fa0a857d
This diff is collapsed.
Click to expand it.
packages/flutter_test/lib/src/accessibility.dart
View file @
fa0a857d
...
@@ -40,8 +40,10 @@ class Evaluation {
...
@@ -40,8 +40,10 @@ class Evaluation {
if
(
other
==
null
)
if
(
other
==
null
)
return
this
;
return
this
;
final
StringBuffer
buffer
=
new
StringBuffer
();
final
StringBuffer
buffer
=
new
StringBuffer
();
if
(
reason
!=
null
)
if
(
reason
!=
null
)
{
buffer
.
write
(
reason
);
buffer
.
write
(
reason
);
buffer
.
write
(
' '
);
}
if
(
other
.
reason
!=
null
)
if
(
other
.
reason
!=
null
)
buffer
.
write
(
other
.
reason
);
buffer
.
write
(
other
.
reason
);
return
new
Evaluation
.
_
(
passed
&&
other
.
passed
,
buffer
.
isEmpty
?
null
:
buffer
.
toString
());
return
new
Evaluation
.
_
(
passed
&&
other
.
passed
,
buffer
.
isEmpty
?
null
:
buffer
.
toString
());
...
@@ -84,8 +86,13 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
...
@@ -84,8 +86,13 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
result
+=
traverse
(
child
);
result
+=
traverse
(
child
);
return
true
;
return
true
;
});
});
if
(
node
.
isMergedIntoParent
)
return
result
;
final
SemanticsData
data
=
node
.
getSemanticsData
();
final
SemanticsData
data
=
node
.
getSemanticsData
();
if
(!
data
.
hasAction
(
ui
.
SemanticsAction
.
longPress
)
&&
!
data
.
hasAction
(
ui
.
SemanticsAction
.
tap
))
// Skip node if it has no actions, or is marked as hidden.
if
((!
data
.
hasAction
(
ui
.
SemanticsAction
.
longPress
)
&&
!
data
.
hasAction
(
ui
.
SemanticsAction
.
tap
))
||
data
.
hasFlag
(
ui
.
SemanticsFlag
.
isHidden
))
return
result
;
return
result
;
Rect
paintBounds
=
node
.
rect
;
Rect
paintBounds
=
node
.
rect
;
SemanticsNode
current
=
node
;
SemanticsNode
current
=
node
;
...
@@ -94,6 +101,14 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
...
@@ -94,6 +101,14 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
paintBounds
=
MatrixUtils
.
transformRect
(
current
.
transform
,
paintBounds
);
paintBounds
=
MatrixUtils
.
transformRect
(
current
.
transform
,
paintBounds
);
current
=
current
.
parent
;
current
=
current
.
parent
;
}
}
// skip node if it is touching the edge of the screen, since it might
// be partially scrolled offscreen.
const
double
delta
=
0.001
;
if
(
paintBounds
.
left
<=
delta
||
paintBounds
.
top
<=
delta
||
(
paintBounds
.
bottom
-
ui
.
window
.
physicalSize
.
height
).
abs
()
<=
delta
||
(
paintBounds
.
right
-
ui
.
window
.
physicalSize
.
width
).
abs
()
<=
delta
)
return
result
;
// shrink by device pixel ratio.
// shrink by device pixel ratio.
final
Size
candidateSize
=
paintBounds
.
size
/
ui
.
window
.
devicePixelRatio
;
final
Size
candidateSize
=
paintBounds
.
size
/
ui
.
window
.
devicePixelRatio
;
if
(
candidateSize
.
width
<
size
.
width
||
candidateSize
.
height
<
size
.
height
)
if
(
candidateSize
.
width
<
size
.
width
||
candidateSize
.
height
<
size
.
height
)
...
@@ -146,6 +161,8 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
...
@@ -146,6 +161,8 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
final
OffsetLayer
layer
=
renderView
.
layer
;
final
OffsetLayer
layer
=
renderView
.
layer
;
ui
.
Image
image
;
ui
.
Image
image
;
final
ByteData
byteData
=
await
tester
.
binding
.
runAsync
<
ByteData
>(()
async
{
final
ByteData
byteData
=
await
tester
.
binding
.
runAsync
<
ByteData
>(()
async
{
// Needs to be the same pixel ratio otherwise our dimensions won't match the
// last transform layer.
image
=
await
layer
.
toImage
(
renderView
.
paintBounds
,
pixelRatio:
1.0
);
image
=
await
layer
.
toImage
(
renderView
.
paintBounds
,
pixelRatio:
1.0
);
return
image
.
toByteData
();
return
image
.
toByteData
();
});
});
...
@@ -168,7 +185,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
...
@@ -168,7 +185,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
double
fontSize
;
double
fontSize
;
bool
isBold
;
bool
isBold
;
final
String
text
=
(
data
.
label
?.
isEmpty
==
true
)
?
data
.
value
:
data
.
label
;
final
String
text
=
(
data
.
label
?.
isEmpty
==
true
)
?
data
.
value
:
data
.
label
;
final
List
<
Element
>
elements
=
find
.
text
(
text
).
evaluate
().
toList
();
final
List
<
Element
>
elements
=
find
.
text
(
text
).
hitTestable
().
evaluate
().
toList
();
if
(
elements
.
length
==
1
)
{
if
(
elements
.
length
==
1
)
{
final
Element
element
=
elements
.
single
;
final
Element
element
=
elements
.
single
;
final
Widget
widget
=
element
.
widget
;
final
Widget
widget
=
element
.
widget
;
...
@@ -186,28 +203,38 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
...
@@ -186,28 +203,38 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
assert
(
false
);
assert
(
false
);
}
}
}
else
if
(
elements
.
length
>
1
)
{
}
else
if
(
elements
.
length
>
1
)
{
return
const
Evaluation
.
fail
(
'Multiple nodes with the same label
'
);
return
new
Evaluation
.
fail
(
'Multiple nodes with the same label:
${data.label}
\n
'
);
}
else
{
}
else
{
// If we can't find the text node
, then look up the default tex
t
// If we can't find the text node
then assume the label does no
t
fontSize
=
12.0
;
// correspond to actual text.
isBold
=
false
;
return
result
;
}
}
// Transform local coordinate to screen coordinates.
// Transform local coordinate to screen coordinates.
Rect
paintBounds
=
node
.
rect
;
Rect
paintBounds
=
node
.
rect
;
SemanticsNode
current
=
node
;
SemanticsNode
current
=
node
;
while
(
current
!=
null
)
{
while
(
current
!=
null
&&
current
.
parent
!=
null
)
{
if
(
current
.
transform
!=
null
)
if
(
current
.
transform
!=
null
)
paintBounds
=
MatrixUtils
.
transformRect
(
current
.
transform
,
paintBounds
);
paintBounds
=
MatrixUtils
.
transformRect
(
current
.
transform
,
paintBounds
);
paintBounds
=
paintBounds
.
shift
(
current
.
parent
?.
rect
?.
topLeft
??
Offset
.
zero
);
paintBounds
=
paintBounds
.
shift
(
current
.
parent
?.
rect
?.
topLeft
??
Offset
.
zero
);
current
=
current
.
parent
;
current
=
current
.
parent
;
}
}
if
(
_isNodeOffScreen
(
paintBounds
))
return
result
;
final
List
<
int
>
subset
=
_subsetToRect
(
byteData
,
paintBounds
,
image
.
width
,
image
.
height
);
final
List
<
int
>
subset
=
_subsetToRect
(
byteData
,
paintBounds
,
image
.
width
,
image
.
height
);
// Node was too far off screen.
if
(
subset
.
isEmpty
)
return
result
;
final
_ContrastReport
report
=
new
_ContrastReport
(
subset
);
final
_ContrastReport
report
=
new
_ContrastReport
(
subset
);
final
double
contrastRatio
=
report
.
contrastRatio
();
final
double
contrastRatio
=
report
.
contrastRatio
();
final
double
targetContrastRatio
=
(
isBold
&&
fontSize
>
kBoldTextMinimumSize
)
?
const
double
delta
=
-
0.01
;
kMinimumRatioLargeText
:
kMinimumRatioNormalText
;
double
targetContrastRatio
;
if
(
contrastRatio
>=
targetContrastRatio
)
if
((
isBold
&&
fontSize
>
kBoldTextMinimumSize
)
||
(
fontSize
??
12.0
)
>
kLargeTextMinimumSize
)
{
targetContrastRatio
=
kMinimumRatioLargeText
;
}
else
{
targetContrastRatio
=
kMinimumRatioNormalText
;
}
if
(
contrastRatio
-
targetContrastRatio
>=
delta
)
return
result
+
const
Evaluation
.
pass
();
return
result
+
const
Evaluation
.
pass
();
return
result
+
new
Evaluation
.
fail
(
return
result
+
new
Evaluation
.
fail
(
'
$node
:
\n
Expected contrast ratio of at least '
'
$node
:
\n
Expected contrast ratio of at least '
...
@@ -229,6 +256,17 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
...
@@ -229,6 +256,17 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
return
false
;
return
false
;
}
}
// Returns a rect that is entirely on screen, or null if it is too far off.
//
// Given an 1800 * 2400 pixel buffer, can we actually get all the data from
// this node? allow a small delta overlap before culling the node.
bool
_isNodeOffScreen
(
Rect
paintBounds
)
{
return
paintBounds
.
top
<
-
50.0
||
paintBounds
.
left
<
-
50.0
||
paintBounds
.
bottom
>
2400.0
+
50.0
||
paintBounds
.
right
>
1800.0
+
50.0
;
}
List
<
int
>
_subsetToRect
(
ByteData
data
,
Rect
paintBounds
,
int
width
,
int
height
)
{
List
<
int
>
_subsetToRect
(
ByteData
data
,
Rect
paintBounds
,
int
width
,
int
height
)
{
final
int
newWidth
=
paintBounds
.
size
.
width
.
ceil
();
final
int
newWidth
=
paintBounds
.
size
.
width
.
ceil
();
final
int
newHeight
=
paintBounds
.
size
.
height
.
ceil
();
final
int
newHeight
=
paintBounds
.
size
.
height
.
ceil
();
...
@@ -241,8 +279,8 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
...
@@ -241,8 +279,8 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
// Data is stored in row major order.
// Data is stored in row major order.
for
(
int
i
=
0
;
i
<
data
.
lengthInBytes
;
i
+=
4
)
{
for
(
int
i
=
0
;
i
<
data
.
lengthInBytes
;
i
+=
4
)
{
final
int
index
=
i
~/
4
;
final
int
index
=
i
~/
4
;
final
int
d
y
=
index
%
width
;
final
int
d
x
=
index
%
width
;
final
int
d
x
=
index
~/
width
;
final
int
d
y
=
index
~/
width
;
if
(
dx
>=
leftX
&&
dx
<=
rightX
&&
dy
>=
topY
&&
dy
<=
bottomY
)
{
if
(
dx
>=
leftX
&&
dx
<=
rightX
&&
dy
>=
topY
&&
dy
<=
bottomY
)
{
final
int
r
=
data
.
getUint8
(
i
);
final
int
r
=
data
.
getUint8
(
i
);
final
int
g
=
data
.
getUint8
(
i
+
1
);
final
int
g
=
data
.
getUint8
(
i
+
1
);
...
@@ -271,19 +309,15 @@ class _ContrastReport {
...
@@ -271,19 +309,15 @@ class _ContrastReport {
final
Color
hslColor
=
new
Color
(
colorHistogram
.
keys
.
first
);
final
Color
hslColor
=
new
Color
(
colorHistogram
.
keys
.
first
);
return
new
_ContrastReport
.
_
(
hslColor
,
hslColor
);
return
new
_ContrastReport
.
_
(
hslColor
,
hslColor
);
}
}
if
(
colorHistogram
.
length
==
2
)
{
final
Color
firstColor
=
new
Color
(
colorHistogram
.
keys
.
first
);
final
Color
lastColor
=
new
Color
(
colorHistogram
.
keys
.
last
);
if
(
firstColor
.
computeLuminance
()
<
lastColor
.
computeLuminance
())
{
return
new
_ContrastReport
.
_
(
lastColor
,
firstColor
);
}
return
new
_ContrastReport
.
_
(
firstColor
,
lastColor
);
}
// to determine the lighter and darker color, partition the colors
// to determine the lighter and darker color, partition the colors
// by lightness and then choose the mode from each group.
// by lightness and then choose the mode from each group.
final
double
averageLightness
=
colorHistogram
.
keys
.
fold
(
0.0
,
(
double
total
,
int
color
)
{
double
averageLightness
=
0.0
;
return
total
+
new
HSLColor
.
fromColor
(
new
Color
(
color
)).
lightness
;
for
(
int
color
in
colorHistogram
.
keys
)
{
})
/
colorHistogram
.
length
;
final
HSLColor
hslColor
=
new
HSLColor
.
fromColor
(
new
Color
(
color
));
averageLightness
+=
hslColor
.
lightness
*
colorHistogram
[
color
];
}
averageLightness
/=
colors
.
length
;
assert
(
averageLightness
!=
double
.
nan
);
int
lightColor
=
0
;
int
lightColor
=
0
;
int
darkColor
=
0
;
int
darkColor
=
0
;
int
lightCount
=
0
;
int
lightCount
=
0
;
...
@@ -292,14 +326,15 @@ class _ContrastReport {
...
@@ -292,14 +326,15 @@ class _ContrastReport {
for
(
MapEntry
<
int
,
int
>
entry
in
colorHistogram
.
entries
)
{
for
(
MapEntry
<
int
,
int
>
entry
in
colorHistogram
.
entries
)
{
final
HSLColor
color
=
new
HSLColor
.
fromColor
(
new
Color
(
entry
.
key
));
final
HSLColor
color
=
new
HSLColor
.
fromColor
(
new
Color
(
entry
.
key
));
final
int
count
=
entry
.
value
;
final
int
count
=
entry
.
value
;
if
(
color
.
lightness
<=
averageLightness
&&
count
>
light
Count
)
{
if
(
color
.
lightness
<=
averageLightness
&&
count
>
dark
Count
)
{
darkColor
=
entry
.
key
;
darkColor
=
entry
.
key
;
darkCount
=
count
;
darkCount
=
count
;
}
else
if
(
color
.
lightness
>
averageLightness
&&
count
>
dark
Count
)
{
}
else
if
(
color
.
lightness
>
averageLightness
&&
count
>
light
Count
)
{
lightColor
=
entry
.
key
;
lightColor
=
entry
.
key
;
lightCount
=
count
;
lightCount
=
count
;
}
}
}
}
assert
(
lightColor
!=
0
&&
darkColor
!=
0
);
return
new
_ContrastReport
.
_
(
new
Color
(
lightColor
),
new
Color
(
darkColor
));
return
new
_ContrastReport
.
_
(
new
Color
(
lightColor
),
new
Color
(
darkColor
));
}
}
...
...
packages/flutter_test/test/accessibility_test.dart
View file @
fa0a857d
...
@@ -103,7 +103,7 @@ void main() {
...
@@ -103,7 +103,7 @@ void main() {
handle
.
dispose
();
handle
.
dispose
();
});
});
testWidgets
(
'
grey text on white
background fails with correct message'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'
yellow text on yellow
background fails with correct message'
,
(
WidgetTester
tester
)
async
{
final
SemanticsHandle
handle
=
tester
.
ensureSemantics
();
final
SemanticsHandle
handle
=
tester
.
ensureSemantics
();
await
tester
.
pumpWidget
(
_boilerplate
(
await
tester
.
pumpWidget
(
_boilerplate
(
new
Container
(
new
Container
(
...
@@ -121,12 +121,57 @@ void main() {
...
@@ -121,12 +121,57 @@ void main() {
expect
(
result
.
reason
,
expect
(
result
.
reason
,
'SemanticsNode#21(Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), label: "this is a test",'
'SemanticsNode#21(Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), label: "this is a test",'
' textDirection: ltr):
\n
Expected contrast ratio of at least '
' textDirection: ltr):
\n
Expected contrast ratio of at least '
'4.5 but found
1.17
for a font size of 14.0. '
'4.5 but found
0.88
for a font size of 14.0. '
'The computed foreground color was: Color(0xfff
afafa
), '
'The computed foreground color was: Color(0xfff
feb3b
), '
'The computed background color was: Color(0xffff
eb3b
)
\n
'
'The computed background color was: Color(0xffff
ff00
)
\n
'
'See also: https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html'
);
'See also: https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html'
);
handle
.
dispose
();
handle
.
dispose
();
});
});
testWidgets
(
'label without corresponding text is skipped'
,
(
WidgetTester
tester
)
async
{
final
SemanticsHandle
handle
=
tester
.
ensureSemantics
();
await
tester
.
pumpWidget
(
_boilerplate
(
new
Semantics
(
label:
'This is not text'
,
container:
true
,
child:
new
Container
(
width:
200.0
,
height:
200.0
,
child:
const
Placeholder
(),
),
),
));
final
Evaluation
result
=
await
textContrastGuideline
.
evaluate
(
tester
);
expect
(
result
.
passed
,
true
);
handle
.
dispose
();
});
testWidgets
(
'offscreen text is skipped'
,
(
WidgetTester
tester
)
async
{
final
SemanticsHandle
handle
=
tester
.
ensureSemantics
();
await
tester
.
pumpWidget
(
_boilerplate
(
new
Stack
(
children:
<
Widget
>[
new
Positioned
(
left:
-
300.0
,
child:
new
Container
(
width:
200.0
,
height:
200.0
,
color:
Colors
.
yellow
,
child:
const
Text
(
'this is a test'
,
style:
TextStyle
(
fontSize:
14.0
,
color:
Colors
.
yellowAccent
),
),
),
)
],
)
));
final
Evaluation
result
=
await
textContrastGuideline
.
evaluate
(
tester
);
expect
(
result
.
passed
,
true
);
handle
.
dispose
();
});
});
});
group
(
'tap target size guideline'
,
()
{
group
(
'tap target size guideline'
,
()
{
...
@@ -207,11 +252,114 @@ void main() {
...
@@ -207,11 +252,114 @@ void main() {
final
Evaluation
result
=
await
androidTapTargetGuideline
.
evaluate
(
tester
);
final
Evaluation
result
=
await
androidTapTargetGuideline
.
evaluate
(
tester
);
expect
(
result
.
passed
,
false
);
expect
(
result
.
passed
,
false
);
expect
(
result
.
reason
,
expect
(
result
.
reason
,
'SemanticsNode#
36
(Rect.fromLTRB(376.0, 276.5, 424.0, 323.5), actions: [tap]): expected tap '
'SemanticsNode#
41
(Rect.fromLTRB(376.0, 276.5, 424.0, 323.5), actions: [tap]): expected tap '
'target size of at least Size(48.0, 48.0), but found Size(48.0, 47.0)
\n
'
'target size of at least Size(48.0, 48.0), but found Size(48.0, 47.0)
\n
'
'See also: https://support.google.com/accessibility/android/answer/7101858?hl=en'
);
'See also: https://support.google.com/accessibility/android/answer/7101858?hl=en'
);
handle
.
dispose
();
handle
.
dispose
();
});
});
testWidgets
(
'Box that overlaps edge of window is skipped'
,
(
WidgetTester
tester
)
async
{
final
SemanticsHandle
handle
=
tester
.
ensureSemantics
();
final
Widget
smallBox
=
new
SizedBox
(
width:
48.0
,
height:
47.0
,
child:
new
GestureDetector
(
onTap:
()
{},
),
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
Stack
(
children:
<
Widget
>[
new
Positioned
(
left:
0.0
,
top:
-
1.0
,
child:
smallBox
,
),
],
),
),
);
final
Evaluation
overlappingTopResult
=
await
androidTapTargetGuideline
.
evaluate
(
tester
);
expect
(
overlappingTopResult
.
passed
,
true
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
Stack
(
children:
<
Widget
>[
new
Positioned
(
left:
-
1.0
,
top:
0.0
,
child:
smallBox
,
),
],
),
),
);
final
Evaluation
overlappingLeftResult
=
await
androidTapTargetGuideline
.
evaluate
(
tester
);
expect
(
overlappingLeftResult
.
passed
,
true
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
Stack
(
children:
<
Widget
>[
new
Positioned
(
bottom:
-
1.0
,
child:
smallBox
,
),
],
),
),
);
final
Evaluation
overlappingBottomResult
=
await
androidTapTargetGuideline
.
evaluate
(
tester
);
expect
(
overlappingBottomResult
.
passed
,
true
);
await
tester
.
pumpWidget
(
new
MaterialApp
(
home:
new
Stack
(
children:
<
Widget
>[
new
Positioned
(
right:
-
1.0
,
child:
smallBox
,
),
],
),
),
);
final
Evaluation
overlappingRightResult
=
await
androidTapTargetGuideline
.
evaluate
(
tester
);
expect
(
overlappingRightResult
.
passed
,
true
);
handle
.
dispose
();
});
testWidgets
(
'Does not fail on mergedIntoParent child'
,
(
WidgetTester
tester
)
async
{
final
SemanticsHandle
handle
=
tester
.
ensureSemantics
();
await
tester
.
pumpWidget
(
_boilerplate
(
new
MergeSemantics
(
child:
new
Semantics
(
container:
true
,
child:
new
SizedBox
(
width:
50.0
,
height:
50.0
,
child:
new
Semantics
(
container:
true
,
child:
new
GestureDetector
(
onTap:
()
{},
child:
const
SizedBox
(
width:
4.0
,
height:
4.0
),
)
)
),
)
)
));
final
Evaluation
overlappingRightResult
=
await
androidTapTargetGuideline
.
evaluate
(
tester
);
expect
(
overlappingRightResult
.
passed
,
true
);
handle
.
dispose
();
});
});
});
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment