Files
edgeever/apps/web/src/styles/editor-typography.test.mjs
T
tianma-if 4cae94daa3 fix(web): tint checked task boxes with the note palette
Checked boxes use the palette accent in the editor, shares, and WeChat copy.
2026-09-29 19:41:39 +08:00

175 lines
8.0 KiB
JavaScript

import { readFileSync } from "node:fs";
import { describe, expect, test } from "bun:test";
import { MEMO_CONTENT_STYLE } from "@edgeever/shared";
const PRESET_THEME_FILES = [
"minimal-emerald.css",
"outline-emerald.css",
"wechat-green.css",
"modern-mint.css",
];
const readStyle = (relativePath) => readFileSync(new URL(relativePath, import.meta.url), "utf8");
const declarationsForSelector = (source, selectorSuffix) =>
[...source.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
.filter(([, selectors]) => selectors.split(",").some((selector) => selector.trim().endsWith(selectorSuffix)))
.map(([, , declarations]) => declarations)
.join("\n");
describe("editor typography contract", () => {
test("keeps the shared body rhythm compact", () => {
expect(MEMO_CONTENT_STYLE.body.lineHeight / MEMO_CONTENT_STYLE.body.fontSize).toBe(1.625);
expect(MEMO_CONTENT_STYLE.body.paragraphSpacing).toBe(8);
});
test("configures punctuation squeezing and overflow wrapping for body text", () => {
const globals = readStyle("./globals.css");
const editorRules = declarationsForSelector(globals, ".ProseMirror");
const markdownRules = declarationsForSelector(globals, ".markdown-content");
expect(editorRules).toMatch(/overflow-wrap\s*:\s*break-word/);
expect(editorRules).toMatch(/font-feature-settings\s*:\s*["']chws["']\s*1/);
expect(markdownRules).toMatch(/overflow-wrap\s*:\s*break-word/);
expect(markdownRules).toMatch(/font-feature-settings\s*:\s*["']chws["']\s*1/);
});
test("keeps the native editor and applies account prose instead of paper themes", () => {
const editorPane = readStyle("../components/EditorPane.tsx");
const publishLayout = readStyle("./publish-layout.css");
expect(editorPane).toContain('data-editor-theme="default"');
expect(editorPane).toContain("data-note-palette={noteProse.palette}");
expect(editorPane).toContain("noteProseCssVariables(noteProse)");
expect(editorPane).not.toContain("isPaperEditorTheme");
expect(editorPane).toContain("MEMO_CONTENT_STYLE.divider");
expect(publishLayout).toContain('[data-editor-theme="letter"]');
expect(publishLayout).toContain("[data-paper-theme]");
expect(publishLayout).not.toContain("[data-publish-layout]");
});
test("styles default-theme external hyperlinks so they are distinct from body text", () => {
const globals = readStyle("./globals.css");
const linkRules = declarationsForSelector(globals, ".ProseMirror a");
const markdownLinkRules = declarationsForSelector(globals, ".markdown-content a");
expect(linkRules).toMatch(/color\s*:\s*rgb\(var\(--slate-700-rgb\)\)/);
expect(linkRules).toMatch(/text-decoration\s*:\s*underline/);
expect(markdownLinkRules).toMatch(/color\s*:\s*var\(--brand-green-text\)/);
expect(markdownLinkRules).toMatch(/text-decoration\s*:\s*underline/);
});
test("keeps the rich-editor placeholder aligned with body typography", () => {
const globals = readStyle("./globals.css");
const placeholderRules = declarationsForSelector(
globals,
".ProseMirror.edgeever-note-rich-editor p.is-empty::before",
);
expect(placeholderRules).toMatch(/font-size\s*:\s*inherit/);
expect(placeholderRules).toMatch(/font-weight\s*:\s*inherit/);
expect(placeholderRules).toMatch(/line-height\s*:\s*inherit/);
expect(placeholderRules).toMatch(/color\s*:\s*#a8b5c4/);
});
test("paints bold text with the palette accent", () => {
const prose = readStyle("./note-prose.css");
const boldRules = declarationsForSelector(prose, ".ProseMirror strong");
expect(boldRules).toMatch(/color:\s*var\(--note-palette-accent\)/);
expect(prose).not.toMatch(/--note-palette-dark-accent/);
expect(prose).not.toContain('data-note-palette="custom"');
});
test("named palettes recolor links, inline code, bold, and checked task boxes", () => {
const prose = readStyle("./note-prose.css");
expect(prose).toMatch(/color:\s*var\(--note-palette-link\)/);
expect(prose).toMatch(/color:\s*var\(--note-palette-code-text\)/);
expect(prose).toMatch(/color:\s*var\(--note-palette-accent\)/);
expect(prose).toMatch(/accent-color:\s*var\(--note-palette-accent\)/);
expect(prose).toMatch(/ul\[data-type="taskList"\] li\[data-checked\] > label input/);
expect(prose).toMatch(/\.edgeever-code-block code/);
expect(prose).not.toMatch(/--note-palette-text/);
expect(prose).not.toMatch(/--note-palette-surface/);
expect(prose).not.toMatch(/--note-palette-muted/);
expect(prose).not.toMatch(/--note-palette-divider/);
expect(prose).not.toMatch(/--note-palette-code-bg/);
expect(prose).not.toMatch(/blockquote/);
});
test("keeps bold and italic text visible across platform font fallbacks", () => {
const globals = readStyle("./globals.css");
const mobileEditor = readStyle("./mobile-markdown-editor.css");
const defaultBoldRules = declarationsForSelector(globals, ".ProseMirror strong");
const defaultItalicRules = declarationsForSelector(globals, ".ProseMirror em");
const mobileBoldRules = declarationsForSelector(
mobileEditor,
".edgeever-mobile-tiptap-content strong",
);
const mobileItalicRules = declarationsForSelector(
mobileEditor,
".edgeever-mobile-tiptap-content em",
);
expect(defaultBoldRules).toMatch(/font-synthesis\s*:\s*weight style/);
expect(defaultBoldRules).toMatch(/font-weight\s*:\s*800/);
expect(defaultItalicRules).toMatch(/font-synthesis\s*:\s*weight style/);
expect(defaultItalicRules).toMatch(/font-style\s*:\s*italic/);
expect(mobileBoldRules).toMatch(/font-synthesis\s*:\s*weight style/);
expect(mobileBoldRules).toMatch(/font-weight\s*:\s*800/);
expect(mobileItalicRules).toMatch(/font-synthesis\s*:\s*weight style/);
expect(mobileItalicRules).toMatch(/font-style\s*:\s*italic/);
for (const filename of PRESET_THEME_FILES) {
const source = readStyle(`./editor-themes/${filename}`);
const boldRules = declarationsForSelector(source, ".ProseMirror strong");
const weights = [...boldRules.matchAll(/font-weight\s*:\s*(\d+)/g)].map((match) => Number(match[1]));
expect(weights.length).toBeGreaterThan(0);
expect(Math.max(...weights)).toBeGreaterThanOrEqual(700);
}
});
test("does not let preset themes override body rhythm", () => {
for (const filename of PRESET_THEME_FILES) {
const source = readStyle(`./editor-themes/${filename}`);
const editorRules = declarationsForSelector(source, ".ProseMirror");
const paragraphRules = declarationsForSelector(source, ".ProseMirror p");
const listRules = [
declarationsForSelector(source, ".ProseMirror ul"),
declarationsForSelector(source, ".ProseMirror ol"),
declarationsForSelector(source, ".ProseMirror li"),
].join("\n");
expect(editorRules).not.toMatch(/(?:font-size|line-height)\s*:/);
expect(paragraphRules).not.toMatch(/(?:line-height|margin|padding)(?:-[a-z]+)?\s*:/);
expect(listRules).not.toMatch(/(?:line-height|margin)(?:-[a-z]+)?\s*:/);
}
});
test("does not let preset themes override table geometry", () => {
const geometryPattern =
/(?<![\w-])(?:padding|min-width|max-width|min-height|max-height|line-height|table-layout|width|height)(?:-[a-z]+)?\s*:/;
for (const filename of ["base.css", ...PRESET_THEME_FILES]) {
const source = readStyle(`./editor-themes/${filename}`);
const tableRules = [
declarationsForSelector(source, ".ProseMirror table"),
declarationsForSelector(source, ".ProseMirror .tableWrapper"),
declarationsForSelector(source, ".ProseMirror th"),
declarationsForSelector(source, ".ProseMirror td"),
declarationsForSelector(source, ".ProseMirror th p"),
declarationsForSelector(source, ".ProseMirror td p"),
declarationsForSelector(source, ".ProseMirror col"),
declarationsForSelector(source, ".ProseMirror tr"),
].join("\n");
expect(tableRules).not.toMatch(geometryPattern);
expect(tableRules).not.toMatch(/--mobile-table-column-width\s*:/);
expect(tableRules).not.toMatch(/\bborder\s*:/);
}
});
});