replace script tag with noscript and inline the states of form field components
This commit is contained in:
@@ -11,11 +11,20 @@ function buildNode(n: serializedNodeWithId): Node | null {
|
|||||||
n.systemId,
|
n.systemId,
|
||||||
);
|
);
|
||||||
case NodeType.Element:
|
case NodeType.Element:
|
||||||
const node = document.createElement(n.tagName);
|
const tagName = n.tagName === 'script' ? 'noscript' : n.tagName;
|
||||||
|
const node = document.createElement(tagName);
|
||||||
for (const name in n.attributes) {
|
for (const name in n.attributes) {
|
||||||
if (n.attributes.hasOwnProperty(name)) {
|
if (n.attributes.hasOwnProperty(name)) {
|
||||||
|
let value = n.attributes[name];
|
||||||
|
value = typeof value === 'boolean' ? '' : value;
|
||||||
|
// textarea hack
|
||||||
|
if (n.tagName === 'textarea' && name === 'value') {
|
||||||
|
const child = document.createTextNode(value);
|
||||||
|
node.appendChild(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
node.setAttribute(name, n.attributes[name]);
|
node.setAttribute(name, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// skip invalid attribute
|
// skip invalid attribute
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ function genId(): number {
|
|||||||
return _id++;
|
return _id++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetId() {
|
||||||
|
_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
function serializeNode(n: Node): serializedNode | false {
|
function serializeNode(n: Node): serializedNode | false {
|
||||||
switch (n.nodeType) {
|
switch (n.nodeType) {
|
||||||
case n.DOCUMENT_NODE:
|
case n.DOCUMENT_NODE:
|
||||||
@@ -31,6 +35,28 @@ function serializeNode(n: Node): serializedNode | false {
|
|||||||
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
|
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
|
||||||
attributes[name] = value;
|
attributes[name] = value;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
tagName === 'input' ||
|
||||||
|
tagName === 'textarea' ||
|
||||||
|
tagName === 'select'
|
||||||
|
) {
|
||||||
|
const value = (n as HTMLInputElement | HTMLTextAreaElement).value;
|
||||||
|
if (
|
||||||
|
attributes.type !== 'radio' &&
|
||||||
|
attributes.type !== 'checkbox' &&
|
||||||
|
value
|
||||||
|
) {
|
||||||
|
attributes.value = value;
|
||||||
|
} else if ((n as HTMLInputElement).checked) {
|
||||||
|
attributes.checked = (n as HTMLInputElement).checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagName === 'option') {
|
||||||
|
const selectValue = (n as HTMLOptionElement).parentElement;
|
||||||
|
if (attributes.value === (selectValue as HTMLSelectElement).value) {
|
||||||
|
attributes.selected = (n as HTMLOptionElement).selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: NodeType.Element,
|
type: NodeType.Element,
|
||||||
tagName,
|
tagName,
|
||||||
@@ -65,7 +91,7 @@ function serializeNode(n: Node): serializedNode | false {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapshot(n: Node): serializedNodeWithId | null {
|
function _snapshot(n: Node): serializedNodeWithId | null {
|
||||||
const _serializedNode = serializeNode(n);
|
const _serializedNode = serializeNode(n);
|
||||||
if (!_serializedNode) {
|
if (!_serializedNode) {
|
||||||
// TODO: dev only
|
// TODO: dev only
|
||||||
@@ -80,10 +106,15 @@ function snapshot(n: Node): serializedNodeWithId | null {
|
|||||||
serializedNode.type === NodeType.Element
|
serializedNode.type === NodeType.Element
|
||||||
) {
|
) {
|
||||||
for (const childN of Array.from(n.childNodes)) {
|
for (const childN of Array.from(n.childNodes)) {
|
||||||
serializedNode.childNodes.push(snapshot(childN));
|
serializedNode.childNodes.push(_snapshot(childN));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return serializedNode;
|
return serializedNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapshot(n: Node): serializedNodeWithId | null {
|
||||||
|
resetId();
|
||||||
|
return _snapshot(n);
|
||||||
|
}
|
||||||
|
|
||||||
export default snapshot;
|
export default snapshot;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type documentTypeNode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type attributes = {
|
export type attributes = {
|
||||||
[key: string]: string;
|
[key: string]: string | boolean;
|
||||||
};
|
};
|
||||||
export type elementNode = {
|
export type elementNode = {
|
||||||
type: NodeType.Element;
|
type: NodeType.Element;
|
||||||
|
|||||||
18
test/html/with-script.html
Normal file
18
test/html/with-script.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>with script</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="a.js"></script>
|
||||||
|
<script>
|
||||||
|
console.log(1)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import 'mocha';
|
|
||||||
import mochaDom = require('mocha-jsdom');
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { JSDOM } from 'jsdom';
|
|
||||||
import { snapshot, rebuild } from '../src';
|
|
||||||
|
|
||||||
const htmlFolder = path.join(__dirname, 'html');
|
|
||||||
const htmls = fs.readdirSync(htmlFolder).map(filePath => {
|
|
||||||
return {
|
|
||||||
filePath,
|
|
||||||
content: fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8'),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('integration tests', () => {
|
|
||||||
mochaDom({ url: 'http://localhost' });
|
|
||||||
|
|
||||||
it('will snapshot document type', () => {
|
|
||||||
const raw = '<html></html>';
|
|
||||||
const dom = new JSDOM(raw);
|
|
||||||
const snap = snapshot(dom.window.document);
|
|
||||||
expect(snap).to.deep.equal({
|
|
||||||
type: 0,
|
|
||||||
childNodes: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
tagName: 'html',
|
|
||||||
attributes: {},
|
|
||||||
childNodes: [
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
tagName: 'head',
|
|
||||||
attributes: {},
|
|
||||||
childNodes: [],
|
|
||||||
id: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 2,
|
|
||||||
tagName: 'body',
|
|
||||||
attributes: {},
|
|
||||||
childNodes: [],
|
|
||||||
id: 4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
id: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
id: 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('will not throw error with invalid attribute', () => {
|
|
||||||
const raw = `<html foo='bar' ''></html>`;
|
|
||||||
const dom = new JSDOM(raw);
|
|
||||||
expect(() => rebuild(snapshot(dom.window.document))).not.to.throw();
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const html of htmls) {
|
|
||||||
it('[html file]:' + html.filePath, () => {
|
|
||||||
const dom = new JSDOM(html.content);
|
|
||||||
const snap = snapshot(dom.window.document);
|
|
||||||
const rebuildDom = rebuild(snap);
|
|
||||||
expect((rebuildDom as Document).documentElement.outerHTML).to.equal(
|
|
||||||
dom.window.document.documentElement.outerHTML,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
132
test/integration.ts
Normal file
132
test/integration.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import 'mocha';
|
||||||
|
import mochaDom = require('mocha-jsdom');
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
import { snapshot, rebuild } from '../src';
|
||||||
|
|
||||||
|
const htmlFolder = path.join(__dirname, 'html');
|
||||||
|
const htmls = fs.readdirSync(htmlFolder).map(filePath => {
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
content: fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('integration tests', () => {
|
||||||
|
mochaDom({ url: 'http://localhost' });
|
||||||
|
|
||||||
|
for (const html of htmls) {
|
||||||
|
it('[html file]:' + html.filePath, () => {
|
||||||
|
const dom = new JSDOM(html.content);
|
||||||
|
const snap = snapshot(dom.window.document);
|
||||||
|
const rebuildDom = rebuild(snap);
|
||||||
|
const htmlStr = dom.window.document.documentElement.outerHTML
|
||||||
|
.replace(/<script>(.|\n)*?<\/script>/g, '<script></script>')
|
||||||
|
.replace(/<script(.*?)>/g, '<noscript$1>')
|
||||||
|
.replace(/<\/script>/g, '</noscript>');
|
||||||
|
expect((rebuildDom as Document).documentElement.outerHTML).to.equal(
|
||||||
|
htmlStr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('will snapshot document type', () => {
|
||||||
|
const raw = '<html></html>';
|
||||||
|
const dom = new JSDOM(raw);
|
||||||
|
const snap = snapshot(dom.window.document);
|
||||||
|
expect(snap).to.deep.equal({
|
||||||
|
type: 0,
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'html',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'head',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [],
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
tagName: 'body',
|
||||||
|
attributes: {},
|
||||||
|
childNodes: [],
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will not throw error with invalid attribute', () => {
|
||||||
|
const raw = `<html foo='bar' ''></html>`;
|
||||||
|
const dom = new JSDOM(raw);
|
||||||
|
expect(() => rebuild(snapshot(dom.window.document))).not.to.throw();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will inline text input value', () => {
|
||||||
|
const raw = '<input type="text">';
|
||||||
|
const dom = new JSDOM(raw);
|
||||||
|
dom.window.document.querySelector('input').value = '1';
|
||||||
|
const rebuildDom = rebuild(snapshot(dom.window.document));
|
||||||
|
expect((rebuildDom as Document).querySelector('input').outerHTML).to.equal(
|
||||||
|
'<input type="text" value="1">',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will inline radio input value', () => {
|
||||||
|
const raw = '<input type="radio">';
|
||||||
|
const dom = new JSDOM(raw);
|
||||||
|
dom.window.document.querySelector('input').checked = true;
|
||||||
|
const rebuildDom = rebuild(snapshot(dom.window.document));
|
||||||
|
expect((rebuildDom as Document).querySelector('input').outerHTML).to.equal(
|
||||||
|
'<input type="radio" checked="">',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will inline checkbox input value', () => {
|
||||||
|
const raw = '<input type="checkbox">';
|
||||||
|
const dom = new JSDOM(raw);
|
||||||
|
dom.window.document.querySelector('input').checked = true;
|
||||||
|
const rebuildDom = rebuild(snapshot(dom.window.document));
|
||||||
|
expect((rebuildDom as Document).querySelector('input').outerHTML).to.equal(
|
||||||
|
'<input type="checkbox" checked="">',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will inline textarea value into text node', () => {
|
||||||
|
const raw = '<textarea name="" id="" cols="30" rows="10"></textarea>';
|
||||||
|
const dom = new JSDOM(raw);
|
||||||
|
dom.window.document.querySelector('textarea').value = '1234';
|
||||||
|
const rebuildDom = rebuild(snapshot(dom.window.document));
|
||||||
|
expect(
|
||||||
|
(rebuildDom as Document).querySelector('textarea').outerHTML,
|
||||||
|
).to.equal('<textarea name="" id="" cols="30" rows="10">1234</textarea>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will inline options state', () => {
|
||||||
|
const raw = `
|
||||||
|
<select name="" id="">
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
const dom = new JSDOM(raw);
|
||||||
|
dom.window.document.querySelector('select').value = '2';
|
||||||
|
const rebuildDom = rebuild(snapshot(dom.window.document));
|
||||||
|
expect((rebuildDom as Document).querySelector('select').outerHTML).to.equal(
|
||||||
|
`<select name="" id="" value="2">
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2" selected="">2</option>
|
||||||
|
</select>`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user