Repository: nvaccess/nvda

Issue:
treeInterceptor.documentConstantIdentifier doesn't work in Chrome v100
<!-- Please read the text in this edit field before filling it in.
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/blob/master/devDocs/githubIssueTemplateExplanationAndExamples.md
Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. 

Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question.
-->
### Description
This is mltony, author of BrowserNav add-on. Certain functions stopped working in Chrom v100. In particular, I was using treeInterceptor.documentConstantIdentifier property to retrieve URL of current page and it no longer seems to work on some websites in Chrome v100.
I can't figure out what's the pattern, e.g. amazon.com fails, while google .com seems to work fine.
### Steps to reproduce:
1. Open Google Chrome v100.
2. Navigate to Amazon.com
3. Open NVDA Python console by pressing NVDA+Control+Z
4. Type: `focus.treeInterceptor.documentConstantIdentifier`
### Actual behavior:
Returns empty string.
### Expected behavior:
Should return URL, something like `https://www.amazon.com/`.
### System configuration
#### NVDA installed/portable/running from source:
Installed
#### NVDA version:
2021.3
#### Windows version:
Windows 10
#### Name and version of other software in use when reproducing the issue:
Google Chrome Version 100.0.4896.60 (Official Build) (64-bit)
Used to work fine in Chrome before v100. Also works fine in Firefox and Edge.
#### 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.
Yes, reproduces in 2022.1beta3
#### If NVDA 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:
@mltony Guess this is similar to #13571 which I created after your report.
@mltony The same or similar issues apparently affect NVDA's ability to not report the page title upon load and break the auto-SayAll function on most, if not all, pages.
cc: @feerrenrut, @ObjectInSpace, @aleventhal
I don't see anything actionable on the Chrome side yet. LMK if there is a bug in one of our APIs.

Resolving patch:
diff --git a/source/browseMode.py b/source/browseMode.py
--- a/source/browseMode.py
+++ b/source/browseMode.py
@@ -1257,7 +1257,9 @@ class BrowseModeDocumentTreeInterceptor(documentBase.DocumentWithTableNavigation
 	def __init__(self,obj):
 		super(BrowseModeDocumentTreeInterceptor,self).__init__(obj)
 		self._lastProgrammaticScrollTime = None
-		self.documentConstantIdentifier = self.documentConstantIdentifier
+		# Cache the document constant identifier so it can be saved with the last caret position on termination.
+		# As the original property may not be available as the document will be already dead.
+		self._lastCachedDocumentConstantIdentifier: Optional[str] = self.documentConstantIdentifier
 		self._lastFocusObj = None
 		self._objPendingFocusBeforeActivate = None
 		self._hadFirstGainFocus = False
@@ -1271,8 +1273,11 @@ def __init__(self,obj):
 
 	def terminate(self):
 		if self.shouldRememberCaretPositionAcrossLoads and self._lastCaretPosition:
+			docID = self._lastCachedDocumentConstantIdentifier
+			lastCaretPos = self._lastCaretPosition
+			log.debug(f"Saving caret position {lastCaretPos} for document at {docID}")
 			try:
-				self.rootNVDAObject.appModule._browseModeRememberedCaretPositions[self.documentConstantIdentifier] = self._lastCaretPosition
+				self.rootNVDAObject.appModule._browseModeRememberedCaretPositions[docID] = lastCaretPos
 			except AttributeError:
 				# The app module died.
 				pass
@@ -1292,6 +1297,12 @@ def event_treeInterceptor_gainFocus(self):
 				# We only set the caret position if in browse mode.
 				# If in focus mode, the document must have forced the focus somewhere,
 				# so we don't want to override it.
+				# Update the cached document constant identifier
+				# so it can be saved with the last caret position on termination.
+				# As the original property may not be available as the document will be already dead.
+				# Updating here is necessary as the identifier could have dynamically changed since initial load,
+				# such as with  a SPA (single page app)
+				self._lastCachedDocumentConstantIdentifier = self.documentConstantIdentifier
 				initialPos = self._getInitialCaretPos()
 				if initialPos:
 					self.selection = self.makeTextInfo(initialPos)
@@ -1358,6 +1369,14 @@ def _set_selection(self, info, reason=OutputReason.CARET):
 		caret = info.copy()
 		caret.collapse()
 		self._lastCaretPosition = caret.bookmark
+		docID = self.documentConstantIdentifier
+		if docID:
+			# Update the cached document constant identifier
+			# so it can be saved with the last caret position on termination.
+			# As the original property may not be available as the document will be already dead.
+			# Updating here is necessary as the identifier could have dynamically changed since initial load,
+			# such as with  a SPA (single page app)
+			self._lastCachedDocumentConstantIdentifier = self.documentConstantIdentifier
 		review.handleCaretMove(caret)
 		if reason == OutputReason.FOCUS:
 			self._lastCaretMoveWasFocus = True
@@ -1778,10 +1797,19 @@ def doResult(result):
 			obj = container
 		return doResult(False)
 
-	def _get_documentConstantIdentifier(self):
+	documentConstantIdentifier: Optional[str]
+	""" Typing information for auto-property: _get_documentConstantIdentifier"""
+
+	# Mark documentConstantIdentifier property for caching during the current core cycle
+	_cache_documentConstantIdentifer = True
+
+	def _get_documentConstantIdentifier(self) -> Optional[str]:
 		"""Get the constant identifier for this document.
 		This identifier should uniquely identify all instances (not just one instance) of a document for at least the current session of the hosting application.
 		Generally, the document URL should be used.
+		Although the name of this property suggests that the identifier will be constant,
+		With the introduction of SPAs (single page apps) the URL of a page may dynamically change over time.
+		this property should reflect the most up to date URL.
 		@return: The constant identifier for this document, C{None} if there is none.
 		"""
 		return None
@@ -1794,7 +1822,7 @@ def _get_shouldRememberCaretPositionAcrossLoads(self):
 		@return: C{True} if the caret position should be remembered, C{False} if not.
 		@rtype: bool
 		"""
-		docConstId = self.documentConstantIdentifier
+		docConstId = self._lastCachedDocumentConstantIdentifier
 		# Return True if the URL indicates that this is probably a web browser document.
 		# We do this check because we don't want to remember caret positions for email messages, etc.
 		if isinstance(docConstId, str):
@@ -1812,11 +1840,14 @@ def _getInitialCaretPos(self):
 		@rtype: TextInfo position
 		"""
 		if self.shouldRememberCaretPositionAcrossLoads:
+			docID = self._lastCachedDocumentConstantIdentifier
 			try:
-				return self.rootNVDAObject.appModule._browseModeRememberedCaretPositions[self.documentConstantIdentifier]
+				caretPos = self.rootNVDAObject.appModule._browseModeRememberedCaretPositions[docID]
 			except KeyError:
-				pass
-		return None
+				log.debug(f"No saved caret position for {docID}")
+				return None
+			log.debug(f"Found saved caret pos {caretPos} for document {docID}")
+		return caretPos
 
 	def getEnclosingContainerRange(self, textRange):
 		textRange = textRange.copy()
diff --git a/source/virtualBuffers/__init__.py b/source/virtualBuffers/__init__.py
--- a/source/virtualBuffers/__init__.py
+++ b/source/virtualBuffers/__init__.py
@@ -477,6 +477,13 @@ def _loadBufferDone(self, success=True):
 		if not success:
 			self.passThrough=True
 			return
+		textLength = NVDAHelper.localLib.VBuf_getTextLength(self.VBufHandle)
+		if textLength == 0:
+			log.debugWarning("Empty buffer. Waiting for documentLoadComplete event instead")
+			# Empty buffer.
+			# May be due to focus event too early in Chromium 100 documents
+			# We may get a later chance to see content with a documentLoadComplete event
+			return
 		if self._hadFirstGainFocus:
 			# If this buffer has already had focus once while loaded, this is a refresh.
 			# Translators: Reported when a page reloads (example: after refreshing a webpage).
@@ -484,6 +491,15 @@ def _loadBufferDone(self, success=True):
 		if api.getFocusObject().treeInterceptor == self:
 			self.event_treeInterceptor_gainFocus()
 
+	def event_documentLoadComplete(self, obj, nextHandler):
+		if not self._hadFirstGainFocus:
+			# Any initial gainFocus events were too early to start reporting content in this buffer.
+			# Therefore as we are now alerted the document load is complete,
+			# We should handle the initial automatic say all etc.
+			if api.getFocusObject().treeInterceptor == self:
+				log.debug("Handling initial reporting of virtualBuffer via documentLoadComplete event")
+				self.event_treeInterceptor_gainFocus()
+
 	def _loadProgress(self):
 		# Translators: Reported while loading a document.
 		ui.message(_("Loading document..."))


Test patch: