Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: docs
> ./e2e/tests/paragraph-styles.spec.ts
/**
* Paragraph Style Tests
*
* Comprehensive tests for paragraph styles including:
* - Normal, Heading 1-3, Title, Subtitle styles
* - Style transitions
* - Document structure with styles
* - Styles with additional formatting
*/
import { test, expect } from '@playwright/test';
import { EditorPage } from '../helpers/editor-page';
import * as assertions from '../helpers/assertions';
test.describe('Basic Styles', () => {
let editor: EditorPage;
test.beforeEach(async ({ page }) => {
editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
await editor.newDocument();
await editor.focus();
});
test('apply Normal style', async ({ page }) => {
await editor.typeText('Normal paragraph text');
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Normal paragraph text');
});
test('apply Heading 1 style', async ({ page }) => {
await editor.typeText('Main Heading');
await editor.applyHeading1();
await assertions.assertDocumentContainsText(page, 'Main Heading');
});
test('apply Heading 2 style', async ({ page }) => {
await editor.typeText('Sub Heading');
await editor.applyHeading2();
await assertions.assertDocumentContainsText(page, 'Sub Heading');
});
test('apply Heading 3 style', async ({ page }) => {
await editor.typeText('Section Heading');
await editor.applyHeading3();
await assertions.assertDocumentContainsText(page, 'Section Heading');
});
test('apply Title style', async ({ page }) => {
await editor.typeText('Document Title');
await editor.applyTitleStyle();
await assertions.assertDocumentContainsText(page, 'Document Title');
});
test('apply Subtitle style', async ({ page }) => {
await editor.typeText('Document Subtitle');
await editor.applySubtitleStyle();
await assertions.assertDocumentContainsText(page, 'Document Subtitle');
});
});
test.describe('Style Transitions', () => {
let editor: EditorPage;
test.beforeEach(async ({ page }) => {
editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
await editor.newDocument();
await editor.focus();
});
test('change from Normal to Heading 1', async ({ page }) => {
await editor.typeText('Promote to heading');
await editor.applyNormalStyle();
await editor.applyHeading1();
await assertions.assertDocumentContainsText(page, 'Promote to heading');
});
test('change from Heading 1 to Normal', async ({ page }) => {
await editor.typeText('Demote to normal');
await editor.applyHeading1();
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Demote to normal');
});
test('change from Heading 1 to Heading 2', async ({ page }) => {
await editor.typeText('Demote heading level');
await editor.applyHeading1();
await editor.applyHeading2();
await assertions.assertDocumentContainsText(page, 'Demote heading level');
});
test('change from Heading 2 to Heading 3', async ({ page }) => {
await editor.typeText('Further demote');
await editor.applyHeading2();
await editor.applyHeading3();
await assertions.assertDocumentContainsText(page, 'Further demote');
});
test('change from Title to Heading 1', async ({ page }) => {
await editor.typeText('Title to Heading');
await editor.applyTitleStyle();
await editor.applyHeading1();
await assertions.assertDocumentContainsText(page, 'Title to Heading');
});
test('cycle through styles', async ({ page }) => {
await editor.typeText('Style cycle');
await editor.applyNormalStyle();
await editor.applyHeading1();
await editor.applyHeading2();
await editor.applyHeading3();
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Style cycle');
});
});
test.describe('Document Structure', () => {
let editor: EditorPage;
test.beforeEach(async ({ page }) => {
editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
await editor.newDocument();
await editor.focus();
});
test('multiple headings in document', async ({ page }) => {
await editor.typeText('Chapter One');
await editor.applyHeading1();
await editor.pressEnter();
await editor.typeText('Introduction');
await editor.applyHeading2();
await editor.pressEnter();
await editor.typeText('This is the body text.');
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Chapter One');
await assertions.assertDocumentContainsText(page, 'Introduction');
await assertions.assertDocumentContainsText(page, 'This is the body text.');
});
test('complete document structure', async ({ page }) => {
await editor.typeText('Annual Report');
await editor.applyTitleStyle();
await editor.pressEnter();
await editor.typeText('Fiscal Year 2024');
await editor.applySubtitleStyle();
await editor.pressEnter();
await editor.typeText('Executive Summary');
await editor.applyHeading1();
await editor.pressEnter();
await editor.typeText('This document provides an overview.');
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Annual Report');
await assertions.assertDocumentContainsText(page, 'Fiscal Year 2024');
await assertions.assertDocumentContainsText(page, 'Executive Summary');
});
test('nested heading structure', async ({ page }) => {
await editor.typeText('Chapter 1');
await editor.applyHeading1();
await editor.pressEnter();
await editor.typeText('Section 1.1');
await editor.applyHeading2();
await editor.pressEnter();
await editor.typeText('Subsection 1.1.1');
await editor.applyHeading3();
await editor.pressEnter();
await editor.typeText('Body text here.');
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Chapter 1');
await assertions.assertDocumentContainsText(page, 'Section 1.1');
await assertions.assertDocumentContainsText(page, 'Subsection 1.1.1');
});
test('alternating headings and paragraphs', async ({ page }) => {
await editor.typeText('Heading');
await editor.applyHeading1();
await editor.pressEnter();
await editor.typeText('Paragraph one.');
await editor.applyNormalStyle();
await editor.pressEnter();
await editor.typeText('Another Heading');
await editor.applyHeading1();
await editor.pressEnter();
await editor.typeText('Paragraph two.');
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Heading');
await assertions.assertDocumentContainsText(page, 'Paragraph one.');
await assertions.assertDocumentContainsText(page, 'Another Heading');
await assertions.assertDocumentContainsText(page, 'Paragraph two.');
});
});
test.describe('Styles with Formatting', () => {
let editor: EditorPage;
test.beforeEach(async ({ page }) => {
editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
await editor.newDocument();
await editor.focus();
});
test('style with additional bold', async ({ page }) => {
await editor.typeText('Heading with extra bold');
await editor.applyHeading1();
await editor.selectText('extra');
await editor.applyBold();
await assertions.assertDocumentContainsText(page, 'Heading with extra bold');
});
test('style with additional italic', async ({ page }) => {
await editor.typeText('Normal with italic word');
await editor.applyNormalStyle();
await editor.selectText('italic');
await editor.applyItalic();
await assertions.assertDocumentContainsText(page, 'Normal with italic word');
});
test('style with color override', async ({ page }) => {
await editor.typeText('Colored heading');
await editor.applyHeading1();
await editor.selectAll();
await editor.setTextColor('#FF0000');
await assertions.assertDocumentContainsText(page, 'Colored heading');
});
test('style with alignment', async ({ page }) => {
await editor.typeText('Centered Heading');
await editor.applyHeading1();
await editor.alignCenter();
await assertions.assertDocumentContainsText(page, 'Centered Heading');
});
test('style with font override', async ({ page }) => {
await editor.typeText('Custom Font Heading');
await editor.applyHeading1();
await editor.selectAll();
await editor.setFontFamily('Georgia');
await assertions.assertDocumentContainsText(page, 'Custom Font Heading');
});
test('heading with underline', async ({ page }) => {
await editor.typeText('Underlined Heading');
await editor.applyHeading2();
await editor.selectAll();
await editor.applyUnderline();
await assertions.assertTextIsUnderlined(page, 'Underlined Heading');
});
});
test.describe('Styles Undo/Redo', () => {
let editor: EditorPage;
test.beforeEach(async ({ page }) => {
editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
await editor.newDocument();
await editor.focus();
});
test('undo style change', async ({ page }) => {
await editor.typeText('Undo style test');
await editor.applyHeading1();
await editor.undo();
await assertions.assertDocumentContainsText(page, 'Undo style test');
});
test('redo style change', async ({ page }) => {
await editor.typeText('Redo style test');
await editor.applyHeading1();
await editor.undo();
await editor.redo();
await assertions.assertDocumentContainsText(page, 'Redo style test');
});
test('multiple undo style changes', async ({ page }) => {
await editor.typeText('Multiple undo');
await editor.applyNormalStyle();
await editor.applyHeading1();
await editor.applyHeading2();
await editor.undo();
await editor.undo();
await assertions.assertDocumentContainsText(page, 'Multiple undo');
});
});
test.describe('Styles Edge Cases', () => {
let editor: EditorPage;
test.beforeEach(async ({ page }) => {
editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
await editor.newDocument();
await editor.focus();
});
test('style on empty paragraph', async ({ page }) => {
await editor.applyHeading1();
await editor.typeText('Text after style');
await assertions.assertDocumentContainsText(page, 'Text after style');
});
test('style preserves text content', async ({ page }) => {
await editor.typeText('Important content that should not change');
await editor.applyHeading1();
await editor.applyHeading2();
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Important content that should not change');
});
test('rapid style changes', async ({ page }) => {
await editor.typeText('Rapid changes');
await editor.applyNormalStyle();
await editor.applyHeading1();
await editor.applyHeading2();
await editor.applyHeading3();
await editor.applyTitleStyle();
await editor.applyNormalStyle();
await assertions.assertDocumentContainsText(page, 'Rapid changes');
});
test('style with special characters', async ({ page }) => {
await editor.typeText('Heading: @#$%^&*()');
await editor.applyHeading1();
await assertions.assertDocumentContainsText(page, 'Heading: @#$%^&*()');
});
test('style with unicode', async ({ page }) => {
await editor.typeText('日本語見出し');
await editor.applyHeading1();
await assertions.assertDocumentContainsText(page, '日本語見出し');
});
test('style with numbers', async ({ page }) => {
await editor.typeText('Chapter 1: Introduction');
await editor.applyHeading1();
await assertions.assertDocumentContainsText(page, 'Chapter 1: Introduction');
});
test('long heading text', async ({ page }) => {
await editor.typeText(
'This is a very long heading that spans multiple words and tests how the style handles longer content'
);
await editor.applyHeading1();
await assertions.assertDocumentContainsText(page, 'This is a very long heading');
});
});
test.describe('Empty-paragraph style + next-style on Enter', () => {
let editor: EditorPage;
test.beforeEach(async ({ page }) => {
editor = new EditorPage(page);
await editor.goto();
await editor.waitForReady();
await editor.newDocument();
await editor.focus();
});
/**
* Read each paragraph's styleId + the mark types on its runs straight from
* the ProseMirror model — the source of truth both the hidden editor and
* the visible painter derive from, and which CSS can't mask.
*/
async function paragraphModel(
page: import('@playwright/test').Page
): Promise<Array<{ styleId: string | null; text: string; marks: string[] }>> {
return page.evaluate(() => {
const pm = document.querySelector('.ProseMirror') as unknown as {
pmViewDesc?: { node?: { forEach: (cb: (p: unknown) => void) => void } };
} | null;
const node = pm?.pmViewDesc?.node;
if (!node) return [];
const out: Array<{ styleId: string | null; text: string; marks: string[] }> = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
node.forEach((p: any) => {
const marks = new Set<string>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
p.forEach((c: any) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (c.isText) c.marks.forEach((m: any) => marks.add(m.type.name));
});
out.push({ styleId: p.attrs.styleId ?? null, text: p.textContent, marks: [...marks] });
});
return out;
});
}
test('text typed after applying Heading 1 to an empty paragraph is styled', async ({ page }) => {
// Regression: applying a heading to an empty paragraph then typing used to
// produce unstyled text — the style picker's refocus discarded the stored
// marks before the first keystroke.
await editor.applyHeading1();
await editor.typeText('Heading text');
const paras = await paragraphModel(page);
expect(paras[0].styleId).toBe('Heading1');
expect(paras[0].marks).toContain('bold');
expect(paras[0].marks).toContain('fontSize');
await assertions.assertTextIsBold(page, 'Heading text');
});
test('Enter at the end of a heading drops to the body (next) style', async ({ page }) => {
await editor.applyHeading1();
await editor.typeText('Heading One');
await editor.pressEnter();
await editor.typeText('Body paragraph text');
const paras = await paragraphModel(page);
expect(paras).toHaveLength(2);
expect(paras[0].styleId).toBe('Heading1');
expect(paras[0].marks).toContain('bold');
// The heading's `w:next` is Normal, so the new paragraph is body text.
expect(paras[1].styleId).toBe('Normal');
expect(paras[1].marks).not.toContain('bold');
await assertions.assertTextIsNotBold(page, 'Body paragraph text');
});
});