mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-14 21:53:35 +03:00
feat(snapshots): make cssom overrides efficient (#5218)
- Intercept CSSOM modifications and recalculate overridden css text. - When css text does not change, use "backwards reference" similar to node references. - Set 'Cache-Control: no-cache' for resources that could be overridden.
This commit is contained in:
parent
dbcdf9dcd7
commit
7fe7d0ef32
@ -17,7 +17,7 @@
|
||||
import * as http from 'http';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { TraceModel, trace } from './traceModel';
|
||||
import type { TraceModel, trace, ContextEntry } from './traceModel';
|
||||
import { TraceServer } from './traceServer';
|
||||
import { NodeSnapshot } from '../../trace/traceTypes';
|
||||
|
||||
@ -119,14 +119,23 @@ export class SnapshotServer {
|
||||
function serviceWorkerMain(self: any /* ServiceWorkerGlobalScope */) {
|
||||
let traceModel: TraceModel;
|
||||
|
||||
type ContextData = {
|
||||
resourcesByUrl: Map<string, trace.NetworkResourceTraceEvent[]>,
|
||||
overridenUrls: Set<string>
|
||||
};
|
||||
const contextToData = new Map<ContextEntry, ContextData>();
|
||||
|
||||
function preprocessModel() {
|
||||
for (const contextEntry of traceModel.contexts) {
|
||||
contextEntry.resourcesByUrl = new Map();
|
||||
const contextData: ContextData = {
|
||||
resourcesByUrl: new Map(),
|
||||
overridenUrls: new Set(),
|
||||
};
|
||||
const appendResource = (event: trace.NetworkResourceTraceEvent) => {
|
||||
let responseEvents = contextEntry.resourcesByUrl.get(event.url);
|
||||
let responseEvents = contextData.resourcesByUrl.get(event.url);
|
||||
if (!responseEvents) {
|
||||
responseEvents = [];
|
||||
contextEntry.resourcesByUrl.set(event.url, responseEvents);
|
||||
contextData.resourcesByUrl.set(event.url, responseEvents);
|
||||
}
|
||||
responseEvents.push(event);
|
||||
};
|
||||
@ -134,7 +143,14 @@ export class SnapshotServer {
|
||||
for (const action of pageEntry.actions)
|
||||
action.resources.forEach(appendResource);
|
||||
pageEntry.resources.forEach(appendResource);
|
||||
for (const snapshots of Object.values(pageEntry.snapshotsByFrameId)) {
|
||||
for (const snapshot of snapshots) {
|
||||
for (const { url } of snapshot.snapshot.resourceOverrides)
|
||||
contextData.overridenUrls.add(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
contextToData.set(contextEntry, contextData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,6 +265,23 @@ export class SnapshotServer {
|
||||
return html;
|
||||
}
|
||||
|
||||
function findResourceOverride(snapshots: trace.FrameSnapshotTraceEvent[], snapshotIndex: number, url: string): string | undefined {
|
||||
while (true) {
|
||||
const snapshot = snapshots[snapshotIndex].snapshot;
|
||||
const override = snapshot.resourceOverrides.find(o => o.url === url);
|
||||
if (!override)
|
||||
return;
|
||||
if (override.sha1 !== undefined)
|
||||
return override.sha1;
|
||||
if (override.ref === undefined)
|
||||
return;
|
||||
const referenceIndex = snapshotIndex - override.ref!;
|
||||
if (referenceIndex < 0 || referenceIndex >= snapshotIndex)
|
||||
return;
|
||||
snapshotIndex = referenceIndex;
|
||||
}
|
||||
}
|
||||
|
||||
async function doFetch(event: any /* FetchEvent */): Promise<Response> {
|
||||
try {
|
||||
const pathname = new URL(event.request.url).pathname;
|
||||
@ -278,6 +311,7 @@ export class SnapshotServer {
|
||||
}
|
||||
if (!contextEntry || !pageEntry)
|
||||
return request.mode === 'navigate' ? respondNotAvailable() : respond404();
|
||||
const contextData = contextToData.get(contextEntry)!;
|
||||
|
||||
const frameSnapshots = pageEntry.snapshotsByFrameId[parsed.frameId] || [];
|
||||
let snapshotIndex = -1;
|
||||
@ -304,7 +338,8 @@ export class SnapshotServer {
|
||||
}
|
||||
|
||||
let resource: trace.NetworkResourceTraceEvent | null = null;
|
||||
const resourcesWithUrl = contextEntry.resourcesByUrl.get(removeHash(request.url)) || [];
|
||||
const urlWithoutHash = removeHash(request.url);
|
||||
const resourcesWithUrl = contextData.resourcesByUrl.get(urlWithoutHash) || [];
|
||||
for (const resourceEvent of resourcesWithUrl) {
|
||||
if (resource && resourceEvent.frameId !== parsed.frameId)
|
||||
continue;
|
||||
@ -314,22 +349,28 @@ export class SnapshotServer {
|
||||
}
|
||||
if (!resource)
|
||||
return respond404();
|
||||
const resourceOverride = snapshotEvent.snapshot.resourceOverrides.find(o => o.url === request.url);
|
||||
const overrideSha1 = resourceOverride ? resourceOverride.sha1 : undefined;
|
||||
|
||||
const response = overrideSha1 ?
|
||||
await fetch(`/resources/${resource.resourceId}/override/${overrideSha1}`) :
|
||||
await fetch(`/resources/${resource.resourceId}`);
|
||||
const overrideSha1 = findResourceOverride(frameSnapshots, snapshotIndex, urlWithoutHash);
|
||||
const fetchUrl = overrideSha1 ?
|
||||
`/resources/${resource.resourceId}/override/${overrideSha1}` :
|
||||
`/resources/${resource.resourceId}`;
|
||||
const fetchedResponse = await fetch(fetchUrl);
|
||||
const headers = new Headers(fetchedResponse.headers);
|
||||
// We make a copy of the response, instead of just forwarding,
|
||||
// so that response url is not inherited as "/resources/...", but instead
|
||||
// as the original request url.
|
||||
// Response url turns into resource base uri that is used to resolve
|
||||
// relative links, e.g. url(/foo/bar) in style sheets.
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
if (contextData.overridenUrls.has(urlWithoutHash)) {
|
||||
// No cache, so that we refetch overridden resources.
|
||||
headers.set('Cache-Control', 'no-cache');
|
||||
}
|
||||
const response = new Response(fetchedResponse.body, {
|
||||
status: fetchedResponse.status,
|
||||
statusText: fetchedResponse.statusText,
|
||||
headers,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', function(event: any) {
|
||||
|
@ -29,7 +29,6 @@ export type ContextEntry = {
|
||||
created: trace.ContextCreatedTraceEvent;
|
||||
destroyed: trace.ContextDestroyedTraceEvent;
|
||||
pages: PageEntry[];
|
||||
resourcesByUrl: Map<string, trace.NetworkResourceTraceEvent[]>;
|
||||
}
|
||||
|
||||
export type VideoEntry = {
|
||||
@ -79,7 +78,6 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
|
||||
created: event,
|
||||
destroyed: undefined as any,
|
||||
pages: [],
|
||||
resourcesByUrl: new Map(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -123,19 +121,12 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
|
||||
break;
|
||||
}
|
||||
case 'resource': {
|
||||
const contextEntry = contextEntries.get(event.contextId)!;
|
||||
const pageEntry = pageEntries.get(event.pageId!)!;
|
||||
const action = pageEntry.actions[pageEntry.actions.length - 1];
|
||||
if (action)
|
||||
action.resources.push(event);
|
||||
else
|
||||
pageEntry.resources.push(event);
|
||||
let responseEvents = contextEntry.resourcesByUrl.get(event.url);
|
||||
if (!responseEvents) {
|
||||
responseEvents = [];
|
||||
contextEntry.resourcesByUrl.set(event.url, responseEvents);
|
||||
}
|
||||
responseEvents.push(event);
|
||||
break;
|
||||
}
|
||||
case 'dialog-opened':
|
||||
|
@ -54,7 +54,6 @@ const emptyModel: TraceModel = {
|
||||
name: '<empty>',
|
||||
filePath: '',
|
||||
pages: [],
|
||||
resourcesByUrl: new Map()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -67,10 +67,14 @@ export class Snapshotter {
|
||||
resourceOverrides: [],
|
||||
};
|
||||
for (const { url, content } of data.resourceOverrides) {
|
||||
const buffer = Buffer.from(content);
|
||||
const sha1 = calculateSha1(buffer);
|
||||
this._delegate.onBlob({ sha1, buffer });
|
||||
snapshot.resourceOverrides.push({ url, sha1 });
|
||||
if (typeof content === 'string') {
|
||||
const buffer = Buffer.from(content);
|
||||
const sha1 = calculateSha1(buffer);
|
||||
this._delegate.onBlob({ sha1, buffer });
|
||||
snapshot.resourceOverrides.push({ url, sha1 });
|
||||
} else {
|
||||
snapshot.resourceOverrides.push({ url, ref: content });
|
||||
}
|
||||
}
|
||||
this._delegate.onFrameSnapshot(source.frame, data.url, snapshot, data.snapshotId);
|
||||
});
|
||||
|
@ -29,7 +29,11 @@ export type NodeSnapshot =
|
||||
export type SnapshotData = {
|
||||
doctype?: string,
|
||||
html: NodeSnapshot,
|
||||
resourceOverrides: { url: string, content: string }[],
|
||||
resourceOverrides: {
|
||||
url: string,
|
||||
// String is the content. Number is "x snapshots ago", same url.
|
||||
content: string | number,
|
||||
}[],
|
||||
viewport: { width: number, height: number },
|
||||
url: string,
|
||||
snapshotId?: string,
|
||||
@ -48,17 +52,19 @@ export function frameSnapshotStreamer() {
|
||||
const kScrollTopAttribute = '__playwright_scroll_top_';
|
||||
const kScrollLeftAttribute = '__playwright_scroll_left_';
|
||||
|
||||
// Symbols for our own info on Nodes.
|
||||
// Symbols for our own info on Nodes/StyleSheets.
|
||||
const kSnapshotFrameId = Symbol('__playwright_snapshot_frameid_');
|
||||
const kCachedData = Symbol('__playwright_snapshot_cache_');
|
||||
type CachedData = {
|
||||
ref?: [number, number], // Previous snapshotNumber and nodeIndex.
|
||||
value?: string, // Value for input/textarea elements.
|
||||
cssText?: string, // Text for stylesheets.
|
||||
cssRef?: number, // Previous snapshotNumber for overridden stylesheets.
|
||||
};
|
||||
function ensureCachedData(node: Node): CachedData {
|
||||
if (!(node as any)[kCachedData])
|
||||
(node as any)[kCachedData] = {};
|
||||
return (node as any)[kCachedData];
|
||||
function ensureCachedData(obj: any): CachedData {
|
||||
if (!obj[kCachedData])
|
||||
obj[kCachedData] = {};
|
||||
return obj[kCachedData];
|
||||
}
|
||||
|
||||
const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
|
||||
@ -69,32 +75,45 @@ export function frameSnapshotStreamer() {
|
||||
return s.replace(/[&<]/ug, char => (escaped as any)[char]);
|
||||
}
|
||||
|
||||
function removeHash(url: string) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
u.hash = '';
|
||||
return u.toString();
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
class Streamer {
|
||||
private _removeNoScript = true;
|
||||
private _needStyleOverrides = false;
|
||||
private _timer: NodeJS.Timeout | undefined;
|
||||
private _lastSnapshotNumber = 0;
|
||||
private _observer: MutationObserver;
|
||||
private _staleStyleSheets = new Set<CSSStyleSheet>();
|
||||
private _allStyleSheetsWithUrlOverride = new Set<CSSStyleSheet>();
|
||||
private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
|
||||
|
||||
constructor() {
|
||||
// TODO: should we also intercept setters like CSSRule.cssText and CSSStyleRule.selectorText?
|
||||
this._interceptNative(window.CSSStyleSheet.prototype, 'insertRule', () => this._needStyleOverrides = true);
|
||||
this._interceptNative(window.CSSStyleSheet.prototype, 'deleteRule', () => this._needStyleOverrides = true);
|
||||
this._interceptNative(window.CSSStyleSheet.prototype, 'addRule', () => this._needStyleOverrides = true);
|
||||
this._interceptNative(window.CSSStyleSheet.prototype, 'removeRule', () => this._needStyleOverrides = true);
|
||||
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'deleteRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'addRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||
this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'removeRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'rules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||
this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'cssRules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
|
||||
|
||||
this._observer = new MutationObserver(list => this._handleMutations(list));
|
||||
const observerConfig = { attributes: true, childList: true, subtree: true, characterData: true };
|
||||
this._observer.observe(document, observerConfig);
|
||||
this._interceptNative(window.Element.prototype, 'attachShadow', (node: Node, shadowRoot: ShadowRoot) => {
|
||||
this._invalidateCache(node);
|
||||
this._interceptNativeMethod(window.Element.prototype, 'attachShadow', (node: Node, shadowRoot: ShadowRoot) => {
|
||||
this._invalidateNode(node);
|
||||
this._observer.observe(shadowRoot, observerConfig);
|
||||
});
|
||||
|
||||
this._streamSnapshot();
|
||||
}
|
||||
|
||||
private _interceptNative(obj: any, method: string, cb: (thisObj: any, result: any) => void) {
|
||||
private _interceptNativeMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) {
|
||||
const native = obj[method] as Function;
|
||||
if (!native)
|
||||
return;
|
||||
@ -105,7 +124,58 @@ export function frameSnapshotStreamer() {
|
||||
};
|
||||
}
|
||||
|
||||
private _invalidateCache(node: Node | null) {
|
||||
private _interceptNativeGetter(obj: any, prop: string, cb: (thisObj: any, result: any) => void) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(obj, prop)!;
|
||||
Object.defineProperty(obj, prop, {
|
||||
...descriptor,
|
||||
get: function() {
|
||||
const result = descriptor.get!.call(this);
|
||||
cb(this, result);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _invalidateStyleSheet(sheet: CSSStyleSheet) {
|
||||
if (this._readingStyleSheet)
|
||||
return;
|
||||
this._staleStyleSheets.add(sheet);
|
||||
if (sheet.href !== null)
|
||||
this._allStyleSheetsWithUrlOverride.add(sheet);
|
||||
if (sheet.ownerNode && sheet.ownerNode.nodeName === 'STYLE')
|
||||
this._invalidateNode(sheet.ownerNode);
|
||||
}
|
||||
|
||||
private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet): string | undefined {
|
||||
const data = ensureCachedData(sheet);
|
||||
if (this._staleStyleSheets.has(sheet)) {
|
||||
this._staleStyleSheets.delete(sheet);
|
||||
try {
|
||||
data.cssText = this._getSheetText(sheet);
|
||||
} catch (e) {
|
||||
// Sometimes we cannot access cross-origin stylesheets.
|
||||
}
|
||||
}
|
||||
return data.cssText;
|
||||
}
|
||||
|
||||
// Returns either content, ref, or no override.
|
||||
private _updateLinkStyleSheetTextIfNeeded(sheet: CSSStyleSheet, snapshotNumber: number): string | number | undefined {
|
||||
const data = ensureCachedData(sheet);
|
||||
if (this._staleStyleSheets.has(sheet)) {
|
||||
this._staleStyleSheets.delete(sheet);
|
||||
try {
|
||||
data.cssText = this._getSheetText(sheet);
|
||||
data.cssRef = snapshotNumber;
|
||||
return data.cssText;
|
||||
} catch (e) {
|
||||
// Sometimes we cannot access cross-origin stylesheets.
|
||||
}
|
||||
}
|
||||
return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
|
||||
}
|
||||
|
||||
private _invalidateNode(node: Node | null) {
|
||||
while (node) {
|
||||
ensureCachedData(node).ref = undefined;
|
||||
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && (node as ShadowRoot).host)
|
||||
@ -117,7 +187,7 @@ export function frameSnapshotStreamer() {
|
||||
|
||||
private _handleMutations(list: MutationRecord[]) {
|
||||
for (const mutation of list)
|
||||
this._invalidateCache(mutation.target);
|
||||
this._invalidateNode(mutation.target);
|
||||
}
|
||||
|
||||
markIframe(iframeElement: HTMLIFrameElement | HTMLFrameElement, frameId: string) {
|
||||
@ -177,10 +247,15 @@ export function frameSnapshotStreamer() {
|
||||
}
|
||||
|
||||
private _getSheetText(sheet: CSSStyleSheet): string {
|
||||
const rules: string[] = [];
|
||||
for (const rule of sheet.cssRules)
|
||||
rules.push(rule.cssText);
|
||||
return rules.join('\n');
|
||||
this._readingStyleSheet = true;
|
||||
try {
|
||||
const rules: string[] = [];
|
||||
for (const rule of sheet.cssRules)
|
||||
rules.push(rule.cssText);
|
||||
return rules.join('\n');
|
||||
} finally {
|
||||
this._readingStyleSheet = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _captureSnapshot(snapshotId?: string): SnapshotData {
|
||||
@ -194,38 +269,9 @@ export function frameSnapshotStreamer() {
|
||||
const value = (input as HTMLInputElement | HTMLTextAreaElement).value;
|
||||
const data = ensureCachedData(input);
|
||||
if (data.value !== value)
|
||||
this._invalidateCache(input);
|
||||
this._invalidateNode(input);
|
||||
}
|
||||
|
||||
const styleNodeToStyleSheetText = new Map<Node, string>();
|
||||
const styleSheetUrlToContentOverride = new Map<string, string>();
|
||||
|
||||
const visitStyleSheet = (sheet: CSSStyleSheet) => {
|
||||
// TODO: recalculate these upon changes, and only send them once.
|
||||
if (!this._needStyleOverrides)
|
||||
return;
|
||||
|
||||
try {
|
||||
for (const rule of sheet.cssRules) {
|
||||
if ((rule as CSSImportRule).styleSheet)
|
||||
visitStyleSheet((rule as CSSImportRule).styleSheet);
|
||||
}
|
||||
|
||||
const cssText = this._getSheetText(sheet);
|
||||
if (sheet.ownerNode && sheet.ownerNode.nodeName === 'STYLE') {
|
||||
// Stylesheets with owner STYLE nodes will be rewritten.
|
||||
styleNodeToStyleSheetText.set(sheet.ownerNode, cssText);
|
||||
} else if (sheet.href !== null) {
|
||||
// Other stylesheets will have resource overrides.
|
||||
const base = this._getSheetBase(sheet);
|
||||
const url = this._resolveUrl(base, sheet.href);
|
||||
styleSheetUrlToContentOverride.set(url, cssText);
|
||||
}
|
||||
} catch (e) {
|
||||
// Sometimes we cannot access cross-origin stylesheets.
|
||||
}
|
||||
};
|
||||
|
||||
let nodeCounter = 0;
|
||||
|
||||
const visit = (node: Node | ShadowRoot): NodeSnapshot | undefined => {
|
||||
@ -253,18 +299,19 @@ export function frameSnapshotStreamer() {
|
||||
return escapeText(node.nodeValue || '');
|
||||
|
||||
if (nodeName === 'STYLE') {
|
||||
const cssText = styleNodeToStyleSheetText.get(node) || node.textContent || '';
|
||||
return ['style', {}, escapeText(cssText)];
|
||||
const sheet = (node as HTMLStyleElement).sheet;
|
||||
let cssText: string | undefined;
|
||||
if (sheet)
|
||||
cssText = this._updateStyleElementStyleSheetTextIfNeeded(sheet);
|
||||
nodeCounter++; // Compensate for the extra text node in the list.
|
||||
return ['style', {}, escapeText(cssText || node.textContent || '')];
|
||||
}
|
||||
|
||||
const attrs: { [attr: string]: string } = {};
|
||||
const result: NodeSnapshot = [nodeName, attrs];
|
||||
|
||||
if (nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||||
for (const sheet of (node as ShadowRoot).styleSheets)
|
||||
visitStyleSheet(sheet);
|
||||
if (nodeType === Node.DOCUMENT_FRAGMENT_NODE)
|
||||
attrs[kShadowAttribute] = 'open';
|
||||
}
|
||||
|
||||
if (nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
@ -349,14 +396,11 @@ export function frameSnapshotStreamer() {
|
||||
return result;
|
||||
};
|
||||
|
||||
for (const sheet of doc.styleSheets)
|
||||
visitStyleSheet(sheet);
|
||||
const html = doc.documentElement ? visit(doc.documentElement)! : (['html', {}] as NodeSnapshot);
|
||||
|
||||
return {
|
||||
const result: SnapshotData = {
|
||||
html,
|
||||
doctype: doc.doctype ? doc.doctype.name : undefined,
|
||||
resourceOverrides: Array.from(styleSheetUrlToContentOverride).map(([url, content]) => ({ url, content })),
|
||||
resourceOverrides: [],
|
||||
viewport: {
|
||||
width: Math.max(doc.body ? doc.body.offsetWidth : 0, doc.documentElement ? doc.documentElement.offsetWidth : 0),
|
||||
height: Math.max(doc.body ? doc.body.offsetHeight : 0, doc.documentElement ? doc.documentElement.offsetHeight : 0),
|
||||
@ -364,6 +408,19 @@ export function frameSnapshotStreamer() {
|
||||
url: location.href,
|
||||
snapshotId,
|
||||
};
|
||||
|
||||
for (const sheet of this._allStyleSheetsWithUrlOverride) {
|
||||
const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
|
||||
if (content === undefined) {
|
||||
// Unable to capture stylsheet contents.
|
||||
continue;
|
||||
}
|
||||
const base = this._getSheetBase(sheet);
|
||||
const url = removeHash(this._resolveUrl(base, sheet.href!));
|
||||
result.resourceOverrides.push({ url, content });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,6 +152,6 @@ export type TraceEvent =
|
||||
export type FrameSnapshot = {
|
||||
doctype?: string,
|
||||
html: NodeSnapshot,
|
||||
resourceOverrides: { url: string, sha1: string }[],
|
||||
resourceOverrides: { url: string, sha1?: string, ref?: number }[],
|
||||
viewport: { width: number, height: number },
|
||||
};
|
||||
|
@ -7,14 +7,11 @@
|
||||
}
|
||||
</style>
|
||||
<div>hello, world!</div>
|
||||
<textarea>Before edit</textarea>
|
||||
<div class=root></div>
|
||||
<script>
|
||||
let shadow;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelector('textarea').value = 'After edit';
|
||||
|
||||
const root = document.querySelector('.root');
|
||||
shadow = root.attachShadow({ mode: 'open' });
|
||||
|
||||
@ -27,6 +24,11 @@
|
||||
imaged.className = 'imaged';
|
||||
shadow.appendChild(imaged);
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.textContent = 'Before edit';
|
||||
textarea.style.display = 'block';
|
||||
shadow.appendChild(textarea);
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.width = '600px';
|
||||
iframe.height = '600px';
|
||||
@ -35,13 +37,22 @@
|
||||
});
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
for (const rule of shadow.styleSheets[0].cssRules) {
|
||||
if (rule.styleSheet) {
|
||||
for (const rule2 of rule.styleSheet.cssRules) {
|
||||
if (rule2.cssText.includes('width: 200px'))
|
||||
rule2.style.width = '400px';
|
||||
setTimeout(() => {
|
||||
shadow.querySelector('textarea').value = 'After edit';
|
||||
|
||||
for (const rule of document.styleSheets[1].cssRules) {
|
||||
if (rule.cssText.includes('background: cyan'))
|
||||
rule.style.background = 'magenta';
|
||||
}
|
||||
|
||||
for (const rule of shadow.styleSheets[0].cssRules) {
|
||||
if (rule.styleSheet) {
|
||||
for (const rule2 of rule.styleSheet.cssRules) {
|
||||
if (rule2.cssText.includes('width: 200px'))
|
||||
rule2.style.width = '400px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user