[elements] Use lossless encoding for pseudo-element hide class names Encode characters outside [a-zA-Z0-9-] (including underscores) as hex escape sequences when building the helper class name in toggleClassAndInjectStyleRule. This prevents distinct pseudo-element names (such as non-ASCII identifiers of the same length or identifiers differing in escaped characters vs. underscores) from colliding on the same class name. Fixed: 570543683 Change-Id: If1273876ff63bea5c5289acf1bfc41f86f3eaa97 TAG=agy CONV=aa392134-a395-41af-85d6-48bab269fd1e Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/8548894 Commit-Queue: Yang Guo <yangguo@chromium.org> Reviewed-by: Mathias Bynens <mathias@chromium.org> Auto-Submit: Yang Guo <yangguo@chromium.org>
diff --git a/front_end/core/sdk/DOMModel.test.ts b/front_end/core/sdk/DOMModel.test.ts index be30d07..c762e88 100644 --- a/front_end/core/sdk/DOMModel.test.ts +++ b/front_end/core/sdk/DOMModel.test.ts
@@ -936,6 +936,128 @@ assert.deepEqual(callFunction.firstCall.args[1], [{value: '::view-transition-group(\\31 23\\.foo)'}, {value: true}]); }); + + it('generates distinct classes and style rules when hiding multiple pseudo elements', async function() { + const target = universe.createTarget(); + const model = target.model(SDK.DOMModel.DOMModel) as SDK.DOMModel.DOMModel; + const parentNode = SDK.DOMModel.DOMNode.create(model, null, false, { + nodeId: 1 as Protocol.DOM.NodeId, + backendNodeId: 1 as Protocol.DOM.BackendNodeId, + nodeType: NodeType.ELEMENT_NODE, + nodeName: 'DIV', + localName: 'div', + nodeValue: '', + pseudoElements: [ + { + nodeId: 2 as Protocol.DOM.NodeId, + backendNodeId: 2 as Protocol.DOM.BackendNodeId, + nodeType: NodeType.ELEMENT_NODE, + nodeName: '::view-transition-group', + localName: '::view-transition-group', + nodeValue: '', + pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup, + pseudoIdentifier: 'ヘッダー', + }, + { + nodeId: 3 as Protocol.DOM.NodeId, + backendNodeId: 3 as Protocol.DOM.BackendNodeId, + nodeType: NodeType.ELEMENT_NODE, + nodeName: '::view-transition-group', + localName: '::view-transition-group', + nodeValue: '', + pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup, + pseudoIdentifier: 'フッター', + }, + { + nodeId: 4 as Protocol.DOM.NodeId, + backendNodeId: 4 as Protocol.DOM.BackendNodeId, + nodeType: NodeType.ELEMENT_NODE, + nodeName: '::view-transition-group', + localName: '::view-transition-group', + nodeValue: '', + pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup, + pseudoIdentifier: 'a.b', + }, + { + nodeId: 5 as Protocol.DOM.NodeId, + backendNodeId: 5 as Protocol.DOM.BackendNodeId, + nodeType: NodeType.ELEMENT_NODE, + nodeName: '::view-transition-group', + localName: '::view-transition-group', + nodeValue: '', + pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup, + pseudoIdentifier: 'a b', + }, + { + nodeId: 6 as Protocol.DOM.NodeId, + backendNodeId: 6 as Protocol.DOM.BackendNodeId, + nodeType: NodeType.ELEMENT_NODE, + nodeName: '::view-transition-group', + localName: '::view-transition-group', + nodeValue: '', + pseudoType: ProtocolModule.DOM.PseudoType.ViewTransitionGroup, + pseudoIdentifier: 'a__b', + }, + ], + }); + const vtNodes = parentNode.pseudoElements().get(ProtocolModule.DOM.PseudoType.ViewTransitionGroup); + assert.exists(vtNodes); + assert.lengthOf(vtNodes, 5); + + const classes = new Set<string>(); + const styleClasses = new Set<string>(); + const style = { + textContent: '', + classList: { + contains: function(cls: string) { + return styleClasses.has(cls); + }, + add: function(cls: string) { + styleClasses.add(cls); + }, + }, + }; + const element = { + nodeType: 1, + parentNode: null, + classList: { + toggle: function(cls: string, force: boolean) { + if (force) { + classes.add(cls); + } else { + classes.delete(cls); + } + }, + }, + querySelector: function() { + return style; + }, + }; + const callFunction = sinon.stub().callsFake(function(fn: (this: unknown, ...args: unknown[]) => void, + args: Array<{value: unknown}>) { + fn.apply(element, args.map(function(a) { + return a.value; + })); + return Promise.resolve({}); + }); + const release = sinon.stub(); + sinon.stub(parentNode, 'resolveToObject').resolves({callFunction, + release} as unknown as SDK.RemoteObject.RemoteObject); + + for (const node of vtNodes) { + await node.toggleHideElement(); + } + + assert.strictEqual(classes.size, 5); + assert.include(style.textContent, '::view-transition-group(ヘッダー)'); + assert.include(style.textContent, '::view-transition-group(フッター)'); + assert.include(style.textContent, '::view-transition-group(a\\.b)'); + assert.include(style.textContent, '::view-transition-group(a\\ b)'); + assert.include(style.textContent, '::view-transition-group(a__b)'); + + await vtNodes[0].toggleHideElement(); + assert.strictEqual(classes.size, 4); + }); }); });
diff --git a/front_end/core/sdk/PageFunctions.ts b/front_end/core/sdk/PageFunctions.ts index 0c7d1d9..bfb203c 100644 --- a/front_end/core/sdk/PageFunctions.ts +++ b/front_end/core/sdk/PageFunctions.ts
@@ -109,7 +109,12 @@ const classNamePrefix = '__web-inspector-hide'; const classNameSuffix = '-shortcut__'; const styleTagId = '__web-inspector-hide-shortcut-style__'; - const pseudoElementNameEscaped = pseudoElementName ? pseudoElementName.replace(/[^a-zA-Z0-9_-]/g, '_') : ''; + let pseudoElementNameEscaped = ''; + if (pseudoElementName) { + pseudoElementNameEscaped = pseudoElementName.replace(/[^a-zA-Z0-9-]/g, function(c) { + return '_' + c.charCodeAt(0).toString(16) + '_'; + }); + } const className = classNamePrefix + pseudoElementNameEscaped + classNameSuffix; this.classList.toggle(className, hidden); @@ -117,7 +122,7 @@ while (localRoot.parentNode) { localRoot = (localRoot.parentNode as Element); } - if (localRoot.nodeType === Node.DOCUMENT_NODE) { + if (localRoot.nodeType === 9 /* Node.DOCUMENT_NODE */) { localRoot = document.head; }