refactoring public API

This commit is contained in:
Yanzhen Yu
2020-11-29 13:22:01 +08:00
parent 555398d3f5
commit 852632656f
5 changed files with 146 additions and 70 deletions

View File

@@ -77,9 +77,12 @@ export function addHoverClass(cssText: string): string {
function buildNode( function buildNode(
n: serializedNodeWithId, n: serializedNodeWithId,
doc: Document, options: {
HACK_CSS: boolean, doc: Document;
hackCss: boolean;
},
): Node | null { ): Node | null {
const { doc, hackCss } = options;
switch (n.type) { switch (n.type) {
case NodeType.Document: case NodeType.Document:
return doc.implementation.createDocument(null, '', null); return doc.implementation.createDocument(null, '', null);
@@ -109,7 +112,7 @@ function buildNode(
const isTextarea = tagName === 'textarea' && name === 'value'; const isTextarea = tagName === 'textarea' && name === 'value';
const isRemoteOrDynamicCss = const isRemoteOrDynamicCss =
tagName === 'style' && name === '_cssText'; tagName === 'style' && name === '_cssText';
if (isRemoteOrDynamicCss && HACK_CSS) { if (isRemoteOrDynamicCss && hackCss) {
value = addHoverClass(value); value = addHoverClass(value);
} }
if (isTextarea || isRemoteOrDynamicCss) { if (isTextarea || isRemoteOrDynamicCss) {
@@ -177,7 +180,7 @@ function buildNode(
return node; return node;
case NodeType.Text: case NodeType.Text:
return doc.createTextNode( return doc.createTextNode(
n.isStyle && HACK_CSS ? addHoverClass(n.textContent) : n.textContent, n.isStyle && hackCss ? addHoverClass(n.textContent) : n.textContent,
); );
case NodeType.CDATA: case NodeType.CDATA:
return doc.createCDATASection(n.textContent); return doc.createCDATASection(n.textContent);
@@ -190,12 +193,15 @@ function buildNode(
export function buildNodeWithSN( export function buildNodeWithSN(
n: serializedNodeWithId, n: serializedNodeWithId,
doc: Document, options: {
map: idNodeMap, doc: Document;
skipChild = false, map: idNodeMap;
HACK_CSS = true, skipChild?: boolean;
hackCss: boolean;
},
): INode | null { ): INode | null {
let node = buildNode(n, doc, HACK_CSS); const { doc, map, skipChild = false, hackCss = true } = options;
let node = buildNode(n, { doc, hackCss });
if (!node) { if (!node) {
return null; return null;
} }
@@ -214,7 +220,12 @@ export function buildNodeWithSN(
!skipChild !skipChild
) { ) {
for (const childN of n.childNodes) { for (const childN of n.childNodes) {
const childNode = buildNodeWithSN(childN, doc, map, false, HACK_CSS); const childNode = buildNodeWithSN(childN, {
doc,
map,
skipChild: false,
hackCss,
});
if (!childNode) { if (!childNode) {
console.warn('Failed to rebuild', childN); console.warn('Failed to rebuild', childN);
} else { } else {
@@ -259,15 +270,20 @@ function handleScroll(node: INode) {
function rebuild( function rebuild(
n: serializedNodeWithId, n: serializedNodeWithId,
doc: Document, options: {
onVisit?: (node: INode) => unknown, doc: Document;
/** onVisit?: (node: INode) => unknown;
* This is not a public API yet, just for POC hackCss?: boolean;
*/ },
HACK_CSS: boolean = true,
): [Node | null, idNodeMap] { ): [Node | null, idNodeMap] {
const { doc, onVisit, hackCss = true } = options;
const idNodeMap: idNodeMap = {}; const idNodeMap: idNodeMap = {};
const node = buildNodeWithSN(n, doc, idNodeMap, false, HACK_CSS); const node = buildNodeWithSN(n, {
doc,
map: idNodeMap,
skipChild: false,
hackCss,
});
visit(idNodeMap, (visitedNode) => { visit(idNodeMap, (visitedNode) => {
if (onVisit) { if (onVisit) {
onVisit(visitedNode); onVisit(visitedNode);

View File

@@ -187,13 +187,23 @@ export function _isBlockedElement(
function serializeNode( function serializeNode(
n: Node, n: Node,
doc: Document, options: {
blockClass: string | RegExp, doc: Document;
blockSelector: string | null, blockClass: string | RegExp;
inlineStylesheet: boolean, blockSelector: string | null;
maskInputOptions: MaskInputOptions = {}, inlineStylesheet: boolean;
recordCanvas: boolean, maskInputOptions: MaskInputOptions;
recordCanvas: boolean;
},
): serializedNode | false { ): serializedNode | false {
const {
doc,
blockClass,
blockSelector,
inlineStylesheet,
maskInputOptions = {},
recordCanvas,
} = options;
switch (n.nodeType) { switch (n.nodeType) {
case n.DOCUMENT_NODE: case n.DOCUMENT_NODE:
return { return {
@@ -437,26 +447,39 @@ function slimDOMExcluded(
export function serializeNodeWithId( export function serializeNodeWithId(
n: Node | INode, n: Node | INode,
doc: Document, options: {
map: idNodeMap, doc: Document;
blockClass: string | RegExp, map: idNodeMap;
blockSelector: string | null, blockClass: string | RegExp;
skipChild = false, blockSelector: string | null;
inlineStylesheet = true, skipChild: boolean;
maskInputOptions?: MaskInputOptions, inlineStylesheet: boolean;
slimDOMOptions: SlimDOMOptions = {}, maskInputOptions?: MaskInputOptions;
recordCanvas?: boolean, slimDOMOptions: SlimDOMOptions;
preserveWhiteSpace = true, recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
},
): serializedNodeWithId | null { ): serializedNodeWithId | null {
const _serializedNode = serializeNode( const {
n, doc,
map,
blockClass,
blockSelector,
skipChild = false,
inlineStylesheet = true,
maskInputOptions = {},
slimDOMOptions,
recordCanvas = false,
} = options;
let { preserveWhiteSpace = true } = options;
const _serializedNode = serializeNode(n, {
doc, doc,
blockClass, blockClass,
blockSelector, blockSelector,
inlineStylesheet, inlineStylesheet,
maskInputOptions, maskInputOptions,
recordCanvas || false, recordCanvas,
); });
if (!_serializedNode) { if (!_serializedNode) {
// TODO: dev only // TODO: dev only
console.warn(n, 'not serialized'); console.warn(n, 'not serialized');
@@ -504,8 +527,7 @@ export function serializeNodeWithId(
preserveWhiteSpace = false; preserveWhiteSpace = false;
} }
for (const childN of Array.from(n.childNodes)) { for (const childN of Array.from(n.childNodes)) {
const serializedChildNode = serializeNodeWithId( const serializedChildNode = serializeNodeWithId(childN, {
childN,
doc, doc,
map, map,
blockClass, blockClass,
@@ -516,7 +538,7 @@ export function serializeNodeWithId(
slimDOMOptions, slimDOMOptions,
recordCanvas, recordCanvas,
preserveWhiteSpace, preserveWhiteSpace,
); });
if (serializedChildNode) { if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode); serializedNode.childNodes.push(serializedChildNode);
} }
@@ -527,16 +549,26 @@ export function serializeNodeWithId(
function snapshot( function snapshot(
n: Document, n: Document,
blockClass: string | RegExp = 'rr-block', options?: {
inlineStylesheet = true, blockClass?: string | RegExp;
maskAllInputsOrOptions: boolean | MaskInputOptions, inlineStylesheet?: boolean;
slimDOMSensibleOrOptions: boolean | SlimDOMOptions, maskAllInputs?: boolean | MaskInputOptions;
recordCanvas?: boolean, slimDOM?: boolean | SlimDOMOptions;
blockSelector: string | null = null, recordCanvas?: boolean;
blockSelector?: string | null;
},
): [serializedNodeWithId | null, idNodeMap] { ): [serializedNodeWithId | null, idNodeMap] {
const {
blockClass = 'rr-block',
inlineStylesheet = true,
recordCanvas = false,
blockSelector = null,
maskAllInputs = false,
slimDOM = false,
} = options || {};
const idNodeMap: idNodeMap = {}; const idNodeMap: idNodeMap = {};
const maskInputOptions: MaskInputOptions = const maskInputOptions: MaskInputOptions =
maskAllInputsOrOptions === true maskAllInputs === true
? { ? {
color: true, color: true,
date: true, date: true,
@@ -554,40 +586,39 @@ function snapshot(
textarea: true, textarea: true,
select: true, select: true,
} }
: maskAllInputsOrOptions === false : maskAllInputs === false
? {} ? {}
: maskAllInputsOrOptions; : maskAllInputs;
const slimDOMOptions: SlimDOMOptions = const slimDOMOptions: SlimDOMOptions =
slimDOMSensibleOrOptions === true || slimDOMSensibleOrOptions === 'all' slimDOM === true || slimDOM === 'all'
? // if true: set of sensible options that should not throw away any information ? // if true: set of sensible options that should not throw away any information
{ {
script: true, script: true,
comment: true, comment: true,
headFavicon: true, headFavicon: true,
headWhitespace: true, headWhitespace: true,
headMetaDescKeywords: slimDOMSensibleOrOptions === 'all', // destructive headMetaDescKeywords: slimDOM === 'all', // destructive
headMetaSocial: true, headMetaSocial: true,
headMetaRobots: true, headMetaRobots: true,
headMetaHttpEquiv: true, headMetaHttpEquiv: true,
headMetaAuthorship: true, headMetaAuthorship: true,
headMetaVerification: true, headMetaVerification: true,
} }
: slimDOMSensibleOrOptions === false : slimDOM === false
? {} ? {}
: slimDOMSensibleOrOptions; : slimDOM;
return [ return [
serializeNodeWithId( serializeNodeWithId(n, {
n, doc: n,
n, map: idNodeMap,
idNodeMap,
blockClass, blockClass,
blockSelector, blockSelector,
false, skipChild: false,
inlineStylesheet, inlineStylesheet,
maskInputOptions, maskInputOptions,
slimDOMOptions, slimDOMOptions,
recordCanvas, recordCanvas,
), }),
idNodeMap, idNodeMap,
]; ];
} }

View File

@@ -11,7 +11,7 @@ import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
import { Suite } from 'mocha'; import { Suite } from 'mocha';
const htmlFolder = path.join(__dirname, 'html'); const htmlFolder = path.join(__dirname, 'html');
const htmls = fs.readdirSync(htmlFolder).map(filePath => { const htmls = fs.readdirSync(htmlFolder).map((filePath) => {
const raw = fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8'); const raw = fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8');
return { return {
filePath, filePath,
@@ -24,7 +24,7 @@ interface IMimeType {
} }
const server = () => const server = () =>
new Promise<http.Server>(resolve => { new Promise<http.Server>((resolve) => {
const mimeType: IMimeType = { const mimeType: IMimeType = {
'.html': 'text/html', '.html': 'text/html',
'.js': 'text/javascript', '.js': 'text/javascript',
@@ -73,7 +73,7 @@ interface ISuite extends Suite {
code: string; code: string;
} }
describe('integration tests', function(this: ISuite) { describe('integration tests', function (this: ISuite) {
before(async () => { before(async () => {
this.server = await server(); this.server = await server();
this.browser = await puppeteer.launch({ this.browser = await puppeteer.launch({
@@ -102,16 +102,18 @@ describe('integration tests', function(this: ISuite) {
const page: puppeteer.Page = await this.browser.newPage(); const page: puppeteer.Page = await this.browser.newPage();
// console for debug // console for debug
// tslint:disable-next-line: no-console // tslint:disable-next-line: no-console
page.on('console', msg => console.log(msg.text())); page.on('console', (msg) => console.log(msg.text()));
await page.goto(`http://localhost:3030/html`); await page.goto(`http://localhost:3030/html`);
await page.setContent(html.src, { await page.setContent(html.src, {
waitUntil: 'load', waitUntil: 'load',
}); });
const rebuildHtml = (await page.evaluate(`${this.code} const rebuildHtml = (
await page.evaluate(`${this.code}
const x = new XMLSerializer(); const x = new XMLSerializer();
const [snap] = rrweb.snapshot(document); const [snap] = rrweb.snapshot(document);
x.serializeToString(rrweb.rebuild(snap, document)[0]); x.serializeToString(rrweb.rebuild(snap, { doc: document })[0]);
`)).replace(/\n\n/g, ''); `)
).replace(/\n\n/g, '');
const result = matchSnapshot(rebuildHtml, __filename, title); const result = matchSnapshot(rebuildHtml, __filename, title);
assert(result.pass, result.pass ? '' : result.report()); assert(result.pass, result.pass ? '' : result.report());
}).timeout(5000); }).timeout(5000);

13
typings/rebuild.d.ts vendored
View File

@@ -1,5 +1,14 @@
import { serializedNodeWithId, idNodeMap, INode } from './types'; import { serializedNodeWithId, idNodeMap, INode } from './types';
export declare function addHoverClass(cssText: string): string; export declare function addHoverClass(cssText: string): string;
export declare function buildNodeWithSN(n: serializedNodeWithId, doc: Document, map: idNodeMap, skipChild?: boolean, HACK_CSS?: boolean): INode | null; export declare function buildNodeWithSN(n: serializedNodeWithId, options: {
declare function rebuild(n: serializedNodeWithId, doc: Document, onVisit?: (node: INode) => unknown, HACK_CSS?: boolean): [Node | null, idNodeMap]; doc: Document;
map: idNodeMap;
skipChild?: boolean;
hackCss: boolean;
}): INode | null;
declare function rebuild(n: serializedNodeWithId, options: {
doc: Document;
onVisit?: (node: INode) => unknown;
hackCss?: boolean;
}): [Node | null, idNodeMap];
export default rebuild; export default rebuild;

22
typings/snapshot.d.ts vendored
View File

@@ -4,8 +4,26 @@ export declare function absoluteToStylesheet(cssText: string | null, href: strin
export declare function absoluteToDoc(doc: Document, attributeValue: string): string; export declare function absoluteToDoc(doc: Document, attributeValue: string): string;
export declare function transformAttribute(doc: Document, name: string, value: string): string; export declare function transformAttribute(doc: Document, name: string, value: string): string;
export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean; export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean;
export declare function serializeNodeWithId(n: Node | INode, doc: Document, map: idNodeMap, blockClass: string | RegExp, blockSelector: string | null, skipChild?: boolean, inlineStylesheet?: boolean, maskInputOptions?: MaskInputOptions, slimDOMOptions?: SlimDOMOptions, recordCanvas?: boolean, preserveWhiteSpace?: boolean): serializedNodeWithId | null; export declare function serializeNodeWithId(n: Node | INode, options: {
declare function snapshot(n: Document, blockClass: string | RegExp | undefined, inlineStylesheet: boolean | undefined, maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean, blockSelector?: string | null): [serializedNodeWithId | null, idNodeMap]; doc: Document;
map: idNodeMap;
blockClass: string | RegExp;
blockSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
maskInputOptions?: MaskInputOptions;
slimDOMOptions: SlimDOMOptions;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
}): serializedNodeWithId | null;
declare function snapshot(n: Document, options?: {
blockClass?: string | RegExp;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
slimDOM?: boolean | SlimDOMOptions;
recordCanvas?: boolean;
blockSelector?: string | null;
}): [serializedNodeWithId | null, idNodeMap];
export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void; export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
export declare function cleanupSnapshot(): void; export declare function cleanupSnapshot(): void;
export default snapshot; export default snapshot;