diff --git a/src/server/supplements/injected/html.ts b/src/server/supplements/injected/html.ts
deleted file mode 100644
index bf5a0c3d7d..0000000000
--- a/src/server/supplements/injected/html.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-const templateCache = new Map();
-
-export interface Element$ extends HTMLElement {
- $(id: string): HTMLElement;
- $$(id: string): Iterable
-}
-
-const BOOLEAN_ATTRS = new Set([
- 'async', 'autofocus', 'autoplay', 'checked', 'contenteditable', 'controls',
- 'default', 'defer', 'disabled', 'expanded', 'formNoValidate', 'frameborder', 'hidden',
- 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate',
- 'open', 'readonly', 'required', 'reversed', 'scoped', 'selected', 'typemustmatch',
-]);
-
-type Sub = {
- node: Element,
- type?: string,
- nameParts?: string[],
- valueParts?: string[],
- isSimpleValue?: boolean,
- attr?: string,
- nodeIndex?: number
-};
-
-export function onDOMEvent(target: EventTarget, name: string, listener: (e: any) => void, capturing = false): () => void {
- target.addEventListener(name, listener, capturing);
- return () => {
- target.removeEventListener(name, listener, capturing);
- };
-}
-
-export function onDOMResize(target: HTMLElement, callback: () => void) {
- const resizeObserver = new (window as any).ResizeObserver(callback);
- resizeObserver.observe(target);
- return () => resizeObserver.disconnect();
-}
-
-export function html(strings: TemplateStringsArray, ...values: any): Element$ {
- let cache = templateCache.get(strings);
- if (!cache) {
- cache = prepareTemplate(strings);
- templateCache.set(strings, cache);
- }
- const node = renderTemplate(cache.template, cache.subs, values) as any;
- if (node.querySelector) {
- node.$ = node.querySelector.bind(node);
- node.$$ = node.querySelectorAll.bind(node);
- }
- return node;
-}
-
-const SPACE_REGEX = /^\s*\n\s*$/;
-const MARKER_REGEX = /---dom-template-\d+---/;
-
-function prepareTemplate(strings: TemplateStringsArray) {
- const template = document.createElement('template');
- let html = '';
- for (let i = 0; i < strings.length - 1; ++i) {
- html += strings[i];
- html += `---dom-template-${i}---`;
- }
- html += strings[strings.length - 1];
- template.innerHTML = html;
-
- const walker = template.ownerDocument.createTreeWalker(
- template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, false);
- const emptyTextNodes: Node[] = [];
- const subs: Sub[] = [];
- while (walker.nextNode()) {
- const node = walker.currentNode;
- if (node.nodeType === Node.ELEMENT_NODE && MARKER_REGEX.test((node as Element).tagName))
- throw new Error('Should not use a parameter as an html tag');
-
- if (node.nodeType === Node.ELEMENT_NODE && (node as Element).hasAttributes()) {
- const element = node as Element;
- for (let i = 0; i < element.attributes.length; i++) {
- const name = element.attributes[i].name;
-
- const nameParts = name.split(MARKER_REGEX);
- const valueParts = element.attributes[i].value.split(MARKER_REGEX);
- const isSimpleValue = valueParts.length === 2 && valueParts[0] === '' && valueParts[1] === '';
-
- if (nameParts.length > 1 || valueParts.length > 1)
- subs.push({ node: element, nameParts, valueParts, isSimpleValue, attr: name});
- }
- } else if (node.nodeType === Node.TEXT_NODE && MARKER_REGEX.test((node as Text).data)) {
- const text = node as Text;
- const texts = text.data.split(MARKER_REGEX);
- text.data = texts[0];
- const anchor = node.nextSibling;
- for (let i = 1; i < texts.length; ++i) {
- const span = document.createElement('span');
- node.parentNode!.insertBefore(span, anchor);
- node.parentNode!.insertBefore(document.createTextNode(texts[i]), anchor);
- subs.push({
- node: span,
- type: 'replace-node',
- });
- }
- if (shouldRemoveTextNode(text))
- emptyTextNodes.push(text);
- } else if (node.nodeType === Node.TEXT_NODE && shouldRemoveTextNode((node as Text))) {
- emptyTextNodes.push(node);
- }
- }
-
- for (const emptyTextNode of emptyTextNodes)
- (emptyTextNode as any).remove();
-
- const markedNodes = new Map();
- for (const sub of subs) {
- let index = markedNodes.get(sub.node);
- if (index === undefined) {
- index = markedNodes.size;
- sub.node.setAttribute('dom-template-marked', 'true');
- markedNodes.set(sub.node, index);
- }
- sub.nodeIndex = index;
- }
- return {template, subs};
-}
-
-function shouldRemoveTextNode(node: Text) {
- if (!node.previousSibling && !node.nextSibling)
- return !node.data.length;
- return (!node.previousSibling || node.previousSibling.nodeType === Node.ELEMENT_NODE) &&
- (!node.nextSibling || node.nextSibling.nodeType === Node.ELEMENT_NODE) &&
- (!node.data.length || SPACE_REGEX.test(node.data));
-}
-
-function renderTemplate(template: HTMLTemplateElement, subs: Sub[], values: (string | Node)[]): DocumentFragment | ChildNode {
- const content = template.ownerDocument.importNode(template.content, true)!;
- const boundElements = Array.from(content.querySelectorAll('[dom-template-marked]'));
- for (const node of boundElements)
- node.removeAttribute('dom-template-marked');
-
- let valueIndex = 0;
- const interpolateText = (texts: string[]) => {
- let newText = texts[0];
- for (let i = 1; i < texts.length; ++i) {
- newText += values[valueIndex++];
- newText += texts[i];
- }
- return newText;
- };
-
- for (const sub of subs) {
- const n = boundElements[sub.nodeIndex!];
- if (sub.attr) {
- n.removeAttribute(sub.attr);
- const name = interpolateText(sub.nameParts!);
- const value = sub.isSimpleValue ? values[valueIndex++] : interpolateText(sub.valueParts!);
- if (BOOLEAN_ATTRS.has(name))
- n.toggleAttribute(name, !!value);
- else
- n.setAttribute(name, String(value));
- } else if (sub.type === 'replace-node') {
- const replacement = values[valueIndex++];
- if (Array.isArray(replacement)) {
- const fragment = document.createDocumentFragment();
- for (const node of replacement)
- fragment.appendChild(node);
- n.replaceWith(fragment);
- } else if (replacement instanceof Node) {
- n.replaceWith(replacement);
- } else {
- n.replaceWith(document.createTextNode(replacement || ''));
- }
- }
- }
-
- return content.firstChild && content.firstChild === content.lastChild ? content.firstChild : content;
-}
-
-export function deepActiveElement() {
- let activeElement = document.activeElement;
- while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
- activeElement = activeElement.shadowRoot.activeElement;
- return activeElement;
-}
diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts
index b4f9298133..b93e51ac65 100644
--- a/src/server/supplements/injected/recorder.ts
+++ b/src/server/supplements/injected/recorder.ts
@@ -17,7 +17,6 @@
import type * as actions from '../recorder/recorderActions';
import type InjectedScript from '../../injected/injectedScript';
import { generateSelector, querySelector } from './selectorGenerator';
-import { html } from './html';
import type { Point } from '../../../common/types';
import type { UIState } from '../recorder/recorderTypes';
@@ -57,33 +56,30 @@ export class Recorder {
constructor(injectedScript: InjectedScript, params: { isUnderTest: boolean }) {
this._params = params;
this._injectedScript = injectedScript;
- this._outerGlassPaneElement = html`
-
- `;
+ this._outerGlassPaneElement = document.createElement('x-pw-glass');
+ this._outerGlassPaneElement.style.position = 'fixed';
+ this._outerGlassPaneElement.style.top = '0';
+ this._outerGlassPaneElement.style.right = '0';
+ this._outerGlassPaneElement.style.bottom = '0';
+ this._outerGlassPaneElement.style.left = '0';
+ this._outerGlassPaneElement.style.zIndex = '2147483647';
+ this._outerGlassPaneElement.style.pointerEvents = 'none';
+ this._outerGlassPaneElement.style.display = 'flex';
- this._tooltipElement = html``;
- this._actionPointElement = html``;
+ this._tooltipElement = document.createElement('x-pw-tooltip');
+ this._actionPointElement = document.createElement('x-pw-action-point');
+ this._actionPointElement.setAttribute('hidden', 'true');
- this._innerGlassPaneElement = html`
-
- ${this._tooltipElement}
- `;
+ this._innerGlassPaneElement = document.createElement('x-pw-glass-inner');
+ this._innerGlassPaneElement.style.flex = 'auto';
+ this._innerGlassPaneElement.appendChild(this._tooltipElement);
// Use a closed shadow root to prevent selectors matching our internal previews.
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: this._params.isUnderTest ? 'open' : 'closed' });
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
this._glassPaneShadow.appendChild(this._actionPointElement);
- this._glassPaneShadow.appendChild(html`
-
- `);
+ `;
+ this._glassPaneShadow.appendChild(styleElement);
+
this._refreshListenersIfNeeded();
setInterval(() => {
this._refreshListenersIfNeeded();
@@ -394,15 +391,13 @@ export class Recorder {
}
private _createHighlightElement(): HTMLElement {
- const highlightElement = html`
-
- `;
+ const highlightElement = document.createElement('x-pw-highlight');
+ highlightElement.style.position = 'absolute';
+ highlightElement.style.top = '0';
+ highlightElement.style.left = '0';
+ highlightElement.style.width = '0';
+ highlightElement.style.height = '0';
+ highlightElement.style.boxSizing = 'border-box';
this._glassPaneShadow.appendChild(highlightElement);
return highlightElement;
}
diff --git a/test/cli/cli-codegen-1.spec.ts b/test/cli/cli-codegen-1.spec.ts
index 9f1fa1f8d7..72a0a6e055 100644
--- a/test/cli/cli-codegen-1.spec.ts
+++ b/test/cli/cli-codegen-1.spec.ts
@@ -83,6 +83,43 @@ await page.ClickAsync("text=Submit");`);
expect(message.text()).toBe('click');
});
+ it('should work with TrustedTypes', async ({ page, recorder }) => {
+ await recorder.setContentAndWait(`
+
+
+
+
+
+ `);
+
+ const selector = await recorder.hoverOverElement('button');
+ expect(selector).toBe('text=Submit');
+
+ const [message, sources] = await Promise.all([
+ page.waitForEvent('console'),
+ recorder.waitForOutput('', 'click'),
+ page.dispatchEvent('button', 'click', { detail: 1 })
+ ]);
+
+ expect(sources.get('').text).toContain(`
+ // Click text=Submit
+ await page.click('text=Submit');`);
+
+ expect(sources.get('').text).toContain(`
+ # Click text=Submit
+ page.click("text=Submit")`);
+
+ expect(sources.get('').text).toContain(`
+ # Click text=Submit
+ await page.click("text=Submit")`);
+
+ expect(sources.get('').text).toContain(`
+// Click text=Submit
+await page.ClickAsync("text=Submit");`);
+
+ expect(message.text()).toBe('click');
+ });
+
it('should not target selector preview by text regexp', async ({ page, recorder }) => {
await recorder.setContentAndWait(`dummy`);