mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
chore: dom translator and tests (#65)
* chore: dom translator and tests * fix: proper skipping of tests
This commit is contained in:
parent
8dee9dd838
commit
d5ab10232d
6
package-lock.json
generated
6
package-lock.json
generated
@ -5985,6 +5985,12 @@
|
||||
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
|
||||
"dev": true
|
||||
},
|
||||
"esm": {
|
||||
"version": "3.2.25",
|
||||
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
|
||||
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
|
||||
"dev": true
|
||||
},
|
||||
"espree": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"esm": "^3.2.25",
|
||||
"fixpack": "^3.0.6",
|
||||
"husky": "^4.3.0",
|
||||
"jsdom": "^16.4.0",
|
||||
@ -56,7 +57,7 @@
|
||||
"size": "cross-env SIZE=1 rollup -c ./rollup.config.js && node ./utilities/sizes.js",
|
||||
"size:check": "cross-env CHECK=1 npm run size",
|
||||
"size:write": "cross-env WRITE=1 npm run size && git add .sizes.json",
|
||||
"test": "cross-env NODE_ENV=test MARKO_SOURCE_RUNTIME=1 mocha -r ts-node/register -r source-map-support/register packages/*/test/{*.test.ts,*/*.test.ts}",
|
||||
"test": "cross-env NODE_ENV=test MARKO_SOURCE_RUNTIME=1 TS_NODE_IGNORE='/node_modules/(?!@marko/)/' mocha -r esm -r ts-node/register -r source-map-support/register packages/*/test/{*.test.ts,*/*.test.ts}",
|
||||
"test:coverage": "nyc --reporter=text-summary npm run test",
|
||||
"test:watch": "npm run test -- --watch --watch-files '**/*.ts'"
|
||||
}
|
||||
|
||||
@ -6,12 +6,13 @@ import {
|
||||
assertNoVar
|
||||
} from "@marko/babel-utils";
|
||||
import { writeHTML } from "../util/html-write";
|
||||
import { writeTemplate } from "../util/dom-writer";
|
||||
import { isOutputHTML } from "../util/marko-config";
|
||||
|
||||
export function enter(tag: NodePath<t.MarkoTag>) {
|
||||
if (isOutputHTML(tag)) {
|
||||
writeHTML(tag)`<!--`;
|
||||
}
|
||||
} else writeTemplate(tag, `<!--`);
|
||||
}
|
||||
|
||||
export function exit(tag: NodePath<t.MarkoTag>) {
|
||||
@ -22,7 +23,7 @@ export function exit(tag: NodePath<t.MarkoTag>) {
|
||||
|
||||
if (isOutputHTML(tag)) {
|
||||
writeHTML(tag)`-->`;
|
||||
}
|
||||
} else writeTemplate(tag, `-->`);
|
||||
|
||||
tag.remove();
|
||||
}
|
||||
|
||||
@ -1,6 +1,41 @@
|
||||
import { types as t, NodePath } from "@marko/babel-types";
|
||||
import { callRuntime } from "../util/runtime";
|
||||
import {
|
||||
needsPlaceholderMarker,
|
||||
isOnlyChild,
|
||||
Walks,
|
||||
writeHydrate,
|
||||
writeTemplate,
|
||||
writeWalks,
|
||||
checkNextMarker
|
||||
} from "../util/dom-writer";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export default function (placeholder: NodePath<t.MarkoPlaceholder>) {
|
||||
// TODO.
|
||||
if (needsPlaceholderMarker(placeholder)) {
|
||||
console.log("REPLACE");
|
||||
writeWalks(placeholder, Walks.REPLACE);
|
||||
writeTemplate(placeholder, "<!>");
|
||||
} else if (isOnlyChild(placeholder)) {
|
||||
console.log("GET");
|
||||
writeWalks(placeholder, Walks.GET);
|
||||
writeTemplate(placeholder, " ");
|
||||
} else if (!checkNextMarker(placeholder)) {
|
||||
console.log("AFTER");
|
||||
writeWalks(placeholder, Walks.AFTER);
|
||||
} else {
|
||||
console.log("BEFORE");
|
||||
writeWalks(placeholder, Walks.BEFORE);
|
||||
}
|
||||
|
||||
// writeHydrate(
|
||||
// placeholder,
|
||||
// t.expressionStatement(callRuntime(placeholder, "walk"))
|
||||
// );
|
||||
writeHydrate(
|
||||
placeholder,
|
||||
t.expressionStatement(
|
||||
callRuntime(placeholder, "text", placeholder.get("value").node)
|
||||
)
|
||||
);
|
||||
placeholder.remove();
|
||||
}
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { types as t, NodePath } from "@marko/babel-types";
|
||||
import { writeExports } from "../util/dom-export";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function enter(program: NodePath<t.Program>) {}
|
||||
export function enter(program: NodePath<t.Program>) {
|
||||
program.state.template = "";
|
||||
program.state.walks = [];
|
||||
program.state.hydrate = [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function exit(program: NodePath<t.Program>) {}
|
||||
export function exit(program: NodePath<t.Program>) {
|
||||
writeExports(program);
|
||||
}
|
||||
|
||||
@ -1,10 +1,79 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { types as t, NodePath } from "@marko/babel-types";
|
||||
import { getTagDef } from "@marko/babel-utils";
|
||||
import {
|
||||
writeHydrate,
|
||||
writeTemplate,
|
||||
writeWalks,
|
||||
Walks,
|
||||
setOnlyChild,
|
||||
clearOnlyChild
|
||||
} from "../../util/dom-writer";
|
||||
import { callRuntime, getHTMLRuntime } from "../../util/runtime";
|
||||
|
||||
export function enter(tag: NodePath<t.MarkoTag>) {
|
||||
// TODO
|
||||
const attrs = tag.get("attributes");
|
||||
const tagDef = getTagDef(tag);
|
||||
const hasSpread = attrs.some(attr => attr.isMarkoSpreadAttribute());
|
||||
let ofInterest = false;
|
||||
if (tagDef) {
|
||||
writeTemplate(tag, `<${tagDef.name}`);
|
||||
for (const attr of attrs as NodePath<t.MarkoAttribute>[]) {
|
||||
const name = attr.node.name;
|
||||
|
||||
const value = attr.get("value");
|
||||
const { confident, value: computed } = value.evaluate();
|
||||
|
||||
// special handling of class/style??
|
||||
if (confident) {
|
||||
writeTemplate(tag, getHTMLRuntime(tag).attr(name, computed));
|
||||
} else {
|
||||
if (!ofInterest) {
|
||||
ofInterest = true;
|
||||
writeWalks(tag, Walks.GET);
|
||||
writeHydrate(tag, t.expressionStatement(callRuntime(tag, "walk")));
|
||||
}
|
||||
writeHydrate(
|
||||
tag,
|
||||
t.expressionStatement(
|
||||
callRuntime(tag, "attr", t.stringLiteral(name), attr.node.value!)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let emptyBody = false;
|
||||
|
||||
if (tagDef && tagDef.parseOptions?.openTagOnly) {
|
||||
switch (tagDef.htmlType) {
|
||||
case "svg":
|
||||
case "math":
|
||||
writeTemplate(tag, `/>`);
|
||||
break;
|
||||
default:
|
||||
writeTemplate(tag, `>`);
|
||||
break;
|
||||
}
|
||||
emptyBody = true;
|
||||
} else if (tag.node.body.body.length) {
|
||||
writeTemplate(tag, `>`);
|
||||
if (tag.node.body.body.length === 1) setOnlyChild(tag);
|
||||
} else {
|
||||
writeTemplate(tag, `></${tagDef.name}>`);
|
||||
emptyBody = true;
|
||||
}
|
||||
|
||||
if (emptyBody) {
|
||||
writeWalks(tag, Walks.NEXT);
|
||||
tag.remove();
|
||||
} else writeWalks(tag, Walks.ENTER);
|
||||
}
|
||||
}
|
||||
|
||||
export function exit(tag: NodePath<t.MarkoTag>) {
|
||||
// TODO
|
||||
const tagDef = getTagDef(tag);
|
||||
if (tagDef && tagDef.name) writeTemplate(tag, `</${tagDef.name}>`);
|
||||
writeWalks(tag, Walks.EXIT);
|
||||
clearOnlyChild(tag);
|
||||
tag.remove();
|
||||
}
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
import { types as t, NodePath } from "@marko/babel-types";
|
||||
import { writeHTML } from "./util/html-write";
|
||||
import {
|
||||
Walks,
|
||||
writeTemplate,
|
||||
writeWalks,
|
||||
markTextSiblings,
|
||||
checkLastStatic
|
||||
} from "./util/dom-writer";
|
||||
import { isOutputHTML } from "./util/marko-config";
|
||||
|
||||
export default function (text: NodePath<t.MarkoText>) {
|
||||
if (isOutputHTML(text)) {
|
||||
writeHTML(text)`${text.node.value}`;
|
||||
} else {
|
||||
// TODO
|
||||
writeTemplate(text, text.node.value);
|
||||
if (checkLastStatic(text)) {
|
||||
writeWalks(text, Walks.NEXT);
|
||||
}
|
||||
markTextSiblings(text);
|
||||
}
|
||||
|
||||
text.remove();
|
||||
|
||||
54
packages/translator/src/util/dom-export.ts
Normal file
54
packages/translator/src/util/dom-export.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { NodePath, Program, types as t } from "@marko/babel-types";
|
||||
import { callRuntime } from "./runtime";
|
||||
import { encodeWalks } from "./walks";
|
||||
|
||||
export function writeExports(path: NodePath<Program>) {
|
||||
const template = t.identifier("template");
|
||||
const walks = t.identifier("walks");
|
||||
const hydrate = t.identifier("hydrate");
|
||||
// template
|
||||
path.node.body.push(
|
||||
t.exportNamedDeclaration(
|
||||
t.variableDeclaration("const", [
|
||||
t.variableDeclarator(
|
||||
template,
|
||||
t.stringLiteral(path.state.template || "")
|
||||
)
|
||||
])
|
||||
),
|
||||
t.exportNamedDeclaration(
|
||||
t.variableDeclaration("const", [
|
||||
t.variableDeclarator(
|
||||
walks,
|
||||
t.stringLiteral(encodeWalks(path.state.walks))
|
||||
)
|
||||
])
|
||||
),
|
||||
t.exportNamedDeclaration(
|
||||
t.variableDeclaration("const", [
|
||||
t.variableDeclarator(
|
||||
hydrate,
|
||||
callRuntime(
|
||||
path,
|
||||
"register",
|
||||
t.stringLiteral(path.hub.file.metadata.marko.id),
|
||||
t.arrowFunctionExpression(
|
||||
[t.identifier("input")],
|
||||
t.blockStatement(path.state.hydrate)
|
||||
)
|
||||
)
|
||||
)
|
||||
])
|
||||
),
|
||||
t.exportDefaultDeclaration(
|
||||
callRuntime(
|
||||
path,
|
||||
"createRenderFn",
|
||||
template,
|
||||
walks,
|
||||
t.arrayExpression(),
|
||||
hydrate
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
67
packages/translator/src/util/dom-writer.ts
Normal file
67
packages/translator/src/util/dom-writer.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
NodePath,
|
||||
Node,
|
||||
MarkoTag,
|
||||
MarkoText,
|
||||
types as t,
|
||||
MarkoPlaceholder
|
||||
} from "@marko/babel-types";
|
||||
|
||||
import { Walks } from "./walks";
|
||||
export { Walks } from "./walks";
|
||||
|
||||
export function writeTemplate(path: NodePath<any>, s: string) {
|
||||
path.state.template += s;
|
||||
}
|
||||
|
||||
export function writeHydrate(path: NodePath<any>, code: Node) {
|
||||
path.state.hydrate.push(code);
|
||||
}
|
||||
|
||||
export function writeWalks(path: NodePath<any>, code: Walks) {
|
||||
path.state.walks.push(code);
|
||||
}
|
||||
|
||||
export function checkLastStatic(path: NodePath<any>) {
|
||||
let i = +path.key;
|
||||
let temp: NodePath<any>;
|
||||
while ((temp = path.getSibling(++i)).node) {
|
||||
if (t.isMarkoPlaceholder(temp)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function checkNextMarker(path: NodePath<any>) {
|
||||
let i = +path.key;
|
||||
let temp: NodePath<any>;
|
||||
while ((temp = path.getSibling(++i)).node) {
|
||||
if (!t.isMarkoPlaceholder(temp)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function markTextSiblings(path: NodePath<MarkoText>) {
|
||||
const sibling = path.getSibling(+path.key + 1);
|
||||
if (sibling && t.isMarkoPlaceholder(sibling.node))
|
||||
path.state.precedingText = true;
|
||||
}
|
||||
|
||||
export function needsPlaceholderMarker(path: NodePath<MarkoPlaceholder>) {
|
||||
if (!path.state.precedingText) return false;
|
||||
const sibling = path.getSibling(+path.key + 1);
|
||||
if (sibling && t.isMarkoText(sibling.node)) return true;
|
||||
path.state.precedingText = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function setOnlyChild(path: NodePath<MarkoTag>) {
|
||||
path.state.onlyChild = true;
|
||||
}
|
||||
|
||||
export function clearOnlyChild(path: NodePath<MarkoTag>) {
|
||||
path.state.onlyChild = false;
|
||||
}
|
||||
|
||||
export function isOnlyChild(path: NodePath<any>) {
|
||||
return path.state.onlyChild;
|
||||
}
|
||||
@ -8,7 +8,8 @@ export function importRuntime<T extends t.Node>(
|
||||
path: NodePath<T>,
|
||||
name: string
|
||||
) {
|
||||
return importNamed(path.hub.file, getRuntimePath(path), name);
|
||||
const { output } = getMarkoOpts(path);
|
||||
return importNamed(path.hub.file, getRuntimePath(path, output), name);
|
||||
}
|
||||
|
||||
export function callRuntime<
|
||||
@ -25,19 +26,28 @@ export function callRuntime<
|
||||
}
|
||||
|
||||
export function getHTMLRuntime<T extends t.Node>(path: NodePath<T>) {
|
||||
return getRuntime(path) as typeof import("@marko/runtime-fluurt/src/html");
|
||||
return getRuntime(
|
||||
path,
|
||||
"html"
|
||||
) as typeof import("@marko/runtime-fluurt/src/html");
|
||||
}
|
||||
|
||||
export function getDOMRuntime<T extends t.Node>(path: NodePath<T>) {
|
||||
return getRuntime(path) as typeof import("@marko/runtime-fluurt/src/dom");
|
||||
return getRuntime(
|
||||
path,
|
||||
"dom"
|
||||
) as typeof import("@marko/runtime-fluurt/src/dom");
|
||||
}
|
||||
|
||||
function getRuntime<T extends t.Node>(path: NodePath<T>): unknown {
|
||||
return require(getRuntimePath(path));
|
||||
function getRuntime<T extends t.Node>(
|
||||
path: NodePath<T>,
|
||||
output: string
|
||||
): unknown {
|
||||
return require(getRuntimePath(path, output));
|
||||
}
|
||||
|
||||
function getRuntimePath<T extends t.Node>(path: NodePath<T>) {
|
||||
const { output, optimize } = getMarkoOpts(path);
|
||||
function getRuntimePath<T extends t.Node>(path: NodePath<T>, output: string) {
|
||||
const { optimize } = getMarkoOpts(path);
|
||||
return `@marko/runtime-fluurt/${
|
||||
USE_SOURCE_RUNTIME ? "src" : optimize ? "dist" : "debug"
|
||||
}/${output}`;
|
||||
|
||||
140
packages/translator/src/util/walks.ts
Normal file
140
packages/translator/src/util/walks.ts
Normal file
@ -0,0 +1,140 @@
|
||||
const enum WalkCodes {
|
||||
Get = 33, // !
|
||||
Before = 35, // #
|
||||
After = 36, // $
|
||||
Inside = 37, // %
|
||||
Replace = 38, // &
|
||||
Out = 39,
|
||||
OutEnd = 49,
|
||||
Over = 58,
|
||||
OverEnd = 91,
|
||||
Next = 93,
|
||||
NextEnd = 126
|
||||
}
|
||||
|
||||
const get = String.fromCharCode(WalkCodes.Get);
|
||||
const before = String.fromCharCode(WalkCodes.Before);
|
||||
const after = String.fromCharCode(WalkCodes.After);
|
||||
const replace = String.fromCharCode(WalkCodes.Replace);
|
||||
const inside = String.fromCharCode(WalkCodes.Inside);
|
||||
|
||||
function next(number: number) {
|
||||
return toCharString(number, WalkCodes.Next, WalkCodes.NextEnd);
|
||||
}
|
||||
function over(number: number) {
|
||||
return toCharString(number, WalkCodes.Over, WalkCodes.OverEnd);
|
||||
}
|
||||
function out(number: number) {
|
||||
return toCharString(number, WalkCodes.Out, WalkCodes.OutEnd);
|
||||
}
|
||||
|
||||
function toCharString(
|
||||
number: number,
|
||||
startCharCode: number,
|
||||
endCharCode: number
|
||||
) {
|
||||
const total = endCharCode - startCharCode + 1;
|
||||
let value = "";
|
||||
while (number > total) {
|
||||
value += String.fromCharCode(endCharCode);
|
||||
number -= total;
|
||||
}
|
||||
return value + String.fromCharCode(startCharCode + number - 1);
|
||||
}
|
||||
|
||||
export const enum Walks {
|
||||
ENTER,
|
||||
EXIT,
|
||||
NEXT,
|
||||
OVER,
|
||||
GET,
|
||||
BEFORE,
|
||||
AFTER,
|
||||
INSIDE,
|
||||
REPLACE
|
||||
}
|
||||
|
||||
type WalkInfo = {
|
||||
hasAction: boolean;
|
||||
sequence: Walks[];
|
||||
earlyExits: number;
|
||||
};
|
||||
|
||||
const lookup = {
|
||||
[Walks.EXIT]: out,
|
||||
[Walks.NEXT]: next,
|
||||
[Walks.OVER]: over,
|
||||
[Walks.GET]: get,
|
||||
[Walks.BEFORE]: before,
|
||||
[Walks.AFTER]: after,
|
||||
[Walks.INSIDE]: inside,
|
||||
[Walks.REPLACE]: replace
|
||||
};
|
||||
|
||||
function resolveSequence(walks: Walks[]) {
|
||||
let current: Walks;
|
||||
let count = 0;
|
||||
let results = "";
|
||||
for (let i = 0, len = walks.length; i < len; i++) {
|
||||
const w = walks[i];
|
||||
if (w !== current!) {
|
||||
if (current!) {
|
||||
results += (lookup[current] as any)(count);
|
||||
}
|
||||
current = w;
|
||||
count = 0;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
if (current!) results += (lookup[current] as any)(count);
|
||||
return results;
|
||||
}
|
||||
|
||||
export function encodeWalks(walks: Walks[]): string {
|
||||
let results = "";
|
||||
let inner: WalkInfo;
|
||||
const current: WalkInfo[] = [
|
||||
{
|
||||
hasAction: false,
|
||||
sequence: [],
|
||||
earlyExits: 0
|
||||
}
|
||||
];
|
||||
|
||||
for (let i = 0, len = walks.length; i < len; i++) {
|
||||
switch (walks[i]) {
|
||||
case Walks.NEXT:
|
||||
current[0].sequence.push(Walks.NEXT);
|
||||
break;
|
||||
case Walks.ENTER:
|
||||
current.unshift({
|
||||
hasAction: false,
|
||||
sequence: [...current[0].sequence, Walks.NEXT],
|
||||
earlyExits: 0
|
||||
});
|
||||
break;
|
||||
case Walks.EXIT:
|
||||
inner = current.shift()!;
|
||||
if (!inner.hasAction) {
|
||||
current[0].sequence.push(Walks.OVER);
|
||||
} else {
|
||||
current[0].hasAction = true;
|
||||
current[0].earlyExits = inner.earlyExits;
|
||||
for (let j = 0, len = ++current[0].earlyExits; j < len; j++)
|
||||
current[0].sequence.push(Walks.EXIT);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
current[0].hasAction = true;
|
||||
if (current[0].sequence.length) {
|
||||
results += resolveSequence(current[0].sequence);
|
||||
current[0].sequence = [];
|
||||
current[0].earlyExits = 0;
|
||||
}
|
||||
results += lookup[walks[i]];
|
||||
}
|
||||
}
|
||||
results += resolveSequence(current[0].sequence);
|
||||
console.log(results, next(1) + after + out(1));
|
||||
return results;
|
||||
}
|
||||
1
packages/translator/test/fixtures/at-tag-inside-if-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/at-tag-inside-if-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/at-tags-dynamic-and-static/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/at-tags-dynamic-and-static/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/at-tags-dynamic-tag-parent/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/at-tags-dynamic-tag-parent/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/at-tags-dynamic-with-params/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/at-tags-dynamic-with-params/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/at-tags-dynamic/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/at-tags-dynamic/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/at-tags/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/at-tags/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/attr-boolean/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/attr-boolean/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/attr-class/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/attr-class/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/attr-escape/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/attr-escape/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/attr-falsey/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/attr-falsey/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/attr-scoped/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/attr-scoped/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/attr-style/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/attr-style/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/attr-template-literal-escape/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/attr-template-literal-escape/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/cdata/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/cdata/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/comments/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/comments/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/const-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/const-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/context-tag-from-relative-path/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/context-tag-from-relative-path/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/context-tag-from-self/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/context-tag-from-self/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/context-tag-from-tag-name/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/context-tag-from-tag-name/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/custom-tag-child-analyze/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/custom-tag-child-analyze/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/custom-tag-parameters/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/custom-tag-parameters/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/custom-tag-render-body/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/custom-tag-render-body/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/custom-tag-separate-assets/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/custom-tag-separate-assets/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/custom-tag-template/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/custom-tag-template/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/custom-tag-var/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/custom-tag-var/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/declaration/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/declaration/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/doctype/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/doctype/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/dynamic-tag-name/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/dynamic-tag-name/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
@ -0,0 +1 @@
|
||||
(intermediate value)(intermediate value)(intermediate value) is not a function
|
||||
1
packages/translator/test/fixtures/dynamic-tag-var/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/dynamic-tag-var/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/dynamic-tag-var/snapshots/html-rendered-error-expected.txt
vendored
Normal file
1
packages/translator/test/fixtures/dynamic-tag-var/snapshots/html-rendered-error-expected.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
child is not a function
|
||||
1
packages/translator/test/fixtures/entities/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/entities/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/event-handlers/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/event-handlers/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/event-handlers/snapshots/html-rendered-error-expected.txt
vendored
Normal file
1
packages/translator/test/fixtures/event-handlers/snapshots/html-rendered-error-expected.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
_child is not a function
|
||||
1
packages/translator/test/fixtures/for-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/for-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/hello-dynamic/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/hello-dynamic/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/html-entity/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/html-entity/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/if-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/if-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/import-tag-conflict/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/import-tag-conflict/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/import-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/import-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/let-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/let-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/placeholders/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/placeholders/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/style-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/style-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/tag-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/tag-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
1
packages/translator/test/fixtures/yield-tag/config.ts
vendored
Normal file
1
packages/translator/test/fixtures/yield-tag/config.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const skip = ["dom-compiled", "dom-rendered"];
|
||||
@ -6,6 +6,8 @@ import stripAnsi from "strip-ansi";
|
||||
import { compileFile } from "@marko/compiler";
|
||||
import { install } from "marko/node-require";
|
||||
import * as translator from "../src";
|
||||
import snapshot from "./utils/snapshot";
|
||||
import renderAndTrackMutations from "./utils/render-and-track-mutations";
|
||||
|
||||
const baseConfig = {
|
||||
translator,
|
||||
@ -29,10 +31,10 @@ install({
|
||||
|
||||
describe("translator", () => {
|
||||
autotest("fixtures", {
|
||||
"html-compiled": runCompileTest({ output: "html" }),
|
||||
"html-rendered": runHTMLRenderTest,
|
||||
"dom-compiled": () => {},
|
||||
"dom-rendered": () => {}
|
||||
"html-compiled": runTestWithConfig(runCompileTest({ output: "html" })),
|
||||
"html-rendered": runTestWithConfig(runHTMLRenderTest),
|
||||
"dom-compiled": runTestWithConfig(runCompileTest({ output: "dom" })),
|
||||
"dom-rendered": runTestWithConfig(runDOMRenderTest)
|
||||
});
|
||||
});
|
||||
|
||||
@ -53,6 +55,7 @@ function runCompileTest(config: { output: string }) {
|
||||
|
||||
try {
|
||||
output = (await compileFile(templateFile, compilerConfig)).code;
|
||||
// .replace(/"\.\//g, '"../')
|
||||
} catch (compileSnapshotErr) {
|
||||
try {
|
||||
snapshot(stripCwd(stripAnsi(compileSnapshotErr.message)), {
|
||||
@ -78,9 +81,9 @@ function runCompileTest(config: { output: string }) {
|
||||
};
|
||||
}
|
||||
|
||||
function runHTMLRenderTest({ mode, test, resolve, snapshot }) {
|
||||
function runHTMLRenderTest({ mode, test, resolve, snapshot }, { inputHTML }) {
|
||||
// const templateFile = resolve("./snapshots/html-compiled-expected.js");
|
||||
const templateFile = resolve("template.marko");
|
||||
const inputFile = resolve("input.ts");
|
||||
const snapshotsDir = resolve("snapshots");
|
||||
const name = `snapshots${path.sep + mode}`;
|
||||
|
||||
@ -88,18 +91,11 @@ function runHTMLRenderTest({ mode, test, resolve, snapshot }) {
|
||||
await ensureDir(snapshotsDir);
|
||||
|
||||
const { render } = await import(templateFile);
|
||||
let input: Record<string, unknown>;
|
||||
let html = "";
|
||||
|
||||
try {
|
||||
input = await import(inputFile);
|
||||
} catch {
|
||||
input = {};
|
||||
}
|
||||
|
||||
try {
|
||||
await render(
|
||||
input,
|
||||
inputHTML || {},
|
||||
new Writable({
|
||||
write(chunk: string) {
|
||||
html += chunk;
|
||||
@ -121,6 +117,39 @@ function runHTMLRenderTest({ mode, test, resolve, snapshot }) {
|
||||
});
|
||||
}
|
||||
|
||||
function runDOMRenderTest({ mode, test, resolve }, { inputDOM }) {
|
||||
const templateFile = resolve("snapshots/dom-compiled-expected.js");
|
||||
const snapshotsDir = resolve("snapshots");
|
||||
|
||||
test(async () => {
|
||||
await ensureDir(snapshotsDir);
|
||||
|
||||
snapshot(
|
||||
snapshotsDir,
|
||||
`${mode}.md`,
|
||||
await renderAndTrackMutations(templateFile, inputDOM)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function runTestWithConfig(fn) {
|
||||
return opts => {
|
||||
let config;
|
||||
try {
|
||||
config = require(opts.resolve("config.ts"));
|
||||
} catch {
|
||||
config = {};
|
||||
}
|
||||
|
||||
if (config.skip && config.skip.includes(opts.mode)) {
|
||||
opts.skip("Not Implemented");
|
||||
return;
|
||||
}
|
||||
|
||||
return fn(opts, config);
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureDir(dir: string) {
|
||||
try {
|
||||
await fs.promises.access(dir);
|
||||
|
||||
24
packages/translator/test/utils/create-browser.ts
Normal file
24
packages/translator/test/utils/create-browser.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { DOMWindow } from "jsdom";
|
||||
import createBrowser from "jsdom-context-require";
|
||||
|
||||
export default function (options: Parameters<typeof createBrowser>[0]) {
|
||||
// something up with extensions
|
||||
const browser = createBrowser({ ...options, extensions: require.extensions });
|
||||
const window = browser.window as DOMWindow & { MessageChannel: any };
|
||||
window.queueMicrotask = queueMicrotask;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
window.MessageChannel = (window as any).MessageChannel = class MessageChannel {
|
||||
port1: any;
|
||||
port2: any;
|
||||
constructor() {
|
||||
this.port1 = { onmessage() {} };
|
||||
this.port2 = {
|
||||
postMessage: () => {
|
||||
setImmediate(this.port1.onmessage);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
window.requestAnimationFrame = fn => setTimeout(fn);
|
||||
return browser;
|
||||
}
|
||||
29
packages/translator/test/utils/get-node-info.ts
Normal file
29
packages/translator/test/utils/get-node-info.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export function getNodePath(node: Node) {
|
||||
const parts: string[] = [];
|
||||
let cur: Node | null = node;
|
||||
while (cur) {
|
||||
const { parentNode } = cur;
|
||||
|
||||
if (!parentNode || (cur as any).TEST_ROOT) {
|
||||
break;
|
||||
}
|
||||
|
||||
let name = getTypeName(cur);
|
||||
const index = parentNode
|
||||
? (Array.from(parentNode.childNodes) as Node[]).indexOf(cur)
|
||||
: -1;
|
||||
|
||||
if (index !== -1) {
|
||||
name += `${index}`;
|
||||
}
|
||||
|
||||
parts.unshift(name);
|
||||
cur = parentNode;
|
||||
}
|
||||
|
||||
return parts.join("/");
|
||||
}
|
||||
|
||||
export function getTypeName(node: Node) {
|
||||
return node.nodeName.toLowerCase();
|
||||
}
|
||||
114
packages/translator/test/utils/render-and-track-mutations.ts
Normal file
114
packages/translator/test/utils/render-and-track-mutations.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import createBrowser from "./create-browser";
|
||||
import createMutationTracker from "./track-mutations";
|
||||
import { wait, isWait } from "./resolve";
|
||||
|
||||
const browser = createBrowser({
|
||||
dir: __dirname,
|
||||
html: ""
|
||||
});
|
||||
|
||||
const window = browser.window;
|
||||
const document = window.document;
|
||||
|
||||
const { createRenderFn, runInBatch } = browser.require(
|
||||
"@marko/runtime-fluurt/src/dom/index"
|
||||
) as typeof import("@marko/runtime-fluurt/src/dom/index");
|
||||
|
||||
interface Test {
|
||||
wait?: number;
|
||||
inputs: [
|
||||
Record<string, unknown>,
|
||||
...Array<
|
||||
| Record<string, unknown>
|
||||
| ((container: Element) => void)
|
||||
| ReturnType<typeof wait>
|
||||
>
|
||||
];
|
||||
default: ReturnType<typeof createRenderFn>;
|
||||
html: string;
|
||||
FAILS_HYDRATE?: boolean;
|
||||
}
|
||||
|
||||
export default async function renderAndGetMutations(
|
||||
test: string,
|
||||
inputs = []
|
||||
): Promise<string> {
|
||||
if (!Array.isArray(inputs)) inputs = [inputs];
|
||||
const { default: render } = browser.require(test) as Test;
|
||||
const [firstInput] = inputs;
|
||||
const container = Object.assign(document.createElement("div"), {
|
||||
TEST_ROOT: true
|
||||
});
|
||||
const tracker = createMutationTracker(window, container);
|
||||
|
||||
document.body.appendChild(container);
|
||||
|
||||
try {
|
||||
tracker.beginUpdate();
|
||||
|
||||
const instance = render(firstInput);
|
||||
container.appendChild(instance);
|
||||
|
||||
// const initialHTML = container.innerHTML;
|
||||
tracker.logUpdate(firstInput);
|
||||
|
||||
for (const update of inputs.slice(1)) {
|
||||
if (isWait(update)) {
|
||||
await update();
|
||||
} else {
|
||||
tracker.beginUpdate();
|
||||
if (typeof update === "function") {
|
||||
runInBatch(() => update(container));
|
||||
} else {
|
||||
instance.rerender(update);
|
||||
}
|
||||
tracker.logUpdate(update);
|
||||
}
|
||||
}
|
||||
|
||||
// if (!FAILS_HYDRATE) {
|
||||
// const inputSignal = source(firstInput);
|
||||
// (window as any).M$c = [[0, id, dynamicKeys(inputSignal, renderer.input)]];
|
||||
// container.innerHTML = `<!M$0>${initialHTML}<!M$0/>`;
|
||||
// container.insertBefore(document.createTextNode(""), container.firstChild);
|
||||
// tracker.dropUpdate();
|
||||
// init();
|
||||
|
||||
// tracker.logUpdate(firstInput);
|
||||
// const logs = tracker.getRawLogs();
|
||||
// logs[logs.length - 1] = "--- Hydrate ---\n" + logs[logs.length - 1];
|
||||
|
||||
// // Hydrate should end up with the same html as client side render.
|
||||
// assert.equal(container.innerHTML, initialHTML);
|
||||
|
||||
// // Run the same updates after hydrate and ensure the same mutations.
|
||||
// let resultIndex = 0;
|
||||
// for (const update of inputs.slice(1)) {
|
||||
// if (wait) {
|
||||
// await resolveAfter(null, wait);
|
||||
// }
|
||||
// if (typeof update === "function") {
|
||||
// update(container);
|
||||
// } else {
|
||||
// const batch = beginBatch();
|
||||
// set(inputSignal, update);
|
||||
// endBatch(batch);
|
||||
// }
|
||||
|
||||
// assert.equal(
|
||||
// tracker.getUpdate(update),
|
||||
// tracker.getRawLogs()[++resultIndex]
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (wait) {
|
||||
// await resolveAfter(null, wait);
|
||||
// }
|
||||
// }
|
||||
|
||||
return tracker.getLogs();
|
||||
} finally {
|
||||
tracker.cleanup();
|
||||
document.body.removeChild(container);
|
||||
}
|
||||
}
|
||||
27
packages/translator/test/utils/resolve.ts
Normal file
27
packages/translator/test/utils/resolve.ts
Normal file
@ -0,0 +1,27 @@
|
||||
const TIMEOUT_MULTIPLIER = 16;
|
||||
|
||||
export function wait(timeout: number) {
|
||||
return Object.assign(() => resolveAfter(`wait:${timeout}`, timeout), {
|
||||
wait: true
|
||||
});
|
||||
}
|
||||
|
||||
export function isWait(value: any): value is ReturnType<typeof wait> {
|
||||
return value.wait;
|
||||
}
|
||||
|
||||
export function resolveAfter<T>(value: T, timeout: number) {
|
||||
const p = new Promise(resolve =>
|
||||
setTimeout(() => resolve(value), timeout * TIMEOUT_MULTIPLIER)
|
||||
) as Promise<T>;
|
||||
|
||||
return Object.assign(p, { value });
|
||||
}
|
||||
|
||||
export function rejectAfter<T extends Error>(value: T, timeout: number) {
|
||||
const p = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(value), timeout * TIMEOUT_MULTIPLIER)
|
||||
) as Promise<never>;
|
||||
|
||||
return Object.assign(p, { value });
|
||||
}
|
||||
48
packages/translator/test/utils/snapshot.ts
Normal file
48
packages/translator/test/utils/snapshot.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import assert from "assert";
|
||||
|
||||
export default function snapshot(
|
||||
dir: string,
|
||||
file: string,
|
||||
data: string,
|
||||
originalError?: Error
|
||||
) {
|
||||
const parsed = path.parse(file);
|
||||
const ext = parsed.ext;
|
||||
let name = parsed.name;
|
||||
|
||||
if (name) {
|
||||
name += ".";
|
||||
}
|
||||
|
||||
try {
|
||||
fs.accessSync(dir);
|
||||
} catch {
|
||||
fs.mkdirSync(dir);
|
||||
}
|
||||
|
||||
const expectedFile = path.join(dir, `${name}expected${ext}`);
|
||||
const actualFile = path.join(dir, `${name}actual${ext}`);
|
||||
|
||||
fs.writeFileSync(actualFile, data, "utf-8");
|
||||
|
||||
if (process.env.UPDATE_EXPECTATIONS) {
|
||||
fs.writeFileSync(expectedFile, data, "utf-8");
|
||||
} else {
|
||||
const expected = fs.existsSync(expectedFile)
|
||||
? fs.readFileSync(expectedFile, "utf-8")
|
||||
: "";
|
||||
|
||||
try {
|
||||
assert.equal(data, expected);
|
||||
} catch (err) {
|
||||
err.snapshot = true;
|
||||
err.name = err.name.replace(" [ERR_ASSERTION]", "");
|
||||
err.stack = "";
|
||||
err.message = path.relative(process.cwd(), actualFile);
|
||||
|
||||
throw originalError || err;
|
||||
}
|
||||
}
|
||||
}
|
||||
131
packages/translator/test/utils/track-mutations.ts
Normal file
131
packages/translator/test/utils/track-mutations.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import format from "pretty-format";
|
||||
import { getNodePath, getTypeName } from "./get-node-info";
|
||||
|
||||
const { DOMElement, DOMCollection } = format.plugins;
|
||||
|
||||
export default function createMutationTracker(window, container) {
|
||||
const result: string[] = [];
|
||||
let currentRecords: unknown[] | null = null;
|
||||
const observer = new window.MutationObserver(records => {
|
||||
if (currentRecords) {
|
||||
currentRecords = currentRecords.concat(records);
|
||||
} else {
|
||||
result.push(getStatusString(container, records, "ASYNC"));
|
||||
}
|
||||
});
|
||||
observer.observe(container, {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
characterData: true,
|
||||
characterDataOldValue: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
return {
|
||||
beginUpdate() {
|
||||
currentRecords = [];
|
||||
},
|
||||
dropUpdate() {
|
||||
observer.takeRecords();
|
||||
currentRecords = null;
|
||||
},
|
||||
getUpdate(update) {
|
||||
if (currentRecords) {
|
||||
currentRecords = currentRecords.concat(observer.takeRecords());
|
||||
} else {
|
||||
currentRecords = observer.takeRecords();
|
||||
}
|
||||
const updateString = getStatusString(container, currentRecords, update);
|
||||
currentRecords = null;
|
||||
return updateString;
|
||||
},
|
||||
log(message) {
|
||||
result.push(message);
|
||||
},
|
||||
logUpdate(update) {
|
||||
result.push(this.getUpdate(update));
|
||||
},
|
||||
getRawLogs() {
|
||||
return result;
|
||||
},
|
||||
getLogs() {
|
||||
return result.join("\n\n\n");
|
||||
},
|
||||
cleanup() {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getStatusString(container: HTMLDivElement, changes, update) {
|
||||
const clone = container.cloneNode(true);
|
||||
clone.normalize();
|
||||
|
||||
return `# Render ${
|
||||
typeof update === "function"
|
||||
? `\n${update
|
||||
.toString()
|
||||
.replace(/^.*?{\s*([\s\S]*?)\s*}.*?$/, "$1")
|
||||
.replace(/^ {4}/gm, "")}\n`
|
||||
: JSON.stringify(update)
|
||||
}\n\`\`\`html\n${Array.from(clone.childNodes)
|
||||
.map(child =>
|
||||
format(child, {
|
||||
plugins: [DOMElement, DOMCollection]
|
||||
}).trim()
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.trim()}\n\`\`\`\n\n# Mutations\n\`\`\`\n${changes
|
||||
.map(formatMutationRecord)
|
||||
.join("\n")}\n\`\`\``;
|
||||
}
|
||||
|
||||
function formatMutationRecord(record: MutationRecord) {
|
||||
const { target, oldValue } = record;
|
||||
|
||||
switch (record.type) {
|
||||
case "attributes": {
|
||||
const { attributeName } = record;
|
||||
const newValue = (target as HTMLElement).getAttribute(
|
||||
attributeName as string
|
||||
);
|
||||
return `${getNodePath(target)}: attr(${attributeName}) ${JSON.stringify(
|
||||
oldValue
|
||||
)} => ${JSON.stringify(newValue)}`;
|
||||
}
|
||||
|
||||
case "characterData": {
|
||||
return `${getNodePath(target)}: ${JSON.stringify(
|
||||
oldValue
|
||||
)} => ${JSON.stringify(target.nodeValue)}`;
|
||||
}
|
||||
|
||||
case "childList": {
|
||||
const { removedNodes, addedNodes, previousSibling, nextSibling } = record;
|
||||
const details: string[] = [];
|
||||
if (removedNodes.length) {
|
||||
const relativeNode = previousSibling || nextSibling || target;
|
||||
const position =
|
||||
relativeNode === previousSibling
|
||||
? "after"
|
||||
: relativeNode === nextSibling
|
||||
? "before"
|
||||
: "in";
|
||||
details.push(
|
||||
`removed ${Array.from(removedNodes)
|
||||
.map(getTypeName)
|
||||
.join(", ")} ${position} ${getNodePath(relativeNode)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (addedNodes.length) {
|
||||
details.push(
|
||||
`inserted ${Array.from(addedNodes).map(getNodePath).join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
return details.join("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user