Repository: nvaccess/nvda

Issue:
Incorrect reading order in web navigation with latest alpha versions
### Steps to reproduce:

1. Visit, for example, [this Facebook page](https://www.facebook.com/AnnabelleandAiden/);
2. scroll to posts section for headings;
3. listen (with patience) the reading of first post (that has article role, important aspect? I think so).

### Actual behavior:

NVDA reads time, content, reactions, comments (article content), and finally the heading where you are, and under which the content is.

### Expected behavior:

I don't know if NVDA should read the article content, but surely the heading should be at the beginning, not at the end.

### System configuration
#### NVDA installed/portable/running from source:

Installed.

#### NVDA version:

NVDA version alpha-23598,a4524704.

#### Windows version:

Windows 10 20H2 (build SO 19042.1165)

#### Name and version of other software in use when reproducing the issue:

Mozilla Firefox 91.0.1, Google Chrome 92.0.4515.159.

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your computer?

Yes.

#### Have you tried any other versions of NVDA? If so, please report their behaviors.

NVDA 2021.2beta1, no problems.

#### If add-ons are disabled, is your problem still occurring?

Yes.

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?

Yes.



Discussion:

Resolving patch:
diff --git a/source/speech/speech.py b/source/speech/speech.py
--- a/source/speech/speech.py
+++ b/source/speech/speech.py
@@ -1784,6 +1784,20 @@ def getControlFieldSpeech(  # noqa: C901
 	description: Optional[str] = None
 	_descriptionFrom = attrs.get('_description-from', controlTypes.DescriptionFrom.UNKNOWN)
 	_descriptionIsContent: bool = attrs.get("descriptionIsContent", False)
+	_reportDescriptionAsAnnotation: bool = (
+		# Don't report other sources of description like "title" all the time
+		# The usages of these is not consistent and often does not seem to have
+		# Screen Reader users in mind
+		config.conf["annotations"]["reportAriaDescription"]
+		and not _descriptionIsContent
+		and controlTypes.DescriptionFrom.ARIA_DESCRIPTION == _descriptionFrom
+		and reason in (
+			OutputReason.FOCUS,
+			OutputReason.QUICKNAV,
+			OutputReason.CARET,
+			OutputReason.SAYALL,
+		)
+	)
 	if (
 		(
 			config.conf["presentation"]["reportObjectDescriptions"]
@@ -1795,19 +1809,7 @@ def getControlFieldSpeech(  # noqa: C901
 			# Not used internally, but may be used by addons.
 			attrs.get('alwaysReportDescription', False)
 		)
-		or (
-			# Don't report other sources of description like "title" all the time
-			# The usages of these is not consistent and often does not seem to have
-			# Screen Reader users in mind
-			config.conf["annotations"]["reportAriaDescription"]
-			and not _descriptionIsContent
-			and controlTypes.DescriptionFrom.ARIA_DESCRIPTION == _descriptionFrom
-			and reason in (
-				OutputReason.FOCUS,
-				OutputReason.CARET,
-				OutputReason.SAYALL,
-			)
-		)
+		or _reportDescriptionAsAnnotation
 	):
 		description = attrs.get('description')
 
@@ -2024,7 +2026,7 @@ def getControlFieldSpeech(  # noqa: C901
 		out = []
 		if isCurrent != controlTypes.IsCurrent.NO:
 			out.extend(isCurrentSequence)
-		if descriptionSequence:
+		if descriptionSequence and _reportDescriptionAsAnnotation:
 			out.extend(descriptionSequence)
 		# Speak expanded / collapsed / level for treeview items (in ARIA treegrids)
 		if role == controlTypes.Role.TREEVIEWITEM:


Test patch:
diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py
--- a/tests/system/robot/chromeTests.py
+++ b/tests/system/robot/chromeTests.py
@@ -1133,3 +1133,199 @@ def test_ensureNoBrowseModeDescription():
 		]),
 		message="Test focus mode with reportObjectDescriptions=False"
 	)
+
+
+def test_quickNavTargetReporting():
+	"""
+	When using quickNav, the target object should be spoken first, inner context should be given before outer
+	context.
+	"""
+	spy = _NvdaLib.getSpyLib()
+	REPORT_ARTICLES = ["documentFormatting", "reportArticles"]
+	spy.set_configValue(REPORT_ARTICLES, False)
+
+	_chrome.prepareChrome(
+		"""
+		<div
+			aria-describedby="descId"
+			aria-labelledby="labelId"
+			role="article"
+		>
+			<h1>Quick Nav Target</h1>
+			<div id="labelId">
+					<div>Some name.</div>
+			</div>
+			<div id="descId">
+					<span>A bunch of text.</span>
+			</div>
+		</div>
+		"""
+	)
+	# Quick nav to heading
+	actualSpeech = _chrome.getSpeechAfterKey("h")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"Quick Nav Target",  # Heading content (quick nav target), should read first
+			"heading",  # Heading role
+			"level 1",  # Heading level
+		])
+	)
+	# Reset to allow trying again with report articles enabled
+	actualSpeech = _chrome.getSpeechAfterKey("control+home")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"Before Test Case Marker",
+		])
+	)
+
+	# Quick nav to heading with report articles enabled
+	spy.set_configValue(REPORT_ARTICLES, True)
+	actualSpeech = _chrome.getSpeechAfterKey("h")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"Quick Nav Target",  # Heading content (quick nav target), should read first
+			"heading",  # Heading role
+			"level 1",  # Heading level
+			"article",  # article role, enabled via report article
+			"A bunch of text.",  # article (ancestor) description
+		])
+	)
+
+
+def test_focusTargetReporting():
+	"""
+	When moving focus the target object should be spoken first, inner context should be given before outer
+	context.
+	"""
+	spy = _NvdaLib.getSpyLib()
+	REPORT_ARTICLES = ["documentFormatting", "reportArticles"]
+	spy.set_configValue(REPORT_ARTICLES, False)
+
+	_chrome.prepareChrome(
+		"""
+		<a href="#">before Target</a>
+		<div
+			aria-describedby="descId"
+			aria-labelledby="labelId"
+			role="article"
+		>
+			<a href="#">Focus Target</a>
+			<div id="labelId">
+					<div>Some name.</div>
+			</div>
+			<div id="descId">
+					<span>A bunch of text.</span>
+			</div>
+		</div>
+		"""
+	)
+	# Set focus
+	actualSpeech = _chrome.getSpeechAfterKey("tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"before Target",
+			"link",
+		])
+	)
+
+	# Focus the link
+	actualSpeech = _chrome.getSpeechAfterKey("tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"Focus Target",  # link content (focus target), should read first
+			"link",  # link role
+		]),
+		message="browse mode - focus with Report Articles disabled"
+	)
+	# Reset to allow trying again with report articles enabled
+	actualSpeech = _chrome.getSpeechAfterKey("shift+tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"before Target",
+			"link",
+		])
+	)
+
+	# Focus the link with report articles enabled
+	spy.set_configValue(REPORT_ARTICLES, True)
+	actualSpeech = _chrome.getSpeechAfterKey("tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"Focus Target",  # link content (focus target), should read first
+			"link",  # link role
+			"article",  # article role, enabled via report article
+			"A bunch of text.",  # article (ancestor) description
+		]),
+		message="browse mode - focus with Report Articles enabled"
+	)
+
+	# Reset to allow trying again in focus mode
+	actualSpeech = _chrome.getSpeechAfterKey("shift+tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"before Target",
+			"link",
+		])
+	)
+
+	# Force focus mode
+	actualSpeech = _chrome.getSpeechAfterKey("NVDA+space")
+	_asserts.strings_match(
+		actualSpeech,
+		"Focus mode"
+	)
+
+	spy.set_configValue(REPORT_ARTICLES, False)
+	# Focus the link
+	actualSpeech = _chrome.getSpeechAfterKey("tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_CALL_SEP.join([
+			SPEECH_SEP.join([
+				"Some name.",  # name for article
+				"article",  # article role, enabled via report article
+				"A bunch of text.",  # description for article
+			]),
+			SPEECH_SEP.join([
+				"Focus Target",  # link content (focus target), should read first
+				"link",  # link role
+			]),
+		]),
+		message="focus mode - focus with Report Articles disabled"
+	)
+	# Reset to allow trying again with report articles enabled
+	actualSpeech = _chrome.getSpeechAfterKey("shift+tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_SEP.join([
+			"before Target",
+			"link",
+		])
+	)
+
+	# Focus the link with report articles enabled
+	spy.set_configValue(REPORT_ARTICLES, True)
+	actualSpeech = _chrome.getSpeechAfterKey("tab")
+	_asserts.strings_match(
+		actualSpeech,
+		SPEECH_CALL_SEP.join([
+			SPEECH_SEP.join([
+				"Some name.",  # name for article
+				"article",  # article role, enabled via report article
+				"A bunch of text.",  # description for article
+			]),
+			SPEECH_SEP.join([
+				"Focus Target",  # link content (focus target), should read first
+				"link",  # link role
+			]),
+		]),
+		message="focus mode - focus with Report Articles enabled"
+	)
diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot
--- a/tests/system/robot/chromeTests.robot
+++ b/tests/system/robot/chromeTests.robot
@@ -101,4 +101,9 @@ Prevent Duplicate Speech From Description while in Browse mode with tab nav
 Only report description in focus mode due to reportObjectDescriptions
 	[Documentation]	The term object in reportObjectDescriptions (essentially) means focus mode.
 	test_ensureNoBrowseModeDescription
-
+Quick Nav reports target first
+	[Documentation]	Quick Nav target should always be reported before ancestors. Ancestors should be reported from inner to outer.
+	test_quickNavTargetReporting
+Focus reports target first
+	[Documentation]	Focus target should always be reported before ancestors. Ancestors should be reported from inner to outer.
+	test_focusTargetReporting