ferrovue 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client-render.d.ts +4 -0
- package/dist/client-render.js +34 -6
- package/dist/conformance.js +2 -2
- package/dist/hydration.js +2 -2
- package/package.json +1 -1
package/dist/client-render.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ import { type App, type Plugin } from "vue";
|
|
|
6
6
|
* `<input>`'s `value` and `checked`, a `<textarea>`'s `value`, an `<option>`'s `selected`) by its
|
|
7
7
|
* property, which Vue's client sets where the server writes the attribute. */
|
|
8
8
|
export declare function renderedShape(container: Element): string;
|
|
9
|
+
/** `renderedShape` of markup as the browser parsed it, untouched since: an `<option>`'s state is
|
|
10
|
+
* the selection the HTML parser gives it, from the `selected` attributes (the last one in a single
|
|
11
|
+
* `<select>`, or its first enabled option without one), which happy-dom's property misreports. */
|
|
12
|
+
export declare function parsedShape(container: Element): string;
|
|
9
13
|
/** A plugin that takes what the app is mounted on, and its `renderedShape` as `mount` returns,
|
|
10
14
|
* before any update an `onMounted` hook scheduled has run. */
|
|
11
15
|
export declare function shapeAtMount(): {
|
package/dist/client-render.js
CHANGED
|
@@ -3,7 +3,29 @@ import { Fragment, Static } from "vue";
|
|
|
3
3
|
import { settled, stillLoading } from "./settle.js";
|
|
4
4
|
const escapeText = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
5
5
|
const STATE_ATTRS = { INPUT: ["value", "checked"], TEXTAREA: ["value"], OPTION: ["selected"] };
|
|
6
|
-
function
|
|
6
|
+
function selectOf(option) {
|
|
7
|
+
const parent = option.parentElement;
|
|
8
|
+
const select = parent?.tagName === "OPTGROUP" ? parent.parentElement : parent;
|
|
9
|
+
return select?.tagName === "SELECT" ? select : null;
|
|
10
|
+
}
|
|
11
|
+
function optionsOf(select) {
|
|
12
|
+
return [...select.children].flatMap((child) => (child.tagName === "OPTION" ? [child] : child.tagName === "OPTGROUP" ? [...child.children].filter((c) => c.tagName === "OPTION") : []));
|
|
13
|
+
}
|
|
14
|
+
function parsedSelected(option) {
|
|
15
|
+
const select = selectOf(option);
|
|
16
|
+
if (!select || select.hasAttribute("multiple") || Number(select.getAttribute("size")) > 1)
|
|
17
|
+
return option.hasAttribute("selected");
|
|
18
|
+
const options = optionsOf(select);
|
|
19
|
+
const disabled = (o) => o.hasAttribute("disabled") || (o.parentElement?.tagName === "OPTGROUP" && o.parentElement.hasAttribute("disabled"));
|
|
20
|
+
const chosen = options.findLast((o) => o.hasAttribute("selected")) ?? options.find((o) => !disabled(o));
|
|
21
|
+
return option === chosen;
|
|
22
|
+
}
|
|
23
|
+
function stateOf(el, name, parsed) {
|
|
24
|
+
if (parsed && el.tagName === "OPTION")
|
|
25
|
+
return parsedSelected(el);
|
|
26
|
+
return el[name];
|
|
27
|
+
}
|
|
28
|
+
function element(el, scratch, parsed) {
|
|
7
29
|
const state = STATE_ATTRS[el.tagName] ?? [];
|
|
8
30
|
const attrs = el
|
|
9
31
|
.getAttributeNames()
|
|
@@ -17,8 +39,8 @@ function element(el, scratch) {
|
|
|
17
39
|
if (style)
|
|
18
40
|
attrs.push(` style=${JSON.stringify(style)}`);
|
|
19
41
|
for (const name of state)
|
|
20
|
-
attrs.push(` .${name}=${JSON.stringify(el
|
|
21
|
-
const children = el.tagName === "TEXTAREA" ? "" : nodes(el.childNodes, scratch);
|
|
42
|
+
attrs.push(` .${name}=${JSON.stringify(stateOf(el, name, parsed))}`);
|
|
43
|
+
const children = el.tagName === "TEXTAREA" ? "" : nodes(el.childNodes, scratch, parsed);
|
|
22
44
|
return `<${el.localName}${attrs.join("")}>${children}</${el.localName}>`;
|
|
23
45
|
}
|
|
24
46
|
function styleShape(text, scratch) {
|
|
@@ -33,7 +55,7 @@ function styleShape(text, scratch) {
|
|
|
33
55
|
}
|
|
34
56
|
return [...own, ...custom.toSorted()].join(" ");
|
|
35
57
|
}
|
|
36
|
-
function nodes(list, scratch) {
|
|
58
|
+
function nodes(list, scratch, parsed) {
|
|
37
59
|
let out = "";
|
|
38
60
|
let text = "";
|
|
39
61
|
for (const node of list) {
|
|
@@ -43,7 +65,7 @@ function nodes(list, scratch) {
|
|
|
43
65
|
}
|
|
44
66
|
if (node.nodeType !== 1)
|
|
45
67
|
continue;
|
|
46
|
-
out += escapeText(text) + element(node, scratch);
|
|
68
|
+
out += escapeText(text) + element(node, scratch, parsed);
|
|
47
69
|
text = "";
|
|
48
70
|
}
|
|
49
71
|
return out + escapeText(text);
|
|
@@ -55,7 +77,13 @@ function nodes(list, scratch) {
|
|
|
55
77
|
* `<input>`'s `value` and `checked`, a `<textarea>`'s `value`, an `<option>`'s `selected`) by its
|
|
56
78
|
* property, which Vue's client sets where the server writes the attribute. */
|
|
57
79
|
export function renderedShape(container) {
|
|
58
|
-
return nodes(container.childNodes, container.ownerDocument.createElement("div"));
|
|
80
|
+
return nodes(container.childNodes, container.ownerDocument.createElement("div"), false);
|
|
81
|
+
}
|
|
82
|
+
/** `renderedShape` of markup as the browser parsed it, untouched since: an `<option>`'s state is
|
|
83
|
+
* the selection the HTML parser gives it, from the `selected` attributes (the last one in a single
|
|
84
|
+
* `<select>`, or its first enabled option without one), which happy-dom's property misreports. */
|
|
85
|
+
export function parsedShape(container) {
|
|
86
|
+
return nodes(container.childNodes, container.ownerDocument.createElement("div"), true);
|
|
59
87
|
}
|
|
60
88
|
/** A plugin that takes what the app is mounted on, and its `renderedShape` as `mount` returns,
|
|
61
89
|
* before any update an `onMounted` hook scheduled has run. */
|
package/dist/conformance.js
CHANGED
|
@@ -8,7 +8,7 @@ import { allPages, fileRoutes, pagesFolder, routeEntries } from "./file-routes.j
|
|
|
8
8
|
import { fixtureApp, peer, readFixture } from "./fixture.js";
|
|
9
9
|
import { headRendered, settled } from "./settle.js";
|
|
10
10
|
import { attachSsrRender } from "./ssr.js";
|
|
11
|
-
import { applyCssVars, renderDifference, renderedShape } from "./client-render.js";
|
|
11
|
+
import { applyCssVars, parsedShape, renderDifference, renderedShape } from "./client-render.js";
|
|
12
12
|
export const TELEPORTS = "<!--fv-teleports-->";
|
|
13
13
|
export const HEAD = "<!--fv-head-->";
|
|
14
14
|
/** The head a recorded render wrote after its body, if it wrote one. */
|
|
@@ -204,7 +204,7 @@ export async function clientRenderDifference(app, html) {
|
|
|
204
204
|
throw new Error("rendering a fixture on the client needs a DOM: run the suite with `environment: \"happy-dom\"` (or jsdom)");
|
|
205
205
|
document.body.innerHTML = hydrationBody(html);
|
|
206
206
|
const root = document.getElementById("root");
|
|
207
|
-
const expected =
|
|
207
|
+
const expected = parsedShape(root);
|
|
208
208
|
root.replaceChildren();
|
|
209
209
|
app.config.warnHandler = () => { };
|
|
210
210
|
const { warn } = console;
|
package/dist/hydration.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mountPage, readPage, renderPage } from "./client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { parsedShape, renderDifference, shapeAtMount } from "./client-render.js";
|
|
3
3
|
import { settled } from "./settle.js";
|
|
4
4
|
function recordScript(id, record) {
|
|
5
5
|
const json = JSON.stringify(record).replace(/[<>&\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
@@ -96,7 +96,7 @@ export async function renderRecordedPage(page, layout, components, options = {})
|
|
|
96
96
|
const container = typeof selector === "string" ? doc.querySelector(selector) : selector;
|
|
97
97
|
if (!container)
|
|
98
98
|
throw new Error(`the recorded page has no ${selector}`);
|
|
99
|
-
const expected =
|
|
99
|
+
const expected = parsedShape(container);
|
|
100
100
|
const problems = [];
|
|
101
101
|
const watch = {
|
|
102
102
|
install(app) {
|
package/package.json
CHANGED