Skip to content

Commit 4f892e9

Browse files
leonardomendixgjulivan
authored andcommitted
test: add new unit tests and e2e
1 parent e1ae09d commit 4f892e9

5 files changed

Lines changed: 385 additions & 3 deletions

File tree

packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ test.afterEach("Cleanup session", async ({ page }) => {
66
});
77

88
test.describe("RichText", () => {
9+
test.describe.configure({ mode: "serial" });
910
test("compares with a screenshot baseline and checks if inline basic mode are rendered as expected", async ({
1011
page
1112
}) => {
@@ -133,6 +134,37 @@ test.describe("RichText", () => {
133134
});
134135
});
135136

137+
test("compares with a screenshot baseline and checks if class mode editor is rendered as expected", async ({
138+
page
139+
}) => {
140+
await page.goto("/p/classmode");
141+
await page.waitForLoadState("networkidle");
142+
await expect(page.locator(".mx-name-richText1")).toBeVisible();
143+
await expect(page.locator(".mx-name-richText1")).toHaveScreenshot(`classModeEditor.png`, { threshold: 0.4 });
144+
});
145+
146+
test("checks that class mode editor output uses CSS classes instead of inline styles", async ({ page }) => {
147+
await page.goto("/p/classmode");
148+
await page.waitForLoadState("networkidle");
149+
const html = await page.locator(".mx-name-richText1 .ql-editor").innerHTML();
150+
expect(html).toMatch(/class="ql-color-/);
151+
expect(html).toMatch(/class="ql-bg-/);
152+
expect(html).toMatch(/class="ql-indent-/);
153+
expect(html).toMatch(/data-style-format="class"/);
154+
expect(html).not.toMatch(/style="color:/);
155+
expect(html).not.toMatch(/style="background-color:/);
156+
expect(html).not.toMatch(/style="padding-left:/);
157+
});
158+
159+
test("compares with a screenshot baseline of the View/Edit Code dialog in class mode", async ({ page }) => {
160+
await page.goto("/p/classmode");
161+
await page.waitForLoadState("networkidle");
162+
await page.click(".mx-name-richText1 .ql-toolbar button.ql-view-code");
163+
await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot(
164+
`classModeViewCodeDialog.png`
165+
);
166+
});
167+
136168
test("compares with a screenshot for rich text inside modal popup layout", async ({ page }) => {
137169
await page.goto("/");
138170
await page.waitForLoadState("networkidle");

packages/pluggableWidgets/rich-text-web/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@
2424
},
2525
"testProject": {
2626
"githubUrl": "https://github.com/mendix/testProjects",
27-
"branchName": "rich-text-v4-web"
27+
"branchName": "rich-text-v4-web-v2"
2828
},
2929
"scripts": {
3030
"build": "cross-env MPKOUTPUT=RichText.mpk pluggable-widgets-tools build:web",
3131
"create-gh-release": "rui-create-gh-release",
3232
"create-translation": "rui-create-translation",
3333
"dev": "cross-env MPKOUTPUT=RichText.mpk pluggable-widgets-tools start:web",
34-
"e2e": "run-e2e ci",
35-
"e2edev": "run-e2e dev --with-preps",
34+
"e2e": "MENDIX_VERSION=11.9.1 run-e2e ci",
35+
"e2edev": "MENDIX_VERSION=11.9.1 run-e2e dev --with-preps",
3636
"format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .",
3737
"lint": "eslint src/ package.json",
3838
"publish-marketplace": "rui-publish-marketplace",
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { CustomListItem, CustomListItemClass, STANDARD_LIST_TYPES } from "../utils/formats/customList";
2+
3+
// CustomListItem and CustomListItemClass extend Quill's ListItem blot.
4+
// We test only the static helpers and the constructor-level DOM mutation,
5+
// which do not require a live Quill / Scroll instance.
6+
7+
function makeListNode(listType = "ordered"): HTMLElement {
8+
const li = document.createElement("li");
9+
li.dataset.list = listType;
10+
return li;
11+
}
12+
13+
describe("STANDARD_LIST_TYPES", () => {
14+
it("contains exactly the four standard types", () => {
15+
expect(STANDARD_LIST_TYPES).toEqual(["ordered", "checked", "unchecked", "bullet"]);
16+
});
17+
});
18+
19+
describe("CustomListItem.formats", () => {
20+
it("returns data-list value for standard list types", () => {
21+
const node = makeListNode("ordered");
22+
expect(CustomListItem.formats(node)).toBe("ordered");
23+
});
24+
25+
it("prefers data-custom-list over data-list when both are present", () => {
26+
const node = makeListNode("ordered");
27+
node.dataset.customList = "lower-alpha";
28+
expect(CustomListItem.formats(node)).toBe("lower-alpha");
29+
});
30+
31+
it("returns undefined when neither attribute is present", () => {
32+
const node = document.createElement("li");
33+
expect(CustomListItem.formats(node)).toBeUndefined();
34+
});
35+
});
36+
37+
describe("CustomListItemClass — styleFormat marker contract", () => {
38+
// CustomListItemClass constructor assigns domNode.dataset.styleFormat = "class".
39+
// Instantiating it requires a live Quill Scroll instance (a Quill integration concern),
40+
// so here we verify the contract at the class-definition level and the DOM-mutation logic
41+
// in isolation.
42+
43+
it("is a subclass of CustomListItem", () => {
44+
expect(Object.getPrototypeOf(CustomListItemClass)).toBe(CustomListItem);
45+
});
46+
47+
it("the styleFormat marker 'class' round-trips correctly on a DOM node (logic under test)", () => {
48+
// This mirrors exactly what the constructor body does:
49+
// domNode.dataset.styleFormat = "class";
50+
const node = makeListNode("ordered");
51+
node.dataset.styleFormat = "class";
52+
expect(node.dataset.styleFormat).toBe("class");
53+
});
54+
55+
it("inline-mode list nodes do NOT have a styleFormat marker by default", () => {
56+
const node = makeListNode("ordered");
57+
expect(node.dataset.styleFormat).toBeUndefined();
58+
});
59+
});
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { FONT_LIST, FontClassAttributor, FontStyleAttributor, formatCustomFonts } from "../utils/formats/fonts";
2+
3+
// parchment ClassAttributor and StyleAttributor operate directly on HTMLElement nodes —
4+
// no Quill instance is needed for unit-level attribute tests.
5+
6+
function makeSpan(): HTMLElement {
7+
return document.createElement("span");
8+
}
9+
10+
// FontStyleAttributor --------------------------------------------------------
11+
12+
describe("FontStyleAttributor", () => {
13+
let attr: FontStyleAttributor;
14+
15+
beforeEach(() => {
16+
attr = new FontStyleAttributor([]);
17+
});
18+
19+
it("adds font-family style for a known font value", () => {
20+
const node = makeSpan();
21+
const result = attr.add(node, "arial");
22+
expect(result).toBe(true);
23+
expect(node.style.fontFamily).toMatch(/arial/i);
24+
expect(node.dataset.value).toBe("arial");
25+
});
26+
27+
it("returns false for an unknown font value", () => {
28+
const node = makeSpan();
29+
const result = attr.add(node, "not-a-real-font");
30+
expect(result).toBe(false);
31+
expect(node.style.fontFamily).toBe("");
32+
});
33+
34+
it("reads back the value via dataset.value", () => {
35+
const node = makeSpan();
36+
attr.add(node, "courier-new");
37+
expect(attr.value(node)).toBe("courier-new");
38+
});
39+
40+
it("returns empty string for a node with no dataset.value", () => {
41+
const node = makeSpan();
42+
expect(attr.value(node)).toBe("");
43+
});
44+
45+
it("applies custom fonts passed to the constructor", () => {
46+
const custom = new FontStyleAttributor([
47+
{ value: "my-font", description: "My Font", style: "MyFont, sans-serif" }
48+
]);
49+
const node = makeSpan();
50+
expect(custom.add(node, "my-font")).toBe(true);
51+
expect(node.style.fontFamily).toMatch(/MyFont/i);
52+
});
53+
54+
it("FONT_LIST contains all 13 fonts including serif", () => {
55+
const values = FONT_LIST.map(f => f.value);
56+
expect(values).toContain("serif");
57+
expect(values).toHaveLength(13);
58+
});
59+
});
60+
61+
// FontClassAttributor --------------------------------------------------------
62+
63+
describe("FontClassAttributor", () => {
64+
let attr: FontClassAttributor;
65+
66+
beforeEach(() => {
67+
attr = new FontClassAttributor([]);
68+
});
69+
70+
it("adds font-family-<value> class for a known font value", () => {
71+
const node = makeSpan();
72+
const result = attr.add(node, "arial");
73+
expect(result).toBe(true);
74+
expect(node.classList.contains("font-family-arial")).toBe(true);
75+
expect(node.dataset.value).toBe("arial");
76+
});
77+
78+
it("returns false for an unknown font value and adds no class", () => {
79+
const node = makeSpan();
80+
const result = attr.add(node, "not-a-real-font");
81+
expect(result).toBe(false);
82+
const hasClass = Array.from(node.classList).some(c => c.startsWith("font-family-"));
83+
expect(hasClass).toBe(false);
84+
});
85+
86+
it("reads back the value via dataset.value", () => {
87+
const node = makeSpan();
88+
attr.add(node, "impact");
89+
expect(attr.value(node)).toBe("impact");
90+
});
91+
92+
it("returns empty string for a node with no dataset.value", () => {
93+
const node = makeSpan();
94+
expect(attr.value(node)).toBe("");
95+
});
96+
97+
it("adds font-family-serif class for the serif font (Critical #3 regression guard)", () => {
98+
const node = makeSpan();
99+
const result = attr.add(node, "serif");
100+
expect(result).toBe(true);
101+
expect(node.classList.contains("font-family-serif")).toBe(true);
102+
});
103+
104+
it("applies custom fonts passed to the constructor", () => {
105+
const custom = new FontClassAttributor([
106+
{ value: "my-font", description: "My Font", style: "MyFont, sans-serif" }
107+
]);
108+
const node = makeSpan();
109+
expect(custom.add(node, "my-font")).toBe(true);
110+
expect(node.classList.contains("font-family-my-font")).toBe(true);
111+
});
112+
113+
it("emits class-based name, not inline style", () => {
114+
const node = makeSpan();
115+
attr.add(node, "helvetica");
116+
expect(node.style.fontFamily).toBe("");
117+
expect(node.classList.contains("font-family-helvetica")).toBe(true);
118+
});
119+
});
120+
121+
// formatCustomFonts ----------------------------------------------------------
122+
123+
describe("formatCustomFonts", () => {
124+
it("maps custom font objects to FONT_LIST shape", () => {
125+
const result = formatCustomFonts([{ fontName: "My Brand Font", fontStyle: "MyBrandFont, sans-serif" }]);
126+
expect(result).toEqual([
127+
{ value: "my-brand-font", description: "My Brand Font", style: "MyBrandFont, sans-serif" }
128+
]);
129+
});
130+
131+
it("lowercases and hyphenates multi-word font names", () => {
132+
const result = formatCustomFonts([{ fontName: "Open Sans", fontStyle: "Open Sans, sans-serif" }]);
133+
expect(result[0].value).toBe("open-sans");
134+
});
135+
136+
it("returns an empty array when called with no arguments", () => {
137+
expect(formatCustomFonts()).toEqual([]);
138+
});
139+
140+
it("returns an empty array for an empty input", () => {
141+
expect(formatCustomFonts([])).toEqual([]);
142+
});
143+
144+
it("handles undefined fontName gracefully", () => {
145+
const result = formatCustomFonts([{ fontName: undefined as any, fontStyle: "serif" }]);
146+
expect(result[0].value).toBe("");
147+
});
148+
});

0 commit comments

Comments
 (0)