Files
webstudio/apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.test.tsx
Oleg Isonen 05706b5425 feat: Make Content Block templates the MDX authoring interface (#6307)
Closes #6306

## Outcome

Content Block templates are now the public component model for MDX
authoring.

- Standard Markdown resolves through matching Content Block templates.
- PascalCase JSX resolves to a named template first, then to a
registered component.
- Adding or changing a template immediately affects matching MDX,
including content authored before that template existed.
- Every supported standard MDX element is present in new Content Blocks
by default.
- Template `name` is a stable PascalCase JSX identifier. `label` remains
optional display text only.
- Lowercase JSX represents native HTML and SVG without exposing
`ws.element` in authored syntax.
- Unknown, ambiguous, and unsafe JSX stays in source, produces a
source-ranged Builder diagnostic, and is omitted from published output.
- Frontmatter images preserve exact `$ref` values and use the Asset
description for `alt` text.
- Keyboard and mouse insertion from the slash menu remove the trigger
slash and preserve the selected template.

## Public MDX contract

- Markdown is canonical for standard prose.
- Lowercase JSX creates native elements:
`<section><p>Text</p></section>`.
- PascalCase JSX uses templates and registered components:
`<PromotionCard />`, `<Heading tag="h1">Title</Heading>`, and `<YouTube
src="https://youtu.be/KI5JHpzBK1s" />`.
- Component-name collisions use stable qualified identifiers such as
`Checkbox` and `RadixCheckbox`.
- A self-closing template keeps its designed default subtree. An
explicitly paired empty template clears it.
- Static quoted values and bare booleans are supported.
- Imports, exports, expressions, spreads, functions, executable
JavaScript, unsafe attributes, `<$.*>`, and authored `<ws.element>` are
rejected.
- `ws` remains only for compiler primitives with no React component.

## Implementation

- [x] Add stable, unique template names and keep them separate from
labels.
- [x] Parse and validate Markdown and JSX through their ASTs with
precise source ranges.
- [x] Share component identity between template JSX, Content MDX,
Builder materialization, reload, copy, and publication.
- [x] Resolve templates live after add, edit, rename, reorder, removal,
undo/redo, collaboration updates, and reload.
- [x] Preserve authored props, children, Asset references, template
defaults, and the exact slash-menu template choice.
- [x] Make one typed MDX semantic inventory drive default templates and
slash-menu eligibility.
- [x] Move component-dependent template assembly into the component
registry.
- [x] Replace production `$`, `animation`, and `radix` JSX namespaces
with direct component imports.
- [x] Keep only explicit compiler primitives in `ws` template JSX.
- [x] Migrate repository templates, fixtures, stories, examples, MCP
guidance, CLI guidance, and generated outputs.
- [x] Add Builder unit and Playwright coverage for authoring,
diagnostics, insertion, synchronization, reload, and publication.
- [x] Review and update the authoritative Content Block and generated
MCP documentation.

## Compatibility

- Persisted project data continues to use namespaced component IDs and
`ws:element` for generic HTML elements. No project-data migration is
required.
- Legacy `<ws.element ws:tag="…">`, `<ws.element ws:name="…">`, `<$.*>`,
`<animation.*>`, and `<radix.*>` input remains readable during the
compatibility window.
- Current serializers and repository-owned sources emit only canonical
Markdown, lowercase intrinsic JSX, or PascalCase identifiers.
- Untouched user MDX is not rewritten automatically. Legacy syntax is
canonicalized when its owning source is edited.
- Deferred follow-up: remove legacy namespace readers and proxy support
only after the canonical writer is deployed and existing projects are
backfilled. This is intentionally outside this PR.

## Verification

- [x] CI package tests, typechecks, lint, package boundaries, generated
assets, migrations, and size checks pass.
- [x] All six Builder E2E shards pass.
- [x] Fixture checks pass on Ubuntu and Windows.
- [x] Targeted local Content Engine, Template, SDK, registry, Project
Build, migration, Builder, component, CLI, and Playwright tests pass.
- [x] Runtime contracts, CLI docs, and MCP docs were regenerated from
their owning sources.
- [x] Local fixture generation completed without an unexpected fixture
diff.
- [x] Documentation impact reviewed and included in this PR.
- [x] Review and approve the intentional List visual snapshot change.
- [x] Run the complete agent evaluation suite after explicit approval.
- [x] Run the network fixture link/sync pipeline against the matching PR
deployment after explicit permission.

## Known visual change

The generated List story now targets its actual default `ul` element
instead of `ol`, so the default list styling appears in the visual
snapshot. The visual change was reviewed and the approved visual check
passes.
2026-09-06 15:19:54 +02:00

3390 lines
86 KiB
TypeScript

import type { JSX } from "react";
import { test, expect, describe, beforeEach } from "vitest";
import { createId } from "@webstudio-is/sdk";
import {
type NestingRule,
createRegularStyleSheet,
} from "@webstudio-is/css-engine";
import {
initialBreakpoints,
type WebstudioFragment,
type Instance,
} from "@webstudio-is/sdk";
import {
createTemplateComponentFixture,
renderData,
} from "@webstudio-is/template";
import * as defaultMetas from "@webstudio-is/sdk-components-react/metas";
import { __testing__, webflow } from "./plugin-webflow";
import { $registeredComponentMetas } from "../../nano-states";
import { $breakpoints } from "~/shared/sync/data-stores";
import { $project, $styleSources, $styles } from "~/shared/sync/data-stores";
import invariant from "tiny-invariant";
import { wfData } from "@webstudio-is/project-build/transfer";
import { pasteIgnored } from "../copy-paste";
const Blockquote = createTemplateComponentFixture("Blockquote");
const Bold = createTemplateComponentFixture("Bold");
const Box = createTemplateComponentFixture("Box");
const Button = createTemplateComponentFixture("Button");
const Checkbox = createTemplateComponentFixture("Checkbox");
const CodeText = createTemplateComponentFixture("CodeText");
const Heading = createTemplateComponentFixture("Heading");
const HtmlEmbed = createTemplateComponentFixture("HtmlEmbed");
const Image = createTemplateComponentFixture("Image");
const Input = createTemplateComponentFixture("Input");
const Italic = createTemplateComponentFixture("Italic");
const Label = createTemplateComponentFixture("Label");
const Link = createTemplateComponentFixture("Link");
const List = createTemplateComponentFixture("List");
const ListItem = createTemplateComponentFixture("ListItem");
const Paragraph = createTemplateComponentFixture("Paragraph");
const RadioButton = createTemplateComponentFixture("RadioButton");
const Subscript = createTemplateComponentFixture("Subscript");
const Superscript = createTemplateComponentFixture("Superscript");
const Text = createTemplateComponentFixture("Text");
const Textarea = createTemplateComponentFixture("Textarea");
const { toWebstudioFragment } = __testing__;
const equalFragment = (fragment: WebstudioFragment, jsx: JSX.Element) => {
const fragmentInstances = new Map();
fragment.instances.forEach((instance: Instance) => {
const newId = expect.any(String) as unknown as string;
fragmentInstances.set(newId, {
...instance,
id: newId,
children: instance.children.map((child) => {
if (child.type === "text") {
return child;
}
return { type: "id", value: expect.any(String) as unknown as string };
}),
});
});
const expected = renderData(jsx);
const expectedInstances = new Map();
for (const instance of expected.instances.values()) {
expectedInstances.set(instance.id, instance);
}
const fragmentProps = new Map();
for (const prop of fragment.props) {
prop.id = expect.any(String) as unknown as string;
prop.instanceId = expect.any(String) as unknown as string;
fragmentProps.set(prop.id, prop);
}
const expectedProps = new Map();
for (const prop of expected.props.values()) {
prop.id = expect.any(String) as unknown as string;
prop.instanceId = expect.any(String) as unknown as string;
expectedProps.set(prop.id, prop);
}
//console.dir(fragmentInstances, { depth: null });
//console.dir(expectedInstances, { depth: null });
//console.dir(expectedProps, { depth: null });
expect(fragmentInstances).toEqual(expectedInstances);
expect(fragmentProps).toEqual(expectedProps);
};
const toCss = (fragment: WebstudioFragment) => {
const sheet = createRegularStyleSheet();
for (const breakpoint of fragment.breakpoints) {
sheet.addMediaRule(breakpoint.id, breakpoint);
}
const ruleByStyleSourceId = new Map<string, NestingRule>();
for (const styleDecl of fragment.styleSources) {
const name = styleDecl.type === "token" ? styleDecl.name : "Local";
const rule = sheet.addNestingRule(name);
ruleByStyleSourceId.set(styleDecl.id, rule);
}
for (const styleDecl of fragment.styles) {
const rule = ruleByStyleSourceId.get(styleDecl.styleSourceId);
rule?.setDeclaration({
breakpoint: styleDecl.breakpointId,
selector: styleDecl.state ?? "",
property: styleDecl.property,
value: styleDecl.value,
});
}
return sheet.cssText;
};
beforeEach(() => {
const defaultMetasMap = new Map(Object.entries(defaultMetas));
$registeredComponentMetas.set(defaultMetasMap);
$project.set({
id: "test",
createdAt: "",
domain: "",
title: "",
userId: "",
isDeleted: false,
previewImageAsset: null,
marketplaceApprovalStatus: "PENDING",
latestStaticBuild: null,
domainsVirtual: [],
latestBuildVirtual: null,
previewImageAssetId: null,
tags: [],
workspaceId: null,
});
$breakpoints.set(
new Map(
initialBreakpoints.map((breakpoint) => {
const id = createId("nano");
return [id, { ...breakpoint, id }];
})
)
);
});
test("ignores non-Webflow paste data", async () => {
await expect(webflow.onPaste?.(`{"type":"other"}`)).resolves.toEqual(
pasteIgnored
);
});
test("reports malformed Webflow-owned paste data", async () => {
await expect(
webflow.onPaste?.(JSON.stringify({ type: "@webflow/XscpData" }))
).resolves.toMatchObject({
success: false,
error: expect.any(String),
});
});
test("Heading", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "97d91be2-3bba-d340-0f13-a84e975b7497",
type: "Heading",
tag: "h1",
children: ["97d91be2-3bba-d340-0f13-a84e975b7498"],
classes: [],
},
{
_id: "97d91be2-3bba-d340-0f13-a84e975b7498",
v: "Turtle in the sea",
text: true,
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Heading ws:tag="h1">Turtle in the sea</Heading>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
h1 {
margin-top: 20px;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0;
font-size: 38px;
font-weight: bold;
line-height: 44px
}
}"
`);
});
test("uses supplied id generator for instances, props, and breakpoints", async () => {
let nextId = 0;
const createId = () => `runtime-id-${++nextId}`;
const fragment = await toWebstudioFragment(
{
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "root",
type: "Block",
tag: "div",
children: [],
classes: ["class"],
data: {
attr: { id: "from-webflow" },
xattr: [{ name: "data-kind", value: "hero" }],
},
},
],
styles: [
{
_id: "class",
fake: false,
type: "class",
name: "Card",
namespace: "",
comb: "",
styleLess: "color: red;",
variants: {
medium: {
styleLess: "color: blue;",
},
},
},
],
assets: [],
},
},
createId
);
expect(fragment.children).toEqual([{ type: "id", value: "runtime-id-1" }]);
expect(fragment.props.map((prop) => prop.id)).toEqual([
"runtime-id-2",
"runtime-id-3",
]);
expect(fragment.breakpoints.map((breakpoint) => breakpoint.id)).toEqual([
"runtime-id-4",
"runtime-id-5",
]);
});
test("Link Block, Button, Text Link", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "97539676-c2ca-2e8f-55f3-6c4a3104a5c0",
type: "Link",
tag: "a",
classes: [],
children: [],
data: {
link: {
url: "https://webstudio.is",
target: "_blank",
},
},
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Link href="https://webstudio.is" target="_blank" />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
a {
background-color: rgb(0 0 0 / 0)
}
a:active {
outline: 0 none currentcolor
}
a:hover {
outline: 0 none currentcolor
}
}"
`);
});
test("List and ListItem", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "7e11a800-c8e2-9b14-37cf-09a9e94754ad",
type: "List",
tag: "ul",
classes: [],
children: [
"7e11a800-c8e2-9b14-37cf-09a9e94754ae",
"7e11a800-c8e2-9b14-37cf-09a9e94754af",
"7e11a800-c8e2-9b14-37cf-09a9e94754b0",
],
},
{
_id: "7e11a800-c8e2-9b14-37cf-09a9e94754ae",
type: "ListItem",
tag: "li",
classes: [],
children: [],
},
{
_id: "7e11a800-c8e2-9b14-37cf-09a9e94754af",
type: "ListItem",
tag: "li",
classes: [],
children: [],
},
{
_id: "7e11a800-c8e2-9b14-37cf-09a9e94754b0",
type: "ListItem",
tag: "li",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<List>
<ListItem />
<ListItem />
<ListItem />
</List>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
ul {
margin-top: 0;
margin-bottom: 10px;
padding-left: 40px
}
}"
`);
});
test("Paragraph", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "dfab64ae-6624-b6db-a909-b85588aa3f8d",
type: "Paragraph",
tag: "p",
classes: [],
children: ["dfab64ae-6624-b6db-a909-b85588aa3f8e"],
},
{
_id: "dfab64ae-6624-b6db-a909-b85588aa3f8e",
text: true,
v: "Text in a paragraph",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Paragraph>Text in a paragraph</Paragraph>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
p {
margin-top: 0;
margin-bottom: 10px
}
}"
`);
});
test("Text", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "adea2109-96eb-63e0-c27f-632a7f40bce8",
type: "Block",
tag: "div",
classes: [],
children: ["adea2109-96eb-63e0-c27f-632a7f40bce9"],
data: {
text: true,
},
},
{
_id: "adea2109-96eb-63e0-c27f-632a7f40bce9",
text: true,
v: "This is some text inside of a div block.",
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Text ws:tag="div">This is some text inside of a div block.</Text>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`""`);
});
test("Blockquote", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Blockquote",
tag: "blockquote",
classes: [],
children: ["25ffefdf-c015-5edd-7673-933b41a25329"],
},
{
_id: "25ffefdf-c015-5edd-7673-933b41a25329",
text: true,
v: "Block Quote",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Blockquote>Block Quote</Blockquote>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
blockquote {
margin-top: 0;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0;
padding-top: 10px;
padding-right: 20px;
padding-bottom: 10px;
padding-left: 20px;
font-size: 18px;
line-height: 22px;
border-left: 5px solid rgb(226 226 226 / 1)
}
}"
`);
});
test("Strong", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Strong",
tag: "strong",
classes: [],
children: ["25ffefdf-c015-5edd-7673-933b41a25329"],
},
{
_id: "25ffefdf-c015-5edd-7673-933b41a25329",
text: true,
v: "Bold Text",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Bold>Bold Text</Bold>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
strong {
font-weight: bold
}
}"
`);
});
test("Emphasized", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Emphasized",
tag: "em",
classes: [],
children: ["25ffefdf-c015-5edd-7673-933b41a25329"],
},
{
_id: "25ffefdf-c015-5edd-7673-933b41a25329",
text: true,
v: "Emphasis",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Italic>Emphasis</Italic>);
expect(toCss(fragment)).toMatchInlineSnapshot(`""`);
});
test("Superscript", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Superscript",
tag: "sup",
classes: [],
children: ["25ffefdf-c015-5edd-7673-933b41a25329"],
},
{
_id: "25ffefdf-c015-5edd-7673-933b41a25329",
text: true,
v: "Superscript",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Superscript>Superscript</Superscript>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
sup {
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
top: -0.5em
}
}"
`);
});
test("Subscript", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Subscript",
tag: "sub",
classes: [],
children: ["25ffefdf-c015-5edd-7673-933b41a25329"],
},
{
_id: "25ffefdf-c015-5edd-7673-933b41a25329",
text: true,
v: "Subscript",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Subscript>Subscript</Subscript>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
sub {
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
bottom: -0.25em
}
}"
`);
});
test("Section", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Section",
tag: "section",
classes: [],
children: ["25ffefdf-c015-5edd-7673-933b41a25329"],
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box ws:tag="section" />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
section {
display: block
}
}"
`);
});
test("Figure", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "7c6bc1fd-128d-514b-167b-605a910e435c",
type: "Block",
tag: "figure",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box ws:tag="figure" />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
figure {
display: block;
margin-top: 0;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0
}
}"
`);
});
test("BlockContainer", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "BlockContainer",
tag: "div",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-layout-blockcontainer {
max-width: 940px;
margin-left: auto;
margin-right: auto;
display: block
}
w-container {
max-width: 940px;
margin-left: auto;
margin-right: auto
}
w-container:after {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table;
clear: both
}
w-container:before {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table
}
}
@media all and (max-width: 991px) {
w-layout-blockcontainer {
max-width: 728px
}
w-container {
max-width: 728px
}
}
@media all and (max-width: 767px) {
w-layout-blockcontainer {
max-width: none
}
}
@media all and (max-width: 479px) {
w-container {
max-width: none
}
}"
`);
});
test("Block", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Block",
tag: "div",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box ws:tag="div" />);
expect(toCss(fragment)).toMatchInlineSnapshot(`""`);
});
test("V Flex", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "VFlex",
tag: "div",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-layout-vflex {
flex-direction: column;
align-items: flex-start;
display: flex
}
}"
`);
});
test("H Flex", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "HFlex",
tag: "div",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-layout-hflex {
flex-direction: row;
align-items: flex-start;
display: flex
}
}"
`);
});
test("QuickStack with instance styles", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "aeb3895f-67c9-b5a0-69b4-7960b893ec04",
type: "Layout",
tag: "div",
classes: [],
children: [
"ca264069-ff87-c2e5-7dc0-a4c0ec0e9146",
"ec30cdfd-1a60-eeb0-1bfa-1df6f1956ed5",
],
data: {
style: {
base: {
main: {
noPseudo: {
gridTemplateColumns: "1fr 1fr",
gridTemplateRows: "auto",
order: 1,
},
},
},
},
},
},
{
_id: "ca264069-ff87-c2e5-7dc0-a4c0ec0e9146",
type: "Cell",
tag: "div",
classes: [],
children: [],
},
{
_id: "ec30cdfd-1a60-eeb0-1bfa-1df6f1956ed5",
type: "Cell",
tag: "div",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Box>
<Box />
<Box />
</Box>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-layout-layout {
row-gap: 20px;
column-gap: 20px;
grid-auto-columns: 1fr;
justify-content: center;
padding: 20px
}
wf-layout-layout {
display: grid
}
Local {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
order: 1
}
w-layout-cell {
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
display: flex
}
}"
`);
});
test("Grid", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "25ffefdf-c015-5edd-7673-933b41a25328",
type: "Grid",
tag: "div",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-layout-grid {
row-gap: 16px;
column-gap: 16px;
grid-template-rows: auto auto;
grid-template-columns: 1fr 1fr;
grid-auto-columns: 1fr;
display: grid
}
}"
`);
});
test("Columns", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "08fb88d6-f6ec-5169-f4d4-8dac98df2b58",
type: "Row",
tag: "div",
classes: [],
children: [
"08fb88d6-f6ec-5169-f4d4-8dac98df2b59",
"08fb88d6-f6ec-5169-f4d4-8dac98df2b5a",
],
},
{
_id: "08fb88d6-f6ec-5169-f4d4-8dac98df2b59",
type: "Column",
tag: "div",
classes: [],
children: [],
},
{
_id: "08fb88d6-f6ec-5169-f4d4-8dac98df2b5a",
type: "Column",
tag: "div",
classes: [],
children: [],
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Box>
<Box />
<Box />
</Box>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-row:after {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table;
clear: both
}
w-row:before {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table
}
w-col {
float: left;
width: 100%;
min-height: 1px;
padding-left: 10px;
padding-right: 10px;
position: relative
}
}
@media all and (max-width: 767px) {
w-row {
margin-left: 0;
margin-right: 0
}
w-col {
width: 100%;
left: auto;
right: auto
}
}
@media all and (max-width: 479px) {
w-col {
width: 100%
}
}"
`);
});
test("Image", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "3c0b6a7a-830f-4b4a-48c5-4215f9c9389a",
type: "Image",
tag: "img",
classes: [],
children: [],
data: {
attr: {
src: "https://test.com/image.jpg",
loading: "eager",
width: "200",
height: "auto",
alt: "Test",
},
},
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Image
alt="Test"
loading="eager"
width="200"
src="https://test.com/image.jpg"
/>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
img {
vertical-align: middle;
max-width: 100%;
display: inline-block;
border: 0 none currentcolor
}
}"
`);
});
test("HtmlEmbed", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "88131b38-7a58-8085-38d2-dc51c5ce887e",
type: "HtmlEmbed",
tag: "div",
classes: [],
children: [],
v: "some html",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <HtmlEmbed code="some html" clientOnly={true} />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-embed {
display: block
}
w-embed:after {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table;
clear: both
}
w-embed:before {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table
}
}"
`);
});
test("CodeBlock", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "f06b6679-6414-3592-a6e3-b59196420d7f",
type: "CodeBlock",
tag: "div",
classes: [],
children: [],
data: {
code: "test",
language: "javascript",
},
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <CodeText lang="javascript">test</CodeText>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-code-block {
margin-top: unset;
margin-right: unset;
margin-bottom: unset;
margin-left: unset
}
}"
`);
});
test("RichText", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "33178bae-2d99-7d1f-ffa5-101356769dad",
type: "RichText",
tag: "div",
classes: [],
children: [
"8418336b-918d-3160-1d76-8c9de474cb43",
"63929533-a4de-17c6-3b13-351aa16c6e10",
"7724e6e0-9e3c-4526-35a9-f11dcd628c35",
"d760a6fd-5da7-7d24-4f4c-24e68a651758",
"49c71aa2-c61b-4823-dfdb-dc3badea814b",
"28784747-282c-fe17-a81b-c36c9212b78e",
"6e4c4ff0-0ebe-1de0-d94d-6f18f08f0762",
"88e7fd8c-98ff-49f7-aff1-3286e2e0e211",
"63202791-9c33-bc97-b49e-71a2af805488",
"b386cecd-1697-e7e8-6c7b-66f36261fec2",
"98b880aa-7ba5-c754-8069-b83cb198ec32",
"2e3bf379-e9ee-182c-9f92-1e1409ba1dd7",
"43d15a61-5f5d-d4bd-4a8c-04b80371e808",
"676d66b8-3df9-45a5-b2a4-62b75b623443",
"57268510-0593-b1df-7786-f1f3b3d02830",
"cd959e82-0fe8-a4a7-6b9f-c5fab19aa6ec",
"10389bae-2b31-83d5-919f-45fc50689288",
],
},
{
_id: "8418336b-918d-3160-1d76-8c9de474cb43",
type: "Heading",
tag: "h1",
classes: [],
children: ["c1759a8a-6657-5c74-4e95-33c8a721caab"],
},
{
_id: "c1759a8a-6657-5c74-4e95-33c8a721caab",
text: true,
v: "Heading 1",
},
{
_id: "63929533-a4de-17c6-3b13-351aa16c6e10",
type: "Heading",
tag: "h2",
classes: [],
children: ["d04d84bb-f382-5af4-482a-e6a0aaab168b"],
},
{
_id: "d04d84bb-f382-5af4-482a-e6a0aaab168b",
text: true,
v: "Heading 2",
},
{
_id: "7724e6e0-9e3c-4526-35a9-f11dcd628c35",
type: "Heading",
tag: "h3",
classes: [],
children: ["409d9647-ea51-e048-2bfd-cbbee2efb0c6"],
},
{
_id: "409d9647-ea51-e048-2bfd-cbbee2efb0c6",
text: true,
v: "Heading 3",
},
{
_id: "d760a6fd-5da7-7d24-4f4c-24e68a651758",
type: "Heading",
tag: "h4",
classes: [],
children: ["4ce7fd9b-c7e9-186c-853a-931c507e5d1d"],
},
{
_id: "4ce7fd9b-c7e9-186c-853a-931c507e5d1d",
text: true,
v: "Heading 4",
},
{
_id: "49c71aa2-c61b-4823-dfdb-dc3badea814b",
type: "Heading",
tag: "h5",
classes: [],
children: ["f28abd5f-e21c-08f5-c6be-ec335166114d"],
},
{
_id: "f28abd5f-e21c-08f5-c6be-ec335166114d",
text: true,
v: "Heading 5",
},
{
_id: "28784747-282c-fe17-a81b-c36c9212b78e",
type: "Heading",
tag: "h6",
classes: [],
children: ["6f753bba-a1d9-5092-e605-2f1756fc4a3d"],
},
{
_id: "6f753bba-a1d9-5092-e605-2f1756fc4a3d",
text: true,
v: "Heading 6",
},
{
_id: "6e4c4ff0-0ebe-1de0-d94d-6f18f08f0762",
type: "Paragraph",
tag: "p",
classes: [],
children: ["27e847ff-962c-0782-1278-2f4ff342aaad"],
},
{
_id: "27e847ff-962c-0782-1278-2f4ff342aaad",
text: true,
v: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
{
_id: "88e7fd8c-98ff-49f7-aff1-3286e2e0e211",
type: "Blockquote",
tag: "blockquote",
classes: [],
children: ["9c1d38a3-512b-4f90-e6c4-cadf585cb608"],
},
{
_id: "9c1d38a3-512b-4f90-e6c4-cadf585cb608",
text: true,
v: "Block quote",
},
{
_id: "63202791-9c33-bc97-b49e-71a2af805488",
type: "Paragraph",
tag: "p",
classes: [],
children: ["3080a095-fc1a-9de8-e6d1-21c801aed169"],
},
{
_id: "3080a095-fc1a-9de8-e6d1-21c801aed169",
text: true,
v: "Ordered list",
},
{
_id: "b386cecd-1697-e7e8-6c7b-66f36261fec2",
type: "List",
tag: "ol",
classes: [],
children: [
"aafe8235-0ff4-c6f4-8382-40fc34900b75",
"0fd98471-33b6-e8d2-76f1-6e373849e6ba",
"2ccfda89-a200-d286-440a-f94699163329",
],
},
{
_id: "aafe8235-0ff4-c6f4-8382-40fc34900b75",
type: "ListItem",
tag: "li",
classes: [],
children: ["bf312343-69b6-bec7-5ee1-e19154d85b62"],
},
{
_id: "bf312343-69b6-bec7-5ee1-e19154d85b62",
text: true,
v: "Item 1",
},
{
_id: "0fd98471-33b6-e8d2-76f1-6e373849e6ba",
type: "ListItem",
tag: "li",
classes: [],
children: ["161683ac-eed6-7a68-64f7-706c42984c65"],
},
{
_id: "161683ac-eed6-7a68-64f7-706c42984c65",
text: true,
v: "Item 2",
},
{
_id: "2ccfda89-a200-d286-440a-f94699163329",
type: "ListItem",
tag: "li",
classes: [],
children: ["59f4270b-72e6-99b1-041a-5ab836481b5c"],
},
{
_id: "59f4270b-72e6-99b1-041a-5ab836481b5c",
text: true,
v: "Item 3",
},
{
_id: "98b880aa-7ba5-c754-8069-b83cb198ec32",
type: "Paragraph",
tag: "p",
classes: [],
children: ["b6755218-5a0d-c537-43c0-071245d7a472"],
},
{
_id: "b6755218-5a0d-c537-43c0-071245d7a472",
text: true,
v: "Unordered list",
},
{
_id: "2e3bf379-e9ee-182c-9f92-1e1409ba1dd7",
type: "List",
tag: "ul",
classes: [],
children: [
"a90c5835-61e2-e1e6-dfd3-0b02a34ad857",
"748eb739-12d7-26fc-b8ee-f2c460f35170",
"54178296-1686-9b14-a341-42ce51958ce5",
],
},
{
_id: "a90c5835-61e2-e1e6-dfd3-0b02a34ad857",
type: "ListItem",
tag: "li",
classes: [],
children: ["8557d8cf-09de-52c0-4631-c2123fc056d1"],
},
{
_id: "8557d8cf-09de-52c0-4631-c2123fc056d1",
text: true,
v: "Item A",
},
{
_id: "748eb739-12d7-26fc-b8ee-f2c460f35170",
type: "ListItem",
tag: "li",
classes: [],
children: ["666b664d-8023-50bb-453a-36017c7a3a38"],
},
{
_id: "666b664d-8023-50bb-453a-36017c7a3a38",
text: true,
v: "Item B",
},
{
_id: "54178296-1686-9b14-a341-42ce51958ce5",
type: "ListItem",
tag: "li",
classes: [],
children: ["15647123-fd1c-5928-3894-d666bc6f3c84"],
},
{
_id: "15647123-fd1c-5928-3894-d666bc6f3c84",
text: true,
v: "Item C",
},
{
_id: "43d15a61-5f5d-d4bd-4a8c-04b80371e808",
type: "Paragraph",
tag: "p",
classes: [],
children: ["db8cce1a-40ff-ccb7-5f18-ebb811777409"],
},
{
_id: "db8cce1a-40ff-ccb7-5f18-ebb811777409",
type: "Link",
tag: "a",
classes: [],
children: ["a8af1cd8-157c-4b92-c27e-bca1ae5e6668"],
data: {
button: false,
block: "",
link: {
url: "https://university.webflow.com/lesson/add-and-nest-text-links-in-webflow",
},
},
},
{
_id: "a8af1cd8-157c-4b92-c27e-bca1ae5e6668",
text: true,
v: "Text link",
},
{
_id: "676d66b8-3df9-45a5-b2a4-62b75b623443",
type: "Paragraph",
tag: "p",
classes: [],
children: ["4f0b961b-5774-f441-e97f-e84a75efcf08"],
},
{
_id: "4f0b961b-5774-f441-e97f-e84a75efcf08",
type: "Strong",
tag: "strong",
classes: [],
children: ["5839c9b9-4281-f778-f7f9-c4844452295e"],
},
{
_id: "5839c9b9-4281-f778-f7f9-c4844452295e",
text: true,
v: "Bold text",
},
{
_id: "57268510-0593-b1df-7786-f1f3b3d02830",
type: "Paragraph",
tag: "p",
classes: [],
children: ["db4e59e1-b859-d2bc-ee4a-08c638f088db"],
},
{
_id: "db4e59e1-b859-d2bc-ee4a-08c638f088db",
type: "Emphasized",
tag: "em",
classes: [],
children: ["c2661284-144d-5052-447a-ffe78f3b8bbe"],
},
{
_id: "c2661284-144d-5052-447a-ffe78f3b8bbe",
text: true,
v: "Emphasis",
},
{
_id: "cd959e82-0fe8-a4a7-6b9f-c5fab19aa6ec",
type: "Paragraph",
tag: "p",
classes: [],
children: ["debd47f8-ddd2-8ead-af6c-5e42a16e15d0"],
},
{
_id: "debd47f8-ddd2-8ead-af6c-5e42a16e15d0",
type: "Superscript",
tag: "sup",
classes: [],
children: ["e6640190-f224-5a57-6181-55a793c00da8"],
},
{
_id: "e6640190-f224-5a57-6181-55a793c00da8",
text: true,
v: "Superscript",
},
{
_id: "10389bae-2b31-83d5-919f-45fc50689288",
type: "Paragraph",
tag: "p",
classes: [],
children: ["ec162f35-9226-0e8a-c0c2-32685ef444c1"],
},
{
_id: "ec162f35-9226-0e8a-c0c2-32685ef444c1",
type: "Subscript",
tag: "sub",
classes: [],
children: ["dcdc8f43-3cdc-1522-e397-60968e378b92"],
},
{
_id: "dcdc8f43-3cdc-1522-e397-60968e378b92",
text: true,
v: "Subscript",
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Box>
<Heading ws:tag="h1">Heading 1</Heading>
<Heading ws:tag="h2">Heading 2</Heading>
<Heading ws:tag="h3">Heading 3</Heading>
<Heading ws:tag="h4">Heading 4</Heading>
<Heading ws:tag="h5">Heading 5</Heading>
<Heading ws:tag="h6">Heading 6</Heading>
<Paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur.
</Paragraph>
<Blockquote>Block quote</Blockquote>
<Paragraph>Ordered list</Paragraph>
<List ordered={true}>
<ListItem>Item 1</ListItem>
<ListItem>Item 2</ListItem>
<ListItem>Item 3</ListItem>
</List>
<Paragraph>Unordered list</Paragraph>
<List>
<ListItem>Item A</ListItem>
<ListItem>Item B</ListItem>
<ListItem>Item C</ListItem>
</List>
<Paragraph>
<Link href="https://university.webflow.com/lesson/add-and-nest-text-links-in-webflow">
Text link
</Link>
</Paragraph>
<Paragraph>
<Bold>Bold text</Bold>
</Paragraph>
<Paragraph>
<Italic>Emphasis</Italic>
</Paragraph>
<Paragraph>
<Superscript>Superscript</Superscript>
</Paragraph>
<Paragraph>
<Subscript>Subscript</Subscript>
</Paragraph>
</Box>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
h1 {
margin-top: 20px;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0;
font-size: 38px;
font-weight: bold;
line-height: 44px
}
h2 {
margin-bottom: 10px;
font-weight: bold;
margin-top: 20px;
font-size: 32px;
line-height: 36px
}
h3 {
margin-bottom: 10px;
font-weight: bold;
margin-top: 20px;
font-size: 24px;
line-height: 30px
}
h4 {
margin-bottom: 10px;
font-weight: bold;
margin-top: 10px;
font-size: 18px;
line-height: 24px
}
h5 {
margin-bottom: 10px;
font-weight: bold;
margin-top: 10px;
font-size: 14px;
line-height: 20px
}
h6 {
margin-bottom: 10px;
font-weight: bold;
margin-top: 10px;
font-size: 12px;
line-height: 18px
}
p {
margin-top: 0;
margin-bottom: 10px
}
blockquote {
margin-top: 0;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0;
padding-top: 10px;
padding-right: 20px;
padding-bottom: 10px;
padding-left: 20px;
font-size: 18px;
line-height: 22px;
border-left: 5px solid rgb(226 226 226 / 1)
}
ol {
margin-top: 0;
margin-bottom: 10px;
padding-left: 40px
}
ul {
margin-top: 0;
margin-bottom: 10px;
padding-left: 40px
}
a {
background-color: rgb(0 0 0 / 0)
}
a:active {
outline: 0 none currentcolor
}
a:hover {
outline: 0 none currentcolor
}
strong {
font-weight: bold
}
sup {
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
top: -0.5em
}
sub {
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
bottom: -0.25em
}
}"
`);
});
test("Form", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "f19c775e-a9ef-fd0c-2d16-aa53c69e9da4",
type: "FormWrapper",
tag: "div",
classes: [],
children: [
"11c85957-89b5-e4f8-da11-f1acb4223a9f",
"492633d9-7425-ba09-036f-e43213b9875b",
"a0877638-7523-ac57-7ed9-19f5469e79d1",
],
},
{
_id: "11c85957-89b5-e4f8-da11-f1acb4223a9f",
type: "FormForm",
tag: "form",
classes: [],
children: ["eeb469a5-8b03-8002-7dec-586856365387"],
},
{
_id: "eeb469a5-8b03-8002-7dec-586856365387",
type: "FormTextInput",
tag: "input",
classes: [],
children: [],
data: {
attr: {
id: "name",
name: "name",
maxlength: 256,
placeholder: "",
disabled: false,
type: "text",
required: false,
autofocus: false,
},
},
},
{
_id: "492633d9-7425-ba09-036f-e43213b9875b",
type: "FormSuccessMessage",
tag: "div",
classes: [],
children: ["442d7e86-2c77-961c-ab1b-431bd34abcf7"],
},
{
_id: "442d7e86-2c77-961c-ab1b-431bd34abcf7",
type: "Block",
tag: "div",
classes: [],
children: ["875baf7f-6c98-b379-0cbe-59b5fdd2c112"],
},
{
_id: "875baf7f-6c98-b379-0cbe-59b5fdd2c112",
text: true,
v: "Thank you! Your submission has been received!",
},
{
_id: "a0877638-7523-ac57-7ed9-19f5469e79d1",
type: "FormErrorMessage",
tag: "div",
classes: [],
children: ["9eaf0a19-b2fa-7aef-4825-41f6319d790d"],
},
{
_id: "9eaf0a19-b2fa-7aef-4825-41f6319d790d",
type: "Block",
tag: "div",
classes: [],
children: ["a87685fe-24b2-31ff-e229-40c71f6155dd"],
},
{
_id: "a87685fe-24b2-31ff-e229-40c71f6155dd",
text: true,
v: "Oops! Something went wrong while submitting the form.",
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Box>
<Box>
<Input
id="name"
name="name"
maxLength={256}
placeholder=""
disabled={false}
type="text"
required={false}
autoFocus={false}
/>
</Box>
</Box>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-form {
margin-top: 0;
margin-right: 0;
margin-bottom: 15px;
margin-left: 0
}
input {
color: inherit;
font-style: inherit;
font-variant-caps: inherit;
font-weight: inherit;
font-stretch: inherit;
font-size: inherit;
line-height: normal;
font-family: inherit;
appearance: button;
cursor: pointer;
box-sizing: border-box;
height: auto;
margin: 0;
padding: 0
}
input::-moz-focus-inner {
border: 0 none currentcolor;
padding: 0
}
w-input {
color: rgb(51 51 51 / 1);
vertical-align: middle;
background-color: rgb(238 238 238 / 1);
width: 100%;
height: auto;
margin-bottom: 10px;
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
font-size: 14px;
line-height: 1.42857;
display: block;
cursor: not-allowed;
border: 1px solid rgb(204 204 204 / 1)
}
w-input:-moz-placeholder {
color: rgb(153 153 153 / 1)
}
w-input::-moz-placeholder {
color: rgb(153 153 153 / 1);
opacity: 1
}
w-input::-webkit-input-placeholder {
color: rgb(153 153 153 / 1)
}
w-input:focus {
border-top-color: rgb(56 152 236 / 1);
border-right-color: rgb(56 152 236 / 1);
border-bottom-color: rgb(56 152 236 / 1);
border-left-color: rgb(56 152 236 / 1);
outline: 0 none currentcolor
}
}"
`);
});
test("FormButton", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "b7d4b56c-77eb-a79d-4b73-7802d6c5f74a",
type: "FormButton",
tag: "input",
classes: [],
children: [],
data: {
attr: {
value: "Submit",
},
},
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Button>Submit</Button>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
input {
color: inherit;
font-style: inherit;
font-variant-caps: inherit;
font-weight: inherit;
font-stretch: inherit;
font-size: inherit;
line-height: normal;
font-family: inherit;
appearance: button;
cursor: pointer;
box-sizing: border-box;
height: auto;
margin: 0;
padding: 0
}
input::-moz-focus-inner {
border: 0 none currentcolor;
padding: 0
}
w-button {
color: rgb(255 255 255 / 1);
line-height: inherit;
cursor: pointer;
background-color: rgb(56 152 236 / 1);
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
padding-top: 9px;
padding-right: 15px;
padding-bottom: 9px;
padding-left: 15px;
text-decoration-line: none;
text-decoration-style: solid;
text-decoration-color: currentcolor;
display: inline-block;
appearance: button;
border: 0 none currentcolor
}
}"
`);
});
test("FormTextInput", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "3b60c6c7-296c-f98b-0557-a55dcac084b3",
type: "FormTextInput",
tag: "input",
classes: [],
children: [],
data: {
attr: {
id: "email",
name: "email",
maxlength: 256,
placeholder: "",
disabled: false,
type: "email",
required: true,
autofocus: false,
},
},
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Input
id="email"
name="email"
maxLength={256}
placeholder=""
disabled={false}
type="email"
required={true}
autoFocus={false}
/>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
input {
color: inherit;
font-style: inherit;
font-variant-caps: inherit;
font-weight: inherit;
font-stretch: inherit;
font-size: inherit;
line-height: normal;
font-family: inherit;
appearance: button;
cursor: pointer;
box-sizing: border-box;
height: auto;
margin: 0;
padding: 0
}
input::-moz-focus-inner {
border: 0 none currentcolor;
padding: 0
}
w-input {
color: rgb(51 51 51 / 1);
vertical-align: middle;
background-color: rgb(238 238 238 / 1);
width: 100%;
height: auto;
margin-bottom: 10px;
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
font-size: 14px;
line-height: 1.42857;
display: block;
cursor: not-allowed;
border: 1px solid rgb(204 204 204 / 1)
}
w-input:-moz-placeholder {
color: rgb(153 153 153 / 1)
}
w-input::-moz-placeholder {
color: rgb(153 153 153 / 1);
opacity: 1
}
w-input::-webkit-input-placeholder {
color: rgb(153 153 153 / 1)
}
w-input:focus {
border-top-color: rgb(56 152 236 / 1);
border-right-color: rgb(56 152 236 / 1);
border-bottom-color: rgb(56 152 236 / 1);
border-left-color: rgb(56 152 236 / 1);
outline: 0 none currentcolor
}
}"
`);
});
test("FormBlockLabel", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "0fdc679b-9891-dfb3-698a-0114d0c6c729",
type: "FormBlockLabel",
tag: "label",
classes: [],
children: ["7827c7aa-cd90-8bda-4570-605f2a1a0d61"],
data: {
attr: {
for: "email",
},
},
},
{
_id: "7827c7aa-cd90-8bda-4570-605f2a1a0d61",
text: true,
v: "Email Address",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Label {...{ for: "email" }}>Email Address</Label>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
label {
margin-bottom: 5px;
font-weight: bold;
display: block
}
}"
`);
});
test("FormTextarea", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "ff81988e-1cc2-8c93-d264-d702d846c30a",
type: "FormTextarea",
tag: "textarea",
classes: [],
children: [],
data: {
attr: {
id: "field",
name: "field",
maxlength: 5000,
placeholder: "Example Text",
required: false,
autofocus: false,
},
},
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Textarea
id="field"
name="field"
maxLength={5000}
placeholder="Example Text"
required={false}
autoFocus={false}
/>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
textarea {
color: inherit;
font-style: inherit;
font-variant-caps: inherit;
font-weight: inherit;
font-stretch: inherit;
font-size: inherit;
line-height: inherit;
font-family: inherit;
overflow-x: auto;
overflow-y: auto;
height: auto;
margin: 0
}
w-input {
color: rgb(51 51 51 / 1);
vertical-align: middle;
background-color: rgb(238 238 238 / 1);
width: 100%;
height: auto;
margin-bottom: 10px;
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
font-size: 14px;
line-height: 1.42857;
display: block;
cursor: not-allowed;
border: 1px solid rgb(204 204 204 / 1)
}
w-input:-moz-placeholder {
color: rgb(153 153 153 / 1)
}
w-input::-moz-placeholder {
color: rgb(153 153 153 / 1);
opacity: 1
}
w-input::-webkit-input-placeholder {
color: rgb(153 153 153 / 1)
}
w-input:focus {
border-top-color: rgb(56 152 236 / 1);
border-right-color: rgb(56 152 236 / 1);
border-bottom-color: rgb(56 152 236 / 1);
border-left-color: rgb(56 152 236 / 1);
outline: 0 none currentcolor
}
}"
`);
});
test("FormBlockLabel", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "0fdc679b-9891-dfb3-698a-0114d0c6c729",
type: "FormBlockLabel",
tag: "label",
classes: [],
children: ["7827c7aa-cd90-8bda-4570-605f2a1a0d61"],
data: {
attr: {
for: "email",
},
},
},
{
_id: "7827c7aa-cd90-8bda-4570-605f2a1a0d61",
text: true,
v: "Email Address",
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Label {...{ for: "email" }}>Email Address</Label>);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
label {
margin-bottom: 5px;
font-weight: bold;
display: block
}
}"
`);
});
test("FormCheckboxWrapper, FormCheckboxInput, FormInlineLabel", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "4b49f914-6011-9030-5727-232bbddc7d56",
type: "FormCheckboxWrapper",
tag: "div",
classes: [],
children: [
"2e32acc8-7027-1741-1be1-cc72606b57cf",
"3f68e88d-5c03-698e-c4e2-a6ad6ff1a00c",
],
},
{
_id: "2e32acc8-7027-1741-1be1-cc72606b57cf",
type: "FormCheckboxInput",
tag: "input",
classes: [],
children: [],
data: {
attr: {
type: "checkbox",
name: "checkbox",
id: "checkbox",
required: false,
checked: false,
},
},
},
{
_id: "3f68e88d-5c03-698e-c4e2-a6ad6ff1a00c",
type: "FormInlineLabel",
tag: "label",
classes: [],
children: ["18550f45-3a50-27d6-4524-b91a226ded60"],
},
{
_id: "18550f45-3a50-27d6-4524-b91a226ded60",
text: true,
v: "Checkbox",
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Label ws:label="Checkbox Field">
<Checkbox
id="checkbox"
name="checkbox"
required={false}
defaultChecked={false}
/>
<Text ws:tag="span" ws:label="Label">
Checkbox
</Text>
</Label>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-checkbox {
margin-bottom: 5px;
padding-left: 20px;
display: block
}
w-checkbox:after {
content: " ";
clear: both;
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table
}
w-checkbox:before {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table
}
input {
color: inherit;
font-style: inherit;
font-variant-caps: inherit;
font-weight: inherit;
font-stretch: inherit;
font-size: inherit;
line-height: normal;
font-family: inherit;
appearance: button;
cursor: pointer;
box-sizing: border-box;
height: auto;
margin: 0;
padding: 0
}
input::-moz-focus-inner {
border: 0 none currentcolor;
padding: 0
}
w-checkbox-input {
float: left;
margin-top: 4px;
margin-right: 0;
margin-bottom: 0;
margin-left: -20px;
line-height: normal
}
label {
margin-bottom: 5px;
font-weight: bold;
display: block
}
w-form-label {
cursor: pointer;
margin-bottom: 0;
font-weight: normal;
display: inline-block
}
}"
`);
});
test("FormRadioWrapper, FormRadioInput, FormInlineLabel", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "712915f3-84e7-6e7a-e1a1-050e12d3b401",
type: "FormRadioWrapper",
tag: "div",
classes: [],
children: [
"2ee7bcbb-e2ec-3c21-2cb0-c76171ff84e0",
"99874ce0-e582-a5ea-9934-f6e42266a80d",
],
},
{
_id: "2ee7bcbb-e2ec-3c21-2cb0-c76171ff84e0",
type: "FormRadioInput",
tag: "input",
classes: [],
children: [],
data: {
attr: {
type: "radio",
name: "radio",
id: "radio",
value: "Radio",
required: false,
},
},
},
{
_id: "99874ce0-e582-a5ea-9934-f6e42266a80d",
type: "FormInlineLabel",
tag: "label",
classes: [],
children: ["9e6a0451-a55e-a063-9acc-ee22ed5eac94"],
},
{
_id: "9e6a0451-a55e-a063-9acc-ee22ed5eac94",
text: true,
v: "Radio",
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Label ws:label="Radio Field">
<RadioButton id="radio" name="radio" required={false} value="Radio" />
<Text ws:tag="span" ws:label="Label">
Radio
</Text>
</Label>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
w-radio {
margin-bottom: 5px;
padding-left: 20px;
display: block
}
w-radio:after {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table;
clear: both
}
w-radio:before {
content: " ";
grid-row-start: 1;
grid-column-start: 1;
grid-row-end: 2;
grid-column-end: 2;
display: table
}
input {
color: inherit;
font-style: inherit;
font-variant-caps: inherit;
font-weight: inherit;
font-stretch: inherit;
font-size: inherit;
line-height: normal;
font-family: inherit;
appearance: button;
cursor: pointer;
box-sizing: border-box;
height: auto;
margin: 0;
padding: 0
}
input::-moz-focus-inner {
border: 0 none currentcolor;
padding: 0
}
w-radio-input {
float: left;
margin-top: 3px;
margin-right: 0;
margin-bottom: 0;
margin-left: -20px;
line-height: normal
}
label {
margin-bottom: 5px;
font-weight: bold;
display: block
}
w-form-label {
cursor: pointer;
margin-bottom: 0;
font-weight: normal;
display: inline-block
}
}"
`);
});
test("FormSelect", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "a68c34ca-e4c3-3dbc-0036-2554387bd7a4",
type: "FormSelect",
tag: "select",
classes: [],
children: [],
data: {
form: {
opts: [
{
t: "Select one...",
v: "",
},
{
t: "First choice",
v: "First",
},
{
t: "Second choice",
v: "Second",
},
{
t: "Third choice",
v: "Third",
},
],
},
attr: {
id: "field-3",
name: "field-3",
required: false,
multiple: false,
},
},
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Input id="field-3" name="field-3" required={false} multiple={false} />
);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
select {
color: inherit;
font-style: inherit;
font-variant-caps: inherit;
font-weight: inherit;
font-stretch: inherit;
font-size: inherit;
line-height: inherit;
font-family: inherit;
text-transform: none;
margin: 0
}
}"
`);
});
test("Multiline text", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "da7b8e40-a038-735c-bde8-0016079a5502",
type: "Block",
tag: "div",
classes: [],
children: [
"629fa602-de2f-b70b-ba5e-a74de823cfff",
"b6a2a455-8ece-d70b-c88b-2f7b7dd39445",
"49a4d697-0430-6739-9179-e75c1dbec765",
],
},
{
_id: "629fa602-de2f-b70b-ba5e-a74de823cfff",
text: true,
v: "a",
},
{
_id: "b6a2a455-8ece-d70b-c88b-2f7b7dd39445",
type: "LineBreak",
tag: "br",
classes: [],
children: [],
},
{
_id: "49a4d697-0430-6739-9179-e75c1dbec765",
text: true,
v: "b",
},
],
styles: [],
assets: [],
},
});
equalFragment(
fragment,
<Box ws:tag="div">
{"a"}
{"\n"}
{"b"}
</Box>
);
expect(toCss(fragment)).toMatchInlineSnapshot(`""`);
});
describe("Custom attributes", () => {
test("Basic", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "249f235e-91b6-bd0f-bc42-00993479e637",
type: "Heading",
tag: "h1",
classes: [],
children: [],
data: {
xattr: [
{
name: "at",
value: "b",
},
],
},
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Heading ws:tag="h1" at="b" />);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
h1 {
margin-top: 20px;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0;
font-size: 38px;
font-weight: bold;
line-height: 44px
}
}"
`);
});
});
test("Set show false when visibility's only condition is false", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "b35ac1a9-5a38-56c6-03ba-3196d421b95e",
type: "Block",
tag: "div",
classes: [],
children: [],
data: {
visibility: {
conditions: [false],
},
},
},
],
styles: [],
assets: [],
},
});
equalFragment(fragment, <Box data-ws-show={false} ws:tag="div" />);
expect(toCss(fragment)).toMatchInlineSnapshot(`""`);
});
describe("Styles", () => {
test("Single class", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "97d91be2-3bba-d340-0f13-a84e975b7497",
type: "Heading",
tag: "h1",
classes: ["a7bff598-b719-1edb-067b-a90a54d68605"],
children: [],
},
],
styles: [
{
_id: "a7bff598-b719-1edb-067b-a90a54d68605",
type: "class",
name: "Heading",
styleLess: "color: hsla(0, 80.00%, 47.78%, 1.00);",
},
],
assets: [],
},
});
expect(fragment.styleSources).toEqual([
{
type: "token",
id: expect.any(String),
name: "h1",
},
{
type: "token",
id: expect.any(String),
name: "Heading",
},
]);
expect(fragment.styleSourceSelections).toEqual([
{
instanceId: expect.any(String),
values: [expect.any(String), expect.any(String)],
},
]);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
h1 {
margin-top: 20px;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0;
font-size: 38px;
font-weight: bold;
line-height: 44px
}
Heading {
color: hsl(0 80% 47.78% / 1)
}
}"
`);
});
test("Combo class", async () => {
$styleSources.set(
new Map([
[
"uuBw1PRC_uE8RhTmwxaH8",
{
id: "uuBw1PRC_uE8RhTmwxaH8",
type: "token",
name: "button",
},
],
[
"uuORexg4BOrRXBJZgB80_",
{
id: "uuORexg4BOrRXBJZgB80_",
type: "token",
name: "button.is-small",
},
],
])
);
$styles.set(
new Map([
[
"uuBw1PRC_uE8RhTmwxaH8",
{
styleSourceId: "uuBw1PRC_uE8RhTmwxaH8",
breakpointId: "base",
property: "color",
value: { type: "keyword", value: "green" },
},
],
[
"uuORexg4BOrRXBJZgB80_0",
{
styleSourceId: "uuORexg4BOrRXBJZgB80_",
breakpointId: "base",
property: "paddingTop",
value: { type: "unit", unit: "rem", value: 1 },
},
],
])
);
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "5f7ab979-89b3-c705-6ab9-35f77dfb209f",
type: "Link",
tag: "a",
classes: [
"194e7d07-469d-6ffa-3925-1f51bdad7e44",
"194e7d07-469d-6ffa-3925-1f51bdad7e47",
"194e7d07-469d-6ffa-3925-1f51bdad7e46",
],
children: [],
data: {
link: {
url: "#",
},
},
},
],
styles: [
{
_id: "194e7d07-469d-6ffa-3925-1f51bdad7e44",
type: "class",
name: "button",
styleLess: "text-align: center;",
children: [
"194e7d07-469d-6ffa-3925-1f51bdad7e47",
"194e7d07-469d-6ffa-3925-1f51bdad7e46",
],
},
{
_id: "194e7d07-469d-6ffa-3925-1f51bdad7e47",
type: "class",
name: "is-small",
comb: "&",
styleLess: "padding: 1rem;",
},
{
_id: "194e7d07-469d-6ffa-3925-1f51bdad7e46",
type: "class",
name: "is-secondary",
comb: "&",
styleLess: "background-color: transparent;",
},
],
assets: [],
},
});
expect(fragment.styleSources).toEqual([
{
type: "token",
id: "uu1p3Xdvlq_AZOxnzDvAv",
name: "a",
},
{
type: "token",
id: "uumXb7vHOnzTr-4SIW-wJ",
name: "button.is-small.is-secondary",
},
]);
expect(fragment.styleSourceSelections).toEqual([
{
instanceId: expect.any(String),
values: [
"uu1p3Xdvlq_AZOxnzDvAv",
"uuBw1PRC_uE8RhTmwxaH8",
"uumXb7vHOnzTr-4SIW-wJ",
"uuORexg4BOrRXBJZgB80_",
],
},
]);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
a {
background-color: rgb(0 0 0 / 0)
}
a:active {
outline: 0 none currentcolor
}
a:hover {
outline: 0 none currentcolor
}
button.is-small.is-secondary {
text-align: center;
background-color: transparent;
padding: 1rem
}
}"
`);
});
test("Skip empty combo class", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "56ede23d-6dcc-a320-0af4-3a803d8efd88",
type: "Block",
tag: "div",
classes: ["08c3532a-4e88-1106-9fda-e4dcd66f93f0"],
children: [
"d06c01f8-da59-b31e-a910-55a6d33898f0",
"b67fb4c4-ce52-b441-1d89-55b165282c99",
],
},
{
_id: "d06c01f8-da59-b31e-a910-55a6d33898f0",
type: "Block",
tag: "div",
classes: ["6569b562-7de4-bd09-3d2d-59d7c11ec988"],
children: [],
},
{
_id: "b67fb4c4-ce52-b441-1d89-55b165282c99",
type: "Block",
tag: "div",
classes: [
"08c3532a-4e88-1106-9fda-e4dcd66f93f0",
"c4851546-799d-c6bf-fb17-0b5765f48d72",
],
children: [],
},
],
styles: [
{
_id: "08c3532a-4e88-1106-9fda-e4dcd66f93f0",
fake: false,
type: "class",
name: "d1",
namespace: "",
comb: "",
styleLess: "color: hsla(0, 70.45%, 48.11%, 1.00);",
variants: {},
children: ["c4851546-799d-c6bf-fb17-0b5765f48d72"],
},
{
_id: "6569b562-7de4-bd09-3d2d-59d7c11ec988",
fake: false,
type: "class",
name: "d2",
namespace: "",
comb: "",
styleLess: "font-size: 200px;",
variants: {},
children: [],
},
{
_id: "c4851546-799d-c6bf-fb17-0b5765f48d72",
fake: false,
type: "class",
name: "d2",
namespace: "",
comb: "&",
styleLess: "",
variants: {},
children: [],
},
],
assets: [],
},
});
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
d1 {
color: hsl(0 70.45% 48.11% / 1)
}
d2 {
font-size: 200px
}
}"
`);
});
test("Webflow @variable syntax used by Relume (legacy?)", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "5f7ab979-89b3-c705-6ab9-35f77dfb209f",
type: "Block",
tag: "div",
classes: ["194e7d07-469d-6ffa-3925-1f51bdad7e44"],
children: [],
},
],
styles: [
{
_id: "194e7d07-469d-6ffa-3925-1f51bdad7e44",
type: "class",
name: "block",
styleLess: "color: @var_relume-variable-color-neutral-1",
children: [],
},
],
assets: [],
},
});
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
block {
color: unset
}
}"
`);
});
test("States", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "5c66aeeb-941d-c52f-8e14-c54312878021",
type: "Link",
tag: "a",
classes: ["81cf331a-ed88-7a5f-3538-8915c7788aea"],
children: [],
data: {
link: {
url: "#",
},
},
},
],
styles: [
{
_id: "81cf331a-ed88-7a5f-3538-8915c7788aea",
fake: false,
type: "class",
name: "x",
namespace: "",
comb: "",
styleLess: "transform: translate3d(7px, 74px, 16px);",
variants: {
main_hover: {
styleLess: "background-color: hsla(0, 85.19%, 42.12%, 1.00);",
},
tiny_focus: {
styleLess: "background-color: hsla(264, 73.75%, 38.37%, 1.00);",
},
tiny_active: {
styleLess: "background-color: hsla(0, 46.06%, 51.80%, 1.00);",
},
"tiny_focus-visible": {
styleLess: "background-color: hsla(264, 36.36%, 41.56%, 1.00);",
},
tiny_visited: {
styleLess: "background-color: hsla(0, 83.67%, 42.10%, 1.00);",
},
tiny_hover: {
styleLess: "background-color: hsla(0, 60.99%, 58.99%, 1.00);",
},
},
},
],
assets: [],
},
});
expect(fragment.styleSources).toEqual([
{
type: "token",
id: expect.any(String),
name: "a",
},
{
type: "token",
id: expect.any(String),
name: "x",
},
]);
expect(fragment.styleSourceSelections).toEqual([
{
instanceId: expect.any(String),
values: [expect.any(String), expect.any(String)],
},
]);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
a {
background-color: rgb(0 0 0 / 0)
}
a:active {
outline: 0 none currentcolor
}
a:hover {
outline: 0 none currentcolor
}
x {
transform: translate3d(7px, 74px, 16px)
}
x:hover {
background-color: hsl(0 85.19% 42.12% / 1)
}
}
@media all and (max-width: 479px) {
x:active {
background-color: hsl(0 46.06% 51.8% / 1)
}
x:focus {
background-color: hsl(264 73.75% 38.37% / 1)
}
x:focus-visible {
background-color: hsl(264 36.36% 41.56% / 1)
}
x:hover {
background-color: hsl(0 60.99% 58.99% / 1)
}
x:visited {
background-color: hsl(0 83.67% 42.1% / 1)
}
}"
`);
});
test("Breakpoints", async () => {
const fragment = await toWebstudioFragment({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "c06c94aa-e2cd-fa7a-d8f8-574b474a20fa",
type: "Block",
tag: "div",
classes: ["81fbefba-d2de-9cc2-81bf-3a929d4eb219"],
children: [],
},
],
styles: [
{
_id: "81fbefba-d2de-9cc2-81bf-3a929d4eb219",
fake: false,
type: "class",
name: "Div Block 2",
namespace: "",
comb: "",
styleLess: "background-color: hsla(191, 100.00%, 50.00%, 1.00);",
variants: {
large: {
styleLess:
"background-color: hsla(150, 100.00%, 50.00%, 1.00);",
},
xl: {
styleLess: "",
},
medium: {
styleLess:
"background-color: hsla(256, 100.00%, 50.00%, 1.00);",
},
small: {
styleLess:
"background-color: hsla(308, 100.00%, 50.00%, 1.00);",
},
tiny: {
styleLess:
"background-color: hsla(359, 100.00%, 50.00%, 1.00);",
},
},
},
],
assets: [],
},
});
expect(fragment.breakpoints).toEqual([
{ id: expect.any(String), label: "base" },
{ id: expect.any(String), label: "large", minWidth: 1280 },
{ id: expect.any(String), label: "medium", maxWidth: 991 },
{ id: expect.any(String), label: "small", maxWidth: 767 },
{ id: expect.any(String), label: "tiny", maxWidth: 479 },
]);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
Div Block 2 {
background-color: hsl(191 100% 50% / 1)
}
}
@media all and (max-width: 991px) {
Div Block 2 {
background-color: hsl(256 100% 50% / 1)
}
}
@media all and (max-width: 767px) {
Div Block 2 {
background-color: hsl(308 100% 50% / 1)
}
}
@media all and (max-width: 479px) {
Div Block 2 {
background-color: hsl(359 100% 50% / 1)
}
}
@media all and (min-width: 1280px) {
Div Block 2 {
background-color: hsl(150 100% 50% / 1)
}
}"
`);
});
test("background images", async () => {
const input = wfData.parse({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "2e9842a4-ac18-9d21-894b-026c6eb20441",
type: "Block",
tag: "div",
classes: ["98133834-439c-9a8c-7e9f-c3186f2fa45f"],
children: [],
data: {
text: false,
},
},
],
styles: [
{
_id: "98133834-439c-9a8c-7e9f-c3186f2fa45f",
fake: false,
type: "class",
name: "Div Block",
namespace: "",
comb: "",
styleLess:
"height: 400px; background-image: linear-gradient(180deg, hsla(0, 0.00%, 0.00%, 0.11), white), @img_667d0b7769e0cc3754b584f6, @img_667d0fe180995eadc1534a26, @img_example_bg; background-position: 0px 0px, 550px 0px, 0px 0px,0px 0px; background-size: auto, contain, auto, auto; background-repeat: repeat, no-repeat, repeat,repeat; background-attachment: scroll, fixed, scroll, fixed;",
variants: {},
children: [],
},
],
assets: [
{
cdnUrl:
"https://uploads-ssl.webflow.com/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1).png",
siteId: "667c32290bd6159c18dca9a0",
width: 800,
height: 600,
fileName: "IMG_2882 (1).png",
createdOn: "2024-06-27T06:49:27.100Z",
origFileName: "IMG_2882 (1).png",
fileHash: "36f49907757795f0a4ecfcfdfc483115",
variants: [
{
origFileName: "IMG_2882%20(1)-p-500.png",
fileName: "667d0b7769e0cc3754b584f6_IMG_2882%20(1)-p-500.png",
format: "png",
size: 192728,
width: 500,
quality: 100,
cdnUrl:
"https://daks2k3a4ib2z.cloudfront.net/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1)-p-500.png",
s3Url:
"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1)-p-500.png",
},
],
mimeType: "image/png",
s3Url:
"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1).png",
thumbUrl: "",
_id: "667d0b7769e0cc3754b584f6",
markedAsDeleted: false,
fileSize: 862053,
},
{
cdnUrl:
"https://uploads-ssl.webflow.com/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20.webp",
siteId: "667c32290bd6159c18dca9a0",
width: 1024,
height: 1024,
fileName: "Привет Мир : %2F .webp",
createdOn: "2024-06-27T07:08:17.010Z",
origFileName: "Привет Мир : %2F .webp",
fileHash: "d86e52a94c04120f455b276effa59046",
variants: [
{
origFileName:
"%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp",
fileName:
"667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp",
format: "webp",
size: 26992,
width: 500,
quality: 100,
cdnUrl:
"https://daks2k3a4ib2z.cloudfront.net/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp",
s3Url:
"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp",
},
{
origFileName:
"%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp",
fileName:
"667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp",
format: "webp",
size: 45964,
width: 800,
quality: 100,
cdnUrl:
"https://daks2k3a4ib2z.cloudfront.net/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp",
s3Url:
"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp",
},
],
mimeType: "image/webp",
s3Url:
"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20.webp",
thumbUrl: "",
_id: "667d0fe180995eadc1534a26",
markedAsDeleted: false,
fileSize: 191270,
},
],
},
});
const fragment = await toWebstudioFragment(input);
const bgStyle = fragment.styles.find(
(style) => style.property === "backgroundImage"
);
expect(bgStyle).toBeDefined();
expect(bgStyle?.value.type).toEqual("layers");
const layers = bgStyle?.value;
invariant(layers?.type === "layers");
const imgA = layers.value[1];
const imgB = layers.value[2];
const noneLayer = layers.value[3];
invariant(imgA.type === "image");
invariant(imgA.value.type === "url");
invariant(imgB.type === "image");
invariant(imgB.value.type === "url");
expect(imgA.value.url).toEqual(input.payload.assets[0].s3Url);
expect(imgB.value.url).toEqual(input.payload.assets[1].s3Url);
expect(noneLayer.type).toEqual("keyword");
invariant(noneLayer.type === "keyword");
expect(noneLayer.value).toEqual("none");
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
Div Block {
height: 400px;
background-image: linear-gradient(180deg,hsla(0,0.00%,0.00%,0.11),white), url("https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1).png"), url("https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20.webp"), none;
background-size: auto, contain, auto, auto;
background-repeat: repeat, no-repeat, repeat, repeat;
background-attachment: scroll, fixed, scroll, fixed;
background-position: 0px 0px, 550px 0px, 0px 0px, 0px 0px
}
}"
`);
});
test("@raw webflow custom properties", async () => {
const input = wfData.parse({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "89ed49d8-2d32-5bab-22e1-264acb8d2d84",
type: "Block",
tag: "div",
classes: ["393ac121-a395-7678-e598-994e30fb7880"],
children: [],
data: { text: false, tag: "div" },
},
],
styles: [
{
_id: "393ac121-a395-7678-e598-994e30fb7880",
fake: false,
type: "class",
name: "Div Block",
namespace: "",
comb: "",
styleLess: "color: @raw<|black|>; background-color: @raw<|red|>;",
variants: {},
children: [],
},
],
assets: [],
},
});
const fragment = await toWebstudioFragment(input);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
Div Block {
color: black;
background-color: red
}
}"
`);
});
test("append transparent color when background-clip is used", async () => {
const input = wfData.parse({
type: "@webflow/XscpData",
payload: {
nodes: [
{
_id: "15c3fb65-b871-abe4-f9a4-3747c8a882e0",
type: "Heading",
tag: "h2",
classes: ["069649be-a33a-1a9a-3763-c0bd9d1f3a3d"],
children: ["b69a5869-f046-5a0c-151e-9b134a6852aa"],
data: {
tag: "h2",
devlink: { runtimeProps: {}, slot: "" },
displayName: "",
attr: { id: "" },
xattr: [],
search: { exclude: false },
visibility: { conditions: [] },
},
},
{
_id: "b69a5869-f046-5a0c-151e-9b134a6852aa",
text: true,
v: "Protect your systems securely with Prism",
},
],
styles: [
{
_id: "069649be-a33a-1a9a-3763-c0bd9d1f3a3d",
fake: false,
type: "class",
name: "H2 Heading 2",
namespace: "",
comb: "",
styleLess:
"background-image: linear-gradient(350deg, hsl(256.3636363636363 72.13% 23.92% / 0.00), hsl(256.2162162162162 72.55% 80.00% / 1.00) 49%, #bba7f1); color: hsl(0 0.00% 100.00% / 1.00); background-clip: text;",
variants: {},
children: [],
createdBy: "58b4b8186ceb395341fcf640",
origin: null,
selector: null,
},
],
assets: [],
},
});
const fragment = await toWebstudioFragment(input);
expect(toCss(fragment)).toMatchInlineSnapshot(`
"@media all {
h2 {
margin-bottom: 10px;
font-weight: bold;
margin-top: 20px;
font-size: 32px;
line-height: 36px
}
H2 Heading 2 {
background-image: linear-gradient(350deg,hsl(256.3636363636363 72.13% 23.92%/0.00),hsl(256.2162162162162 72.55% 80.00%/1.00) 49%,#bba7f1);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent
}
}"
`);
});
});