From e61ca71dffc0b5aa9585c0039cf980056d1dadd8 Mon Sep 17 00:00:00 2001 From: bdbch <6538827+bdbch@users.noreply.github.com> Date: Thu, 21 May 2026 07:12:15 +0200 Subject: [PATCH] test: migrate Cypress e2e suite to Playwright + port core specs to Vitest (#7845) * refactor: remove Cypress setup * test: back up Cypress specs * test: add Playwright setup * test: add Playwright test helpers for editor interaction * test: add Playwright test for Cut command and improve test helpers - Add Playwright spec for Cut command with tests for cutting to start/end of document - Add `start:demos` script to run demos on port 4080 - Update Playwright config to use port 4080 and run demos via `start:demos` script - Refactor `setEditorContent` helper to use `getEditor` instead of direct window access * test: remove backed up Cypress spec for Cut command * test: migrate Commands Cypress specs to Playwright Ports InsertContent, InsertContentApplyingRules, and SetContent specs from the backed-up Cypress suite to Playwright, then removes the migrated backups. * test: migrate Demos Cypress specs to Playwright Ports CollaborationSplitPane and SingleRoomCollab specs from the backed-up Cypress suite to Playwright and removes the migrated backups. * test: migrate Examples Cypress specs to Playwright Ports 33 Examples demos from the backed-up Cypress suite to Playwright and removes the migrated backups. * test: migrate Experiments Cypress specs to Playwright * test: migrate Extensions Cypress specs to Playwright * test: migrate GuideContent Cypress specs to Playwright * test: migrate GuideGettingStarted and GuideMarkViews Cypress specs to Playwright * test: migrate Marks Cypress specs to Playwright * test: migrate Nodes Cypress specs to Playwright * test(core): add unit tests for plugin order and transformPastedHTML * chore: add Playwright cache and blob-report directories to .gitignore * docs(AGENTS.md): migrate testing docs from Cypress to Playwright * test: migrate pluginOrder and transformPastedHTML Cypress specs to Playwright * docs(AGENTS.md): update validation checklist and troubleshooting docs * docs: update instructions for transitioning from Cypress to Playwright * chore(playwright): switch CI reporter from HTML to blob and add HTML for local runs * feat(e2e): add end-to-end test workflow with Playwright and report merging * feat(tests): add Playwright report command for end-to-end tests * test: refactor Cut command tests to use setTextSelection for text selection * feat(tests): enhance Playwright test commands for project-specific execution * docs: update Playwright test commands for improved clarity and browser support * fix: update Playwright demo server command and script names for consistency * fix: update key combination for text selection based on platform * test: remove deprecated Playwright e2e tests for Commands demos and add them to vitest * test: migrate InsertContent e2e tests to vitest * test: migrate BackgroundColor and Collaboration e2e tests to vitest * test: migrate Example e2e tests (Accessibility, Book, CodeBlockLanguage, CollaborativeEditing, NodePos) to Vitest * test: remove deprecated Playwright e2e tests and add new Vitest tests for Details, Subscript, Superscript, and TextStyle features * migrate hybrid specs over to vitest --- .eslintrc.js | 5 +- .github/instructions/tiptap.instructions.md | 56 +- .github/workflows/build.yml | 118 ++- .gitignore | 7 +- .lintstagedrc.cjs | 14 + AGENTS.md | 59 +- demos/package.json | 1 + demos/src/Commands/Cut/React/index.spec.js | 18 - .../InsertContent/React/index.spec.js | 145 --- .../React/index.spec.js | 35 - .../Commands/SetContent/React/index.spec.js | 202 ----- .../React/index.spec.js | 24 - .../CollaborationSplitPane/index.spec.ts | 39 + .../SingleRoomCollab/React/index.spec.js | 21 - .../src/Demos/SingleRoomCollab/index.spec.ts | 26 + .../Accessibility/React/index.spec.js | 80 -- .../AutolinkValidation/React/index.spec.js | 80 -- .../AutolinkValidation/Vue/index.spec.js | 40 - .../Examples/AutolinkValidation/index.spec.ts | 140 +++ demos/src/Examples/Book/React/index.spec.js | 12 - demos/src/Examples/Book/Vue/index.spec.js | 12 - .../Examples/CSSModules/React/index.spec.js | 11 - .../src/Examples/CSSModules/Vue/index.spec.js | 11 - demos/src/Examples/CSSModules/index.spec.ts | 25 + .../CodeBlockLanguage/React/index.spec.js | 29 - .../CodeBlockLanguage/Vue/index.spec.js | 29 - .../CollaborativeEditing/React/index.spec.js | 21 - .../CollaborativeEditing/Vue/index.spec.js | 21 - .../Examples/Community/React/index.spec.js | 38 - .../src/Examples/Community/Vue/index.spec.js | 38 - demos/src/Examples/Community/index.spec.ts | 60 ++ .../CustomDocument/React/index.spec.js | 46 - .../Examples/CustomDocument/Vue/index.spec.js | 48 - .../src/Examples/CustomDocument/index.spec.ts | 82 ++ .../CustomParagraph/React/index.spec.js | 25 - .../CustomParagraph/Vue/index.spec.js | 25 - .../Examples/CustomParagraph/index.spec.ts | 33 + .../src/Examples/Default/React/index.spec.js | 169 ---- .../src/Examples/Default/Svelte/index.spec.js | 169 ---- demos/src/Examples/Default/Vue/index.spec.js | 136 --- demos/src/Examples/Default/index.spec.ts | 176 ++++ demos/src/Examples/Drawing/Vue/index.spec.js | 38 - demos/src/Examples/Drawing/index.spec.ts | 54 ++ .../EnterShortcuts/React/index.spec.js | 25 - .../Examples/EnterShortcuts/Vue/index.spec.js | 23 - .../src/Examples/EnterShortcuts/index.spec.ts | 51 ++ .../Examples/Formatting/React/index.spec.js | 36 - .../src/Examples/Formatting/Vue/index.spec.js | 36 - demos/src/Examples/Formatting/index.spec.ts | 61 ++ demos/src/Examples/Images/React/index.spec.js | 29 - demos/src/Examples/Images/Vue/index.spec.js | 35 - demos/src/Examples/Images/index.spec.ts | 33 + .../React/index.spec.js | 27 - .../InteractivityComponent/Vue/index.spec.js | 27 - .../InteractivityComponent/index.spec.ts | 38 + .../React/index.spec.js | 42 - .../Vue/index.spec.js | 42 - .../index.spec.ts | 83 ++ .../Vue/index.spec.js | 24 - .../index.spec.ts | 45 + demos/src/Examples/JSX/React/index.spec.js | 16 - demos/src/Examples/JSX/Vue/index.spec.js | 16 - demos/src/Examples/JSX/index.spec.ts | 31 + .../MarkdownShortcuts/React/index.spec.js | 63 -- .../MarkdownShortcuts/Vue/index.spec.js | 63 -- .../Examples/MarkdownShortcuts/index.spec.ts | 84 ++ demos/src/Examples/Menus/React/index.spec.js | 46 - demos/src/Examples/Menus/Vue/index.spec.js | 46 - demos/src/Examples/Menus/index.spec.ts | 58 ++ .../src/Examples/Minimal/React/index.spec.js | 35 - demos/src/Examples/Minimal/Vue/index.spec.js | 35 - demos/src/Examples/Minimal/index.spec.ts | 68 ++ .../Examples/MultiMention/React/index.spec.js | 226 ----- .../Examples/MultiMention/Vue/index.spec.js | 7 - demos/src/Examples/MultiMention/index.spec.ts | 336 +++++++ .../src/Examples/NodePos/React/index.spec.js | 139 --- .../Examples/Performance/React/index.spec.js | 12 - .../ResizableImages/React/index.spec.js | 24 - .../ResizableImages/Vue/index.spec.js | 24 - demos/src/Examples/Savvy/React/index.spec.js | 34 - demos/src/Examples/Savvy/Vue/index.spec.js | 34 - demos/src/Examples/Savvy/index.spec.ts | 57 ++ .../StaticRendering/React/index.spec.js | 12 - .../React/index.spec.js | 9 - demos/src/Examples/Tables/React/index.spec.js | 103 --- demos/src/Examples/Tables/Vue/index.spec.js | 103 --- demos/src/Examples/Tasks/React/index.spec.js | 32 - demos/src/Examples/Tasks/Vue/index.spec.js | 32 - demos/src/Examples/Tasks/index.spec.ts | 66 ++ .../TextDirection/React/index.spec.js | 25 - .../src/Examples/Transition/Vue/index.spec.js | 33 - .../TypographyRTL/React/index.spec.js | 37 - .../src/Examples/TypographyRTL/index.spec.ts | 76 ++ .../CollaborationAnnotation/Vue/index.spec.js | 70 -- .../CollaborationAnnotation/index.spec.ts | 21 + .../Experiments/Commands/Vue/index.spec.js | 39 - demos/src/Experiments/Commands/index.spec.ts | 58 ++ .../IsolatingClear/React/index.spec.js | 21 - .../Experiments/IsolatingClear/index.spec.ts | 35 + .../MultipleEditors/Vue/index.spec.js | 7 - .../Experiments/MultipleEditors/index.spec.ts | 21 + .../BackgroundColor/React/index.spec.js | 56 -- .../BackgroundColor/Vue/index.spec.js | 56 -- .../Collaboration/React/index.spec.js | 24 - .../Collaboration/Vue/index.spec.js | 24 - .../CollaborationCaret/React/index.spec.js | 24 - .../CollaborationCaret/Vue/index.spec.js | 24 - .../React/index.spec.js | 36 - .../CollaborationWithMenus/Vue/index.spec.js | 36 - .../src/Extensions/Color/React/index.spec.js | 56 -- demos/src/Extensions/Color/Vue/index.spec.js | 40 - .../Extensions/Dropcursor/React/index.spec.js | 7 - .../Extensions/Dropcursor/Vue/index.spec.js | 7 - .../FloatingMenu/React/index.spec.js | 23 - .../src/Extensions/FloatingMenu/index.spec.ts | 31 + .../src/Extensions/Focus/React/index.spec.js | 9 - demos/src/Extensions/Focus/Vue/index.spec.js | 9 - demos/src/Extensions/Focus/index.spec.ts | 21 + .../Extensions/FontFamily/React/index.spec.js | 52 -- .../Extensions/FontFamily/Vue/index.spec.js | 7 - .../Extensions/FontSize/React/index.spec.js | 25 - .../src/Extensions/FontSize/Vue/index.spec.js | 25 - .../Extensions/Gapcursor/React/index.spec.js | 7 - .../Extensions/Gapcursor/Vue/index.spec.js | 7 - .../InvisibleCharacters/React/index.spec.js | 9 - .../InvisibleCharacters/Vue/index.spec.js | 9 - .../Extensions/LineHeight/React/index.spec.js | 38 - .../Extensions/LineHeight/Vue/index.spec.js | 38 - .../Mathematics/React/index.spec.js | 15 - .../Extensions/Mathematics/Vue/index.spec.js | 15 - .../Extensions/Selection/React/index.spec.js | 9 - .../Extensions/Selection/Vue/index.spec.js | 9 - demos/src/Extensions/Selection/index.spec.ts | 21 + .../TableOfContents/Vue/index.spec.js | 7 - .../Extensions/TextAlign/React/index.spec.js | 127 --- .../Extensions/TextAlign/Vue/index.spec.js | 139 --- demos/src/Extensions/TextAlign/index.spec.ts | 91 ++ .../Extensions/Typography/React/index.spec.js | 100 --- .../Extensions/Typography/Vue/index.spec.js | 91 -- demos/src/Extensions/Typography/index.spec.ts | 71 ++ .../React/index.spec.js | 15 - .../TypographyWithOverrides/Vue/index.spec.js | 15 - .../TypographyWithOverrides/index.spec.ts | 28 + .../Extensions/UndoRedo/React/index.spec.js | 90 -- .../src/Extensions/UndoRedo/Vue/index.spec.js | 90 -- demos/src/Extensions/UndoRedo/index.spec.ts | 82 ++ .../Extensions/UniqueID/React/index.spec.js | 13 - .../src/Extensions/UniqueID/Vue/index.spec.js | 13 - .../UniqueIDWithYdoc/React/index.spec.js | 13 - .../ExportHTML/React/index.spec.js | 19 - .../GuideContent/ExportHTML/Vue/index.spec.js | 19 - .../ExportJSON/React/index.spec.js | 32 - .../GuideContent/ExportJSON/Vue/index.spec.js | 32 - .../GenerateHTML/React/index.spec.js | 11 - .../GenerateHTML/Vue/index.spec.js | 11 - .../GenerateJSON/React/index.spec.js | 36 - .../GenerateJSON/Vue/index.spec.js | 36 - .../GenerateText/React/index.spec.js | 7 - .../GenerateText/Vue/index.spec.js | 7 - .../GuideContent/ReadOnly/React/index.spec.js | 33 - .../GuideContent/ReadOnly/Vue/index.spec.js | 29 - demos/src/GuideContent/ReadOnly/index.spec.ts | 39 + .../StaticRenderHTML/React/index.spec.js | 11 - .../StaticRenderHTML/Vue/index.spec.js | 11 - .../StaticRenderReact/React/index.spec.js | 13 - .../VModel/Vue/index.spec.js | 7 - .../GuideGettingStarted/VModel/index.spec.ts | 24 + .../ReactComponent/React/index.spec.js | 46 - .../ReactComponent/index.spec.ts | 42 + .../VueComponent/Vue/index.spec.js | 41 - .../GuideMarkViews/VueComponent/index.spec.ts | 38 + demos/src/Marks/Bold/React/index.spec.js | 75 -- demos/src/Marks/Bold/Vue/index.spec.js | 75 -- demos/src/Marks/Bold/index.spec.ts | 94 ++ demos/src/Marks/Code/React/index.spec.js | 65 -- demos/src/Marks/Code/Vue/index.spec.js | 54 -- demos/src/Marks/Code/index.spec.ts | 65 ++ demos/src/Marks/Highlight/React/index.spec.js | 116 --- demos/src/Marks/Highlight/Vue/index.spec.js | 116 --- demos/src/Marks/Highlight/index.spec.ts | 117 +++ demos/src/Marks/Italic/React/index.spec.js | 61 -- demos/src/Marks/Italic/Vue/index.spec.js | 61 -- demos/src/Marks/Italic/index.spec.ts | 83 ++ demos/src/Marks/Link/React/index.spec.js | 165 ---- demos/src/Marks/Link/Vue/index.spec.js | 132 --- demos/src/Marks/Link/index.spec.ts | 143 +++ demos/src/Marks/Strike/React/index.spec.js | 75 -- demos/src/Marks/Strike/Vue/index.spec.js | 75 -- demos/src/Marks/Strike/index.spec.ts | 71 ++ demos/src/Marks/Subscript/React/index.spec.js | 39 - demos/src/Marks/Subscript/Vue/index.spec.js | 39 - .../src/Marks/Superscript/React/index.spec.js | 39 - demos/src/Marks/Superscript/Vue/index.spec.js | 39 - demos/src/Marks/TextStyle/React/index.spec.js | 82 -- demos/src/Marks/TextStyle/Vue/index.spec.js | 82 -- demos/src/Marks/Underline/React/index.spec.js | 54 -- demos/src/Marks/Underline/Vue/index.spec.js | 54 -- demos/src/Marks/Underline/index.spec.ts | 66 ++ .../src/Nodes/Blockquote/React/index.spec.js | 83 -- demos/src/Nodes/Blockquote/Vue/index.spec.js | 83 -- demos/src/Nodes/Blockquote/index.spec.ts | 85 ++ .../src/Nodes/BulletList/React/index.spec.js | 115 --- demos/src/Nodes/BulletList/Vue/index.spec.js | 115 --- demos/src/Nodes/BulletList/index.spec.ts | 92 ++ demos/src/Nodes/CodeBlock/React/index.spec.js | 156 ---- demos/src/Nodes/CodeBlock/Vue/index.spec.js | 156 ---- demos/src/Nodes/CodeBlock/index.spec.ts | 108 +++ demos/src/Nodes/Details/Vue/index.spec.js | 45 - demos/src/Nodes/Document/React/index.spec.js | 26 - demos/src/Nodes/Document/Vue/index.spec.js | 26 - demos/src/Nodes/Emoji/React/index.spec.js | 36 - demos/src/Nodes/Emoji/index.spec.ts | 32 + demos/src/Nodes/HardBreak/React/index.spec.js | 49 - demos/src/Nodes/HardBreak/Vue/index.spec.js | 49 - demos/src/Nodes/HardBreak/index.spec.ts | 64 ++ demos/src/Nodes/Heading/React/index.spec.js | 111 --- demos/src/Nodes/Heading/Vue/index.spec.js | 111 --- demos/src/Nodes/Heading/index.spec.ts | 81 ++ .../Nodes/HorizontalRule/React/index.spec.js | 77 -- .../Nodes/HorizontalRule/Vue/index.spec.js | 77 -- demos/src/Nodes/HorizontalRule/index.spec.ts | 79 ++ demos/src/Nodes/Image/React/index.spec.js | 24 - demos/src/Nodes/Image/Vue/index.spec.js | 24 - demos/src/Nodes/ListItem/React/index.spec.js | 38 - demos/src/Nodes/ListItem/Vue/index.spec.js | 38 - demos/src/Nodes/ListItem/index.spec.ts | 45 + demos/src/Nodes/Mention/React/index.spec.js | 112 --- demos/src/Nodes/Mention/Vue/index.spec.js | 7 - demos/src/Nodes/Mention/index.spec.ts | 109 +++ .../src/Nodes/OrderedList/React/index.spec.js | 102 --- demos/src/Nodes/OrderedList/Vue/index.spec.js | 102 --- demos/src/Nodes/OrderedList/index.spec.ts | 87 ++ demos/src/Nodes/Paragraph/React/index.spec.js | 42 - demos/src/Nodes/Paragraph/Vue/index.spec.js | 42 - demos/src/Nodes/Paragraph/index.spec.ts | 59 ++ demos/src/Nodes/Table/React/index.spec.js | 90 -- demos/src/Nodes/Table/Vue/index.spec.js | 90 -- demos/src/Nodes/TaskItem/React/index.spec.js | 7 - demos/src/Nodes/TaskItem/Vue/index.spec.js | 7 - demos/src/Nodes/TaskList/React/index.spec.js | 113 --- demos/src/Nodes/TaskList/Vue/index.spec.js | 113 --- demos/src/Nodes/TaskList/index.spec.ts | 84 ++ demos/src/Nodes/Text/React/index.spec.js | 15 - demos/src/Nodes/Text/Vue/index.spec.js | 15 - demos/src/Nodes/Text/index.spec.ts | 27 + demos/src/Nodes/Youtube/React/index.spec.js | 86 -- demos/src/Nodes/Youtube/Vue/index.spec.js | 86 -- demos/test/helpers.ts | 33 + package.json | 24 +- packages/core/__tests__/cut.spec.ts | 37 + packages/core/__tests__/getContent.spec.ts | 37 + packages/core/__tests__/insertContent.spec.ts | 168 +++- packages/core/__tests__/pluginOrder.spec.ts | 60 ++ packages/core/__tests__/setContent.spec.ts | 167 ++++ .../__tests__/transformPastedHTML.spec.ts | 249 ++++++ .../__tests__/details-commands.spec.ts | 48 + .../__tests__/insert-emoji.spec.ts | 34 + .../__tests__/invisible-characters.spec.ts | 32 + .../__tests__/subscript.spec.ts | 47 + .../__tests__/superscript.spec.ts | 47 + .../__tests__/tableCommands.spec.ts | 191 ++++ .../background-color-commands.spec.ts | 46 + .../__tests__/color-commands.spec.ts | 45 + .../__tests__/font-family-commands.spec.ts | 57 ++ .../__tests__/font-size-commands.spec.ts | 40 + .../__tests__/line-height-commands.spec.ts | 48 + .../__tests__/text-style-merge.spec.ts | 124 +++ .../__tests__/unique-id.spec.ts | 44 + playwright.config.ts | 26 + pnpm-lock.yaml | 835 ++---------------- tests/cypress.config.js | 13 - tests/cypress/fixtures/example.json | 5 - .../integration/core/pluginOrder.spec.ts | 70 -- .../core/transformPastedHTML.spec.ts | 336 ------- tests/cypress/plugins/index.js | 76 -- tests/cypress/support/commands.js | 101 --- tests/cypress/support/e2e.js | 20 - tests/cypress/tsconfig.json | 26 - tests/package.json | 3 - 279 files changed, 5763 insertions(+), 10510 deletions(-) create mode 100644 .lintstagedrc.cjs delete mode 100644 demos/src/Commands/Cut/React/index.spec.js delete mode 100644 demos/src/Commands/InsertContent/React/index.spec.js delete mode 100644 demos/src/Commands/InsertContentApplyingRules/React/index.spec.js delete mode 100644 demos/src/Commands/SetContent/React/index.spec.js delete mode 100644 demos/src/Demos/CollaborationSplitPane/React/index.spec.js create mode 100644 demos/src/Demos/CollaborationSplitPane/index.spec.ts delete mode 100644 demos/src/Demos/SingleRoomCollab/React/index.spec.js create mode 100644 demos/src/Demos/SingleRoomCollab/index.spec.ts delete mode 100644 demos/src/Examples/Accessibility/React/index.spec.js delete mode 100644 demos/src/Examples/AutolinkValidation/React/index.spec.js delete mode 100644 demos/src/Examples/AutolinkValidation/Vue/index.spec.js create mode 100644 demos/src/Examples/AutolinkValidation/index.spec.ts delete mode 100644 demos/src/Examples/Book/React/index.spec.js delete mode 100644 demos/src/Examples/Book/Vue/index.spec.js delete mode 100644 demos/src/Examples/CSSModules/React/index.spec.js delete mode 100644 demos/src/Examples/CSSModules/Vue/index.spec.js create mode 100644 demos/src/Examples/CSSModules/index.spec.ts delete mode 100644 demos/src/Examples/CodeBlockLanguage/React/index.spec.js delete mode 100644 demos/src/Examples/CodeBlockLanguage/Vue/index.spec.js delete mode 100644 demos/src/Examples/CollaborativeEditing/React/index.spec.js delete mode 100644 demos/src/Examples/CollaborativeEditing/Vue/index.spec.js delete mode 100644 demos/src/Examples/Community/React/index.spec.js delete mode 100644 demos/src/Examples/Community/Vue/index.spec.js create mode 100644 demos/src/Examples/Community/index.spec.ts delete mode 100644 demos/src/Examples/CustomDocument/React/index.spec.js delete mode 100644 demos/src/Examples/CustomDocument/Vue/index.spec.js create mode 100644 demos/src/Examples/CustomDocument/index.spec.ts delete mode 100644 demos/src/Examples/CustomParagraph/React/index.spec.js delete mode 100644 demos/src/Examples/CustomParagraph/Vue/index.spec.js create mode 100644 demos/src/Examples/CustomParagraph/index.spec.ts delete mode 100644 demos/src/Examples/Default/React/index.spec.js delete mode 100644 demos/src/Examples/Default/Svelte/index.spec.js delete mode 100644 demos/src/Examples/Default/Vue/index.spec.js create mode 100644 demos/src/Examples/Default/index.spec.ts delete mode 100644 demos/src/Examples/Drawing/Vue/index.spec.js create mode 100644 demos/src/Examples/Drawing/index.spec.ts delete mode 100644 demos/src/Examples/EnterShortcuts/React/index.spec.js delete mode 100644 demos/src/Examples/EnterShortcuts/Vue/index.spec.js create mode 100644 demos/src/Examples/EnterShortcuts/index.spec.ts delete mode 100644 demos/src/Examples/Formatting/React/index.spec.js delete mode 100644 demos/src/Examples/Formatting/Vue/index.spec.js create mode 100644 demos/src/Examples/Formatting/index.spec.ts delete mode 100644 demos/src/Examples/Images/React/index.spec.js delete mode 100644 demos/src/Examples/Images/Vue/index.spec.js create mode 100644 demos/src/Examples/Images/index.spec.ts delete mode 100644 demos/src/Examples/InteractivityComponent/React/index.spec.js delete mode 100644 demos/src/Examples/InteractivityComponent/Vue/index.spec.js create mode 100644 demos/src/Examples/InteractivityComponent/index.spec.ts delete mode 100644 demos/src/Examples/InteractivityComponentContent/React/index.spec.js delete mode 100644 demos/src/Examples/InteractivityComponentContent/Vue/index.spec.js create mode 100644 demos/src/Examples/InteractivityComponentContent/index.spec.ts delete mode 100644 demos/src/Examples/InteractivityComponentProvideInject/Vue/index.spec.js create mode 100644 demos/src/Examples/InteractivityComponentProvideInject/index.spec.ts delete mode 100644 demos/src/Examples/JSX/React/index.spec.js delete mode 100644 demos/src/Examples/JSX/Vue/index.spec.js create mode 100644 demos/src/Examples/JSX/index.spec.ts delete mode 100644 demos/src/Examples/MarkdownShortcuts/React/index.spec.js delete mode 100644 demos/src/Examples/MarkdownShortcuts/Vue/index.spec.js create mode 100644 demos/src/Examples/MarkdownShortcuts/index.spec.ts delete mode 100644 demos/src/Examples/Menus/React/index.spec.js delete mode 100644 demos/src/Examples/Menus/Vue/index.spec.js create mode 100644 demos/src/Examples/Menus/index.spec.ts delete mode 100644 demos/src/Examples/Minimal/React/index.spec.js delete mode 100644 demos/src/Examples/Minimal/Vue/index.spec.js create mode 100644 demos/src/Examples/Minimal/index.spec.ts delete mode 100644 demos/src/Examples/MultiMention/React/index.spec.js delete mode 100644 demos/src/Examples/MultiMention/Vue/index.spec.js create mode 100644 demos/src/Examples/MultiMention/index.spec.ts delete mode 100644 demos/src/Examples/NodePos/React/index.spec.js delete mode 100644 demos/src/Examples/Performance/React/index.spec.js delete mode 100644 demos/src/Examples/ResizableImages/React/index.spec.js delete mode 100644 demos/src/Examples/ResizableImages/Vue/index.spec.js delete mode 100644 demos/src/Examples/Savvy/React/index.spec.js delete mode 100644 demos/src/Examples/Savvy/Vue/index.spec.js create mode 100644 demos/src/Examples/Savvy/index.spec.ts delete mode 100644 demos/src/Examples/StaticRendering/React/index.spec.js delete mode 100644 demos/src/Examples/StaticRenderingAdvanced/React/index.spec.js delete mode 100644 demos/src/Examples/Tables/React/index.spec.js delete mode 100644 demos/src/Examples/Tables/Vue/index.spec.js delete mode 100644 demos/src/Examples/Tasks/React/index.spec.js delete mode 100644 demos/src/Examples/Tasks/Vue/index.spec.js create mode 100644 demos/src/Examples/Tasks/index.spec.ts delete mode 100644 demos/src/Examples/TextDirection/React/index.spec.js delete mode 100644 demos/src/Examples/Transition/Vue/index.spec.js delete mode 100644 demos/src/Examples/TypographyRTL/React/index.spec.js create mode 100644 demos/src/Examples/TypographyRTL/index.spec.ts delete mode 100644 demos/src/Experiments/CollaborationAnnotation/Vue/index.spec.js create mode 100644 demos/src/Experiments/CollaborationAnnotation/index.spec.ts delete mode 100644 demos/src/Experiments/Commands/Vue/index.spec.js create mode 100644 demos/src/Experiments/Commands/index.spec.ts delete mode 100644 demos/src/Experiments/IsolatingClear/React/index.spec.js create mode 100644 demos/src/Experiments/IsolatingClear/index.spec.ts delete mode 100644 demos/src/Experiments/MultipleEditors/Vue/index.spec.js create mode 100644 demos/src/Experiments/MultipleEditors/index.spec.ts delete mode 100644 demos/src/Extensions/BackgroundColor/React/index.spec.js delete mode 100644 demos/src/Extensions/BackgroundColor/Vue/index.spec.js delete mode 100644 demos/src/Extensions/Collaboration/React/index.spec.js delete mode 100644 demos/src/Extensions/Collaboration/Vue/index.spec.js delete mode 100644 demos/src/Extensions/CollaborationCaret/React/index.spec.js delete mode 100644 demos/src/Extensions/CollaborationCaret/Vue/index.spec.js delete mode 100644 demos/src/Extensions/CollaborationWithMenus/React/index.spec.js delete mode 100644 demos/src/Extensions/CollaborationWithMenus/Vue/index.spec.js delete mode 100644 demos/src/Extensions/Color/React/index.spec.js delete mode 100644 demos/src/Extensions/Color/Vue/index.spec.js delete mode 100644 demos/src/Extensions/Dropcursor/React/index.spec.js delete mode 100644 demos/src/Extensions/Dropcursor/Vue/index.spec.js delete mode 100644 demos/src/Extensions/FloatingMenu/React/index.spec.js create mode 100644 demos/src/Extensions/FloatingMenu/index.spec.ts delete mode 100644 demos/src/Extensions/Focus/React/index.spec.js delete mode 100644 demos/src/Extensions/Focus/Vue/index.spec.js create mode 100644 demos/src/Extensions/Focus/index.spec.ts delete mode 100644 demos/src/Extensions/FontFamily/React/index.spec.js delete mode 100644 demos/src/Extensions/FontFamily/Vue/index.spec.js delete mode 100644 demos/src/Extensions/FontSize/React/index.spec.js delete mode 100644 demos/src/Extensions/FontSize/Vue/index.spec.js delete mode 100644 demos/src/Extensions/Gapcursor/React/index.spec.js delete mode 100644 demos/src/Extensions/Gapcursor/Vue/index.spec.js delete mode 100644 demos/src/Extensions/InvisibleCharacters/React/index.spec.js delete mode 100644 demos/src/Extensions/InvisibleCharacters/Vue/index.spec.js delete mode 100644 demos/src/Extensions/LineHeight/React/index.spec.js delete mode 100644 demos/src/Extensions/LineHeight/Vue/index.spec.js delete mode 100644 demos/src/Extensions/Mathematics/React/index.spec.js delete mode 100644 demos/src/Extensions/Mathematics/Vue/index.spec.js delete mode 100644 demos/src/Extensions/Selection/React/index.spec.js delete mode 100644 demos/src/Extensions/Selection/Vue/index.spec.js create mode 100644 demos/src/Extensions/Selection/index.spec.ts delete mode 100644 demos/src/Extensions/TableOfContents/Vue/index.spec.js delete mode 100644 demos/src/Extensions/TextAlign/React/index.spec.js delete mode 100644 demos/src/Extensions/TextAlign/Vue/index.spec.js create mode 100644 demos/src/Extensions/TextAlign/index.spec.ts delete mode 100644 demos/src/Extensions/Typography/React/index.spec.js delete mode 100644 demos/src/Extensions/Typography/Vue/index.spec.js create mode 100644 demos/src/Extensions/Typography/index.spec.ts delete mode 100644 demos/src/Extensions/TypographyWithOverrides/React/index.spec.js delete mode 100644 demos/src/Extensions/TypographyWithOverrides/Vue/index.spec.js create mode 100644 demos/src/Extensions/TypographyWithOverrides/index.spec.ts delete mode 100644 demos/src/Extensions/UndoRedo/React/index.spec.js delete mode 100644 demos/src/Extensions/UndoRedo/Vue/index.spec.js create mode 100644 demos/src/Extensions/UndoRedo/index.spec.ts delete mode 100644 demos/src/Extensions/UniqueID/React/index.spec.js delete mode 100644 demos/src/Extensions/UniqueID/Vue/index.spec.js delete mode 100644 demos/src/Extensions/UniqueIDWithYdoc/React/index.spec.js delete mode 100644 demos/src/GuideContent/ExportHTML/React/index.spec.js delete mode 100644 demos/src/GuideContent/ExportHTML/Vue/index.spec.js delete mode 100644 demos/src/GuideContent/ExportJSON/React/index.spec.js delete mode 100644 demos/src/GuideContent/ExportJSON/Vue/index.spec.js delete mode 100644 demos/src/GuideContent/GenerateHTML/React/index.spec.js delete mode 100644 demos/src/GuideContent/GenerateHTML/Vue/index.spec.js delete mode 100644 demos/src/GuideContent/GenerateJSON/React/index.spec.js delete mode 100644 demos/src/GuideContent/GenerateJSON/Vue/index.spec.js delete mode 100644 demos/src/GuideContent/GenerateText/React/index.spec.js delete mode 100644 demos/src/GuideContent/GenerateText/Vue/index.spec.js delete mode 100644 demos/src/GuideContent/ReadOnly/React/index.spec.js delete mode 100644 demos/src/GuideContent/ReadOnly/Vue/index.spec.js create mode 100644 demos/src/GuideContent/ReadOnly/index.spec.ts delete mode 100644 demos/src/GuideContent/StaticRenderHTML/React/index.spec.js delete mode 100644 demos/src/GuideContent/StaticRenderHTML/Vue/index.spec.js delete mode 100644 demos/src/GuideContent/StaticRenderReact/React/index.spec.js delete mode 100644 demos/src/GuideGettingStarted/VModel/Vue/index.spec.js create mode 100644 demos/src/GuideGettingStarted/VModel/index.spec.ts delete mode 100644 demos/src/GuideMarkViews/ReactComponent/React/index.spec.js create mode 100644 demos/src/GuideMarkViews/ReactComponent/index.spec.ts delete mode 100644 demos/src/GuideMarkViews/VueComponent/Vue/index.spec.js create mode 100644 demos/src/GuideMarkViews/VueComponent/index.spec.ts delete mode 100644 demos/src/Marks/Bold/React/index.spec.js delete mode 100644 demos/src/Marks/Bold/Vue/index.spec.js create mode 100644 demos/src/Marks/Bold/index.spec.ts delete mode 100644 demos/src/Marks/Code/React/index.spec.js delete mode 100644 demos/src/Marks/Code/Vue/index.spec.js create mode 100644 demos/src/Marks/Code/index.spec.ts delete mode 100644 demos/src/Marks/Highlight/React/index.spec.js delete mode 100644 demos/src/Marks/Highlight/Vue/index.spec.js create mode 100644 demos/src/Marks/Highlight/index.spec.ts delete mode 100644 demos/src/Marks/Italic/React/index.spec.js delete mode 100644 demos/src/Marks/Italic/Vue/index.spec.js create mode 100644 demos/src/Marks/Italic/index.spec.ts delete mode 100644 demos/src/Marks/Link/React/index.spec.js delete mode 100644 demos/src/Marks/Link/Vue/index.spec.js create mode 100644 demos/src/Marks/Link/index.spec.ts delete mode 100644 demos/src/Marks/Strike/React/index.spec.js delete mode 100644 demos/src/Marks/Strike/Vue/index.spec.js create mode 100644 demos/src/Marks/Strike/index.spec.ts delete mode 100644 demos/src/Marks/Subscript/React/index.spec.js delete mode 100644 demos/src/Marks/Subscript/Vue/index.spec.js delete mode 100644 demos/src/Marks/Superscript/React/index.spec.js delete mode 100644 demos/src/Marks/Superscript/Vue/index.spec.js delete mode 100644 demos/src/Marks/TextStyle/React/index.spec.js delete mode 100644 demos/src/Marks/TextStyle/Vue/index.spec.js delete mode 100644 demos/src/Marks/Underline/React/index.spec.js delete mode 100644 demos/src/Marks/Underline/Vue/index.spec.js create mode 100644 demos/src/Marks/Underline/index.spec.ts delete mode 100644 demos/src/Nodes/Blockquote/React/index.spec.js delete mode 100644 demos/src/Nodes/Blockquote/Vue/index.spec.js create mode 100644 demos/src/Nodes/Blockquote/index.spec.ts delete mode 100644 demos/src/Nodes/BulletList/React/index.spec.js delete mode 100644 demos/src/Nodes/BulletList/Vue/index.spec.js create mode 100644 demos/src/Nodes/BulletList/index.spec.ts delete mode 100644 demos/src/Nodes/CodeBlock/React/index.spec.js delete mode 100644 demos/src/Nodes/CodeBlock/Vue/index.spec.js create mode 100644 demos/src/Nodes/CodeBlock/index.spec.ts delete mode 100644 demos/src/Nodes/Details/Vue/index.spec.js delete mode 100644 demos/src/Nodes/Document/React/index.spec.js delete mode 100644 demos/src/Nodes/Document/Vue/index.spec.js delete mode 100644 demos/src/Nodes/Emoji/React/index.spec.js create mode 100644 demos/src/Nodes/Emoji/index.spec.ts delete mode 100644 demos/src/Nodes/HardBreak/React/index.spec.js delete mode 100644 demos/src/Nodes/HardBreak/Vue/index.spec.js create mode 100644 demos/src/Nodes/HardBreak/index.spec.ts delete mode 100644 demos/src/Nodes/Heading/React/index.spec.js delete mode 100644 demos/src/Nodes/Heading/Vue/index.spec.js create mode 100644 demos/src/Nodes/Heading/index.spec.ts delete mode 100644 demos/src/Nodes/HorizontalRule/React/index.spec.js delete mode 100644 demos/src/Nodes/HorizontalRule/Vue/index.spec.js create mode 100644 demos/src/Nodes/HorizontalRule/index.spec.ts delete mode 100644 demos/src/Nodes/Image/React/index.spec.js delete mode 100644 demos/src/Nodes/Image/Vue/index.spec.js delete mode 100644 demos/src/Nodes/ListItem/React/index.spec.js delete mode 100644 demos/src/Nodes/ListItem/Vue/index.spec.js create mode 100644 demos/src/Nodes/ListItem/index.spec.ts delete mode 100644 demos/src/Nodes/Mention/React/index.spec.js delete mode 100644 demos/src/Nodes/Mention/Vue/index.spec.js create mode 100644 demos/src/Nodes/Mention/index.spec.ts delete mode 100644 demos/src/Nodes/OrderedList/React/index.spec.js delete mode 100644 demos/src/Nodes/OrderedList/Vue/index.spec.js create mode 100644 demos/src/Nodes/OrderedList/index.spec.ts delete mode 100644 demos/src/Nodes/Paragraph/React/index.spec.js delete mode 100644 demos/src/Nodes/Paragraph/Vue/index.spec.js create mode 100644 demos/src/Nodes/Paragraph/index.spec.ts delete mode 100644 demos/src/Nodes/Table/React/index.spec.js delete mode 100644 demos/src/Nodes/Table/Vue/index.spec.js delete mode 100644 demos/src/Nodes/TaskItem/React/index.spec.js delete mode 100644 demos/src/Nodes/TaskItem/Vue/index.spec.js delete mode 100644 demos/src/Nodes/TaskList/React/index.spec.js delete mode 100644 demos/src/Nodes/TaskList/Vue/index.spec.js create mode 100644 demos/src/Nodes/TaskList/index.spec.ts delete mode 100644 demos/src/Nodes/Text/React/index.spec.js delete mode 100644 demos/src/Nodes/Text/Vue/index.spec.js create mode 100644 demos/src/Nodes/Text/index.spec.ts delete mode 100644 demos/src/Nodes/Youtube/React/index.spec.js delete mode 100644 demos/src/Nodes/Youtube/Vue/index.spec.js create mode 100644 demos/test/helpers.ts create mode 100644 packages/core/__tests__/cut.spec.ts create mode 100644 packages/core/__tests__/getContent.spec.ts create mode 100644 packages/core/__tests__/pluginOrder.spec.ts create mode 100644 packages/core/__tests__/setContent.spec.ts create mode 100644 packages/core/__tests__/transformPastedHTML.spec.ts create mode 100644 packages/extension-details/__tests__/details-commands.spec.ts create mode 100644 packages/extension-emoji/__tests__/insert-emoji.spec.ts create mode 100644 packages/extension-invisible-characters/__tests__/invisible-characters.spec.ts create mode 100644 packages/extension-subscript/__tests__/subscript.spec.ts create mode 100644 packages/extension-superscript/__tests__/superscript.spec.ts create mode 100644 packages/extension-table/__tests__/tableCommands.spec.ts create mode 100644 packages/extension-text-style/__tests__/background-color-commands.spec.ts create mode 100644 packages/extension-text-style/__tests__/color-commands.spec.ts create mode 100644 packages/extension-text-style/__tests__/font-family-commands.spec.ts create mode 100644 packages/extension-text-style/__tests__/font-size-commands.spec.ts create mode 100644 packages/extension-text-style/__tests__/line-height-commands.spec.ts create mode 100644 packages/extension-text-style/__tests__/text-style-merge.spec.ts create mode 100644 packages/extension-unique-id/__tests__/unique-id.spec.ts create mode 100644 playwright.config.ts delete mode 100644 tests/cypress.config.js delete mode 100644 tests/cypress/fixtures/example.json delete mode 100644 tests/cypress/integration/core/pluginOrder.spec.ts delete mode 100644 tests/cypress/integration/core/transformPastedHTML.spec.ts delete mode 100644 tests/cypress/plugins/index.js delete mode 100644 tests/cypress/support/commands.js delete mode 100644 tests/cypress/support/e2e.js delete mode 100644 tests/cypress/tsconfig.json delete mode 100644 tests/package.json diff --git a/.eslintrc.js b/.eslintrc.js index 997ff95870..6bc5544c25 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,10 +14,7 @@ module.exports = { }, { files: ['./**/*.ts', './**/*.tsx', './**/*.js', './**/*.jsx', './**/*.vue'], - plugins: ['html', 'cypress', '@typescript-eslint', 'simple-import-sort'], - env: { - 'cypress/globals': true, - }, + plugins: ['html', '@typescript-eslint', 'simple-import-sort'], globals: { document: false, window: false, diff --git a/.github/instructions/tiptap.instructions.md b/.github/instructions/tiptap.instructions.md index 6bef245e3d..62fcdb7c36 100644 --- a/.github/instructions/tiptap.instructions.md +++ b/.github/instructions/tiptap.instructions.md @@ -29,10 +29,11 @@ Key points for AI assistants: │ ├─ extension-*/ # Individual extensions │ ├─ pm/ # ProseMirror related internals and helpers │ └─ ... # Shared utilities, framework bindings, etc. -├─ demos/ # Vite app for live examples -│ ├─ react/ # React demos -│ └─ vue/ # Vue demos -├─ tests/ # Cypress e2e tests that run against the demos +├─ demos/ # Vite app for live examples and colocated e2e specs +│ ├─ src/ +│ │ ├─ react/ # React demos +│ │ └─ vue/ # Vue demos +│ └─ test/ # Playwright helpers (getEditor, setEditorContent, ...) ├─ .changeset/ # Changesets for versioning and changelogs └─ .github/ # Workflows and docs like this file ``` @@ -41,7 +42,7 @@ Notes: * All packages we publish or use live under `packages/*`. * The `demos/` folder contains a Vite app. It automatically discovers and parses React and Vue demos so they appear in the UI without manual wiring. -* Cypress tests in `tests/` expect the demos to be available on `http://localhost:3000`. +* Playwright e2e specs live alongside their demos as `demos/src/**/index.spec.ts`. `playwright.config.ts` auto-starts the Vite dev server on `http://127.0.0.1:4080` — no need to launch it manually. ## NPM scripts @@ -51,8 +52,14 @@ Scripts defined at the repo root: * `pnpm build` - build all packages via Turborepo * `pnpm lint` - run eslint checks * `pnpm lint:fix` - run prettier + eslint fix -* `pnpm test:open` - open Cypress against `tests/` -* `pnpm test:run` - run Cypress in headless mode +* `pnpm test:e2e` - run Playwright e2e tests headlessly in Chromium +* `pnpm test:e2e:firefox` - same, in Firefox +* `pnpm test:e2e:all` - same, in both browsers +* `pnpm test:e2e:open` - run Playwright in UI mode (Chromium tests) +* `pnpm test:e2e:open:firefox` - UI mode, Firefox tests +* `pnpm test:e2e:open:all` - UI mode, both browsers selectable +* `pnpm test:e2e:report` - open the HTML report from the last run +* `pnpm test:unit` - run Vitest unit tests in `packages/**/__tests__/` * `pnpm test` - build then run all tests * `pnpm serve` - build and serve the demos on port 3000 * `pnpm publish` - build and publish with Changesets @@ -94,23 +101,33 @@ When adding a demo, keep it small and self-contained, with imports from publishe --- -## Testing with Cypress +## Testing -* Cypress lives in `tests/` and drives the demos in a browser. -* Tests assume the app is running on `http://localhost:3000`. +Two layers: -Workflow: +* **Unit tests** with Vitest in `packages/**/__tests__/` (happy-dom). These test `@tiptap/core` and individual extensions in isolation. +* **E2E tests** with Playwright, colocated next to their demos as `demos/src/**/index.spec.ts`. They drive the real Vite-served demo pages in Chromium. + +Run them: ```bash -pnpm dev # terminal A -pnpm test:open # terminal B +pnpm test:unit # Vitest +pnpm test:e2e # Playwright headless (Chromium) +pnpm test:e2e:firefox # Playwright headless (Firefox) +pnpm test:e2e:all # both browsers — every test twice +pnpm test:e2e:open # UI mode (Chromium tests) +pnpm test:e2e:open:firefox # UI mode (Firefox tests) +pnpm test:e2e:open:all # UI mode, switch between browsers in the project picker +pnpm test:e2e:report # open the HTML report from the last run ``` -or for headless CI runs: +Playwright auto-starts the demo dev server (`pnpm -C demos run start:e2e` on port 4080) via `playwright.config.ts` — no separate terminal needed. Shared helpers live in `demos/test/helpers.ts`: `getEditor`, `setEditorContent`, `clickButton`. Use `demos/src/Commands/Cut/index.spec.ts` as a canonical template when adding new specs. -```bash -pnpm test:run -``` +Browser setup: + +* CI installs Chromium only (cached between runs) and only runs the Chromium project. +* For local Firefox testing, install it once with `pnpm exec playwright install firefox` (~80MB). +* UI mode (`--ui`) always opens its host window in Chromium — that's the Playwright UI app itself, not the browser running your tests. Tests still execute in the project you selected (check the trace metadata or `browserName` fixture if you need to confirm). --- @@ -198,7 +215,8 @@ Run the following to validate changes quickly: ```bash pnpm lint pnpm build -pnpm test # runs unit and/or cypress where configured +pnpm test:unit # Vitest +pnpm test:e2e # Playwright (auto-starts the demo server) pnpm dev # optionally run the demos and open http://localhost:3000 ``` @@ -222,7 +240,7 @@ If a single package is failing types, run a targeted build for that package (e.g ### Troubleshooting notes - If CI fails with dependency or lockfile errors, run `pnpm reset` locally and re-run the build. -- For flaky Cypress tests, run the demo locally with `pnpm dev` and reproduce the failing test in `pnpm test:open`. +- For flaky Playwright tests, reproduce locally with `pnpm test:e2e:open` (UI mode) or rerun with `--trace on` and inspect via `pnpm test:e2e:report`. --- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8edd02ec0..03ffd4ecda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,13 +46,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --strict-peer-dependencies - - name: Install Cypress binary - run: pnpm exec cypress install - - name: Pack dependency artifacts run: | tar -czf /tmp/pnpm-store.tar.gz ${{ env.PNPM_STORE_DIR }} - tar -czf /tmp/cypress-cache.tar.gz -C "$HOME/.cache" Cypress - name: Upload dependency artifacts uses: actions/upload-artifact@v7 @@ -60,7 +56,6 @@ jobs: name: node-dependencies path: | /tmp/pnpm-store.tar.gz - /tmp/cypress-cache.tar.gz retention-days: 1 check-linting-formatting: @@ -194,24 +189,15 @@ jobs: run: pnpm run test:unit run-e2e-tests: - name: Run e2e tests + name: Run e2e tests (shard ${{ matrix.shard }}/${{ matrix.total }}) runs-on: ubuntu-latest needs: build-packages - timeout-minutes: 45 - + timeout-minutes: 30 strategy: fail-fast: false matrix: - test-spec: - - { id: integration, name: Integration, spec: './tests/cypress/integration/**/*.spec.{js,ts}' } - - { id: demos-commands, name: 'Demos/Commands', spec: './demos/src/Commands/**/*.spec.{js,ts}' } - - { id: demos-examples, name: 'Demos/Examples', spec: './demos/src/Examples/**/*.spec.{js,ts}' } - - { id: demos-experiments, name: 'Demos/Experiments', spec: './demos/src/Experiments/**/*.spec.{js,ts}' } - - { id: demos-extensions, name: 'Demos/Extensions', spec: './demos/src/Extensions/**/*.spec.{js,ts}' } - - { id: demos-guidecontent, name: 'Demos/GuideContent', spec: './demos/src/GuideContent/**/*.spec.{js,ts}' } - - { id: demos-guidegettingstarted, name: 'Demos/GuideGettingStarted', spec: './demos/src/GuideGettingStarted/**/*.spec.{js,ts}' } - - { id: demos-marks, name: 'Demos/Marks', spec: './demos/src/Marks/**/*.spec.{js,ts}' } - - { id: demos-nodes, name: 'Demos/Nodes', spec: './demos/src/Nodes/**/*.spec.{js,ts}' } + shard: [1, 2, 3, 4] + total: [4] steps: - uses: actions/checkout@v6 @@ -241,9 +227,6 @@ jobs: - name: Restore dependencies run: pnpm install --offline --frozen-lockfile --strict-peer-dependencies - - name: Restore Cypress cache - run: mkdir -p "$HOME/.cache" && tar -xzf /tmp/node-dependencies/cypress-cache.tar.gz -C "$HOME/.cache" - - name: Download build artifacts uses: actions/download-artifact@v8 with: @@ -253,29 +236,84 @@ jobs: - name: Restore build output run: tar -xzf /tmp/build-output/build-output.tar.gz - - name: Test ${{ matrix.test-spec.name }} - uses: cypress-io/github-action@v7.3.0 + - name: Get Playwright version + id: playwright-version + run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@v4 with: - install: false - start: pnpm exec http-server ./demos/dist -s -p 3000 - wait-on: http://localhost:3000 - spec: ${{ matrix.test-spec.spec }} - project: ./tests - browser: chrome - quiet: true - - - name: Export screenshots (on failure only) + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: pnpm exec playwright install --with-deps chromium + + - name: Install Playwright system deps + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: pnpm exec playwright install-deps chromium + + - name: Run Playwright tests + run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shard }}/${{ matrix.total }} + + - name: Upload blob report + if: always() uses: actions/upload-artifact@v7 - if: failure() with: - name: cypress-screenshots-${{ matrix.test-spec.id }} - path: tests/cypress/screenshots + name: e2e-blob-report-${{ matrix.shard }} + path: blob-report retention-days: 7 - - name: Export screen recordings (on failure only) + merge-e2e-reports: + name: Merge e2e reports + runs-on: ubuntu-latest + needs: run-e2e-tests + if: always() + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v6 + + - name: Install pnpm + uses: pnpm/action-setup@v5 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Configure pnpm store + run: pnpm config set store-dir ${{ github.workspace }}/${{ env.PNPM_STORE_DIR }} + + - name: Download dependency artifacts + uses: actions/download-artifact@v8 + with: + name: node-dependencies + path: /tmp/node-dependencies + + - name: Restore pnpm store + run: tar -xzf /tmp/node-dependencies/pnpm-store.tar.gz + + - name: Restore dependencies + run: pnpm install --offline --frozen-lockfile --strict-peer-dependencies + + - name: Download blob reports + uses: actions/download-artifact@v8 + with: + path: all-blob-reports + pattern: e2e-blob-report-* + merge-multiple: true + + - name: Merge into HTML report + run: pnpm exec playwright merge-reports --reporter html ./all-blob-reports + + - name: Upload merged HTML report uses: actions/upload-artifact@v7 - if: failure() with: - name: cypress-videos-${{ matrix.test-spec.id }} - path: tests/cypress/videos - retention-days: 7 + name: playwright-html-report + path: playwright-report + retention-days: 14 diff --git a/.gitignore b/.gitignore index ed4df01ce8..450912d5e9 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,11 @@ yarn-error.log* .rts2_cache_es .rts2_cache_umd -tests/cypress/videos -/tests/cypress/screenshots +# Playwright +/playwright-report +/test-results +/playwright/.cache +/blob-report # Ignore intellij project files .idea diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 0000000000..887be6f676 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,14 @@ +module.exports = files => { + const filteredFiles = files.filter(file => /\.(ts|tsx|js|jsx|vue)$/.test(file) && !file.includes('/tests_backup/')) + + if (filteredFiles.length === 0) { + return [] + } + + const fileList = filteredFiles.join(' ') + + return [ + `prettier --write ${fileList}`, + `eslint --fix --quiet --no-error-on-unmatched-pattern ${fileList}`, + ] +} diff --git a/AGENTS.md b/AGENTS.md index 497e05579b..d59fdf1dd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,11 +25,11 @@ Key points for AI assistants: │ ├─ extension-*/ # Individual extensions │ ├─ pm/ # ProseMirror related internals and helpers │ └─ ... # Shared utilities, framework bindings, etc. -├─ demos/ # Vite app for live examples -│ └─ src/ -│ ├─ react/ # React demos -│ └─ vue/ # Vue demos -├─ tests/ # Cypress e2e tests that run against the demos +├─ demos/ # Vite app for live examples and colocated e2e specs +│ ├─ src/ +│ │ ├─ react/ # React demos +│ │ └─ vue/ # Vue demos +│ └─ test/ # Playwright helpers (getEditor, setEditorContent, ...) ├─ .changeset/ # Changesets for versioning and changelogs └─ .github/ # Workflows and GitHub-related config/docs ``` @@ -38,7 +38,7 @@ Notes: * All packages we publish or use live under `packages/*`. * The `demos/` folder contains a Vite app. It automatically discovers and parses React and Vue demos so they appear in the UI without manual wiring. -* Cypress tests in `tests/` expect the demos to be available on `http://localhost:3000`. +* Playwright e2e specs live alongside their demos as `demos/src/**/index.spec.ts`. `playwright.config.ts` auto-starts the Vite dev server on `http://127.0.0.1:4080` — no need to launch it manually. ## NPM scripts @@ -48,8 +48,14 @@ Scripts defined at the repo root: * `pnpm build` - build all packages via Turborepo * `pnpm lint` - run eslint checks * `pnpm lint:fix` - run prettier + eslint fix -* `pnpm test:e2e:open` - open Cypress against `tests/` -* `pnpm test:e2e` - run Cypress in headless mode +* `pnpm test:e2e` - run Playwright e2e tests headlessly in Chromium +* `pnpm test:e2e:firefox` - same, in Firefox +* `pnpm test:e2e:all` - same, in both browsers +* `pnpm test:e2e:open` - run Playwright in UI mode (Chromium tests) +* `pnpm test:e2e:open:firefox` - UI mode, Firefox tests +* `pnpm test:e2e:open:all` - UI mode, both browsers selectable +* `pnpm test:e2e:report` - open the HTML report from the last run +* `pnpm test:unit` - run Vitest unit tests in `packages/**/__tests__/` * `pnpm test` - build then run all tests * `pnpm serve` - build and serve the demos on port 3000 * `pnpm publish` - build and publish with Changesets @@ -91,23 +97,33 @@ When adding a demo, keep it small and self-contained, with imports from publishe --- -## Testing with Cypress +## Testing -* Cypress lives in `tests/` and drives the demos in a browser. -* Tests assume the app is running on `http://localhost:3000`. +Two layers: -Workflow: +* **Unit tests** with Vitest in `packages/**/__tests__/` (happy-dom). These test `@tiptap/core` and individual extensions in isolation. +* **E2E tests** with Playwright, colocated next to their demos as `demos/src/**/index.spec.ts`. They drive the real Vite-served demo pages in Chromium. + +Run them: ```bash -pnpm dev # terminal A -pnpm test:open # terminal B +pnpm test:unit # Vitest +pnpm test:e2e # Playwright headless (Chromium) +pnpm test:e2e:firefox # Playwright headless (Firefox) +pnpm test:e2e:all # both browsers — every test twice +pnpm test:e2e:open # UI mode (Chromium tests) +pnpm test:e2e:open:firefox # UI mode (Firefox tests) +pnpm test:e2e:open:all # UI mode, switch between browsers in the project picker +pnpm test:e2e:report # open the HTML report from the last run ``` -or for headless CI runs: +Playwright auto-starts the demo dev server (`pnpm -C demos run start:e2e` on port 4080) via `playwright.config.ts` — no separate terminal needed. Shared helpers live in `demos/test/helpers.ts`: `getEditor`, `setEditorContent`, `clickButton`. Use `demos/src/Commands/Cut/index.spec.ts` as a canonical template when adding new specs. -```bash -pnpm test:run -``` +Browser setup: + +* CI installs Chromium only (cached between runs) and only runs the Chromium project. +* For local Firefox testing, install it once with `pnpm exec playwright install firefox` (~80MB). +* UI mode (`--ui`) always opens its host window in Chromium — that's the Playwright UI app itself, not the browser running your tests. Tests still execute in the project you selected (check the trace metadata or `browserName` fixture if you need to confirm). --- @@ -168,8 +184,9 @@ Run the following to validate changes quickly: ```bash pnpm lint pnpm build -pnpm test # runs unit and/or cypress where configured -pnpm dev # optionally run the demos and open http://localhost:3000 +pnpm test:unit # Vitest +pnpm test:e2e # Playwright (auto-starts the demo server) +pnpm dev # optionally run the demos locally for manual verification ``` If a single package is failing types, run a targeted build for that package (e.g. `pnpm -w -F @tiptap/core build`), or run `pnpm build` at the repo root. @@ -192,4 +209,4 @@ If a single package is failing types, run a targeted build for that package (e.g ### Troubleshooting notes - If CI fails with dependency or lockfile errors, run `pnpm reset` locally and re-run the build. -- For flaky Cypress tests, run the demo locally with `pnpm dev` and reproduce the failing test in `pnpm test:open`. +- For flaky Playwright tests, reproduce locally with `pnpm test:e2e:open` (UI mode) or rerun with `--trace on` and inspect via `pnpm test:e2e:report`. diff --git a/demos/package.json b/demos/package.json index 21d8f6a121..e7a548ac27 100644 --- a/demos/package.json +++ b/demos/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "start": "vite --host", + "start:e2e": "vite --host --port 4080", "build:demos": "vite build", "preview": "vite preview" }, diff --git a/demos/src/Commands/Cut/React/index.spec.js b/demos/src/Commands/Cut/React/index.spec.js deleted file mode 100644 index 0921e44f8a..0000000000 --- a/demos/src/Commands/Cut/React/index.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -context('/src/Commands/Cut/React/', () => { - beforeEach(() => { - cy.visit('/src/Commands/Cut/React/') - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('
Hello World
This is a paragraph
with a break.
And this is some additional string content.
', - ) - }) - - it('should keep spaces inbetween tags in html content', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('Hello World
') - cy.get('.tiptap').should('contain.html', 'Hello World
') - }) - }) - - it('should keep empty spaces', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent(' ') - cy.get('.tiptap').should('contain.html', '') - }) - }) - - it('should insert text content correctly', () => { - cy.get('button[data-test-id="text-content"]').click() - - // check if the content html is correct - cy.get('.tiptap').should( - 'contain.html', - 'Hello World\nThis is content with a new line. Is this working?\n\nLets see if multiple new lines are inserted correctly', - ) - }) - - it('should keep newlines in pre tag', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('
foo\nbar')
- cy.get('.tiptap').should('contain.html', 'foo\nbar')
- })
- })
-
- it('should keep newlines and tabs', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.insertContent('Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK
') - cy.get('.tiptap').should('contain.html', 'Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK
') - }) - }) - - it('should keep newlines and tabs', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('Hello World
') - cy.get('.tiptap').should('contain.html', 'Hello World
') - }) - }) - - it('should allow inserting nothing', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('') - cy.get('.tiptap').should('contain.html', '') - }) - }) - - it('should allow inserting a partial HTML tag', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('foo') - cy.get('.tiptap').should('contain.html', '
foo
') - }) - }) - - it('should allow inserting an incomplete HTML tag', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('foofoo<p
') - }) - }) - - it('should allow inserting a list', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('ABC
123
Hello\n World\n
\n', { - parseOptions: { preserveWhitespace: false }, - }) - cy.get('.tiptap').should('contain.html', 'Hello World
') - }) - }) - - it('should split content when image is inserted inbetween text', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('HelloWorld
') - editor.commands.setTextSelection(6) - editor.commands.insertContent('Hello
World
', - ) - }) - }) - - it('should not split content when image is inserted at beginning of text', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('HelloWorld
') - editor.commands.setTextSelection(1) - editor.commands.insertContent('HelloWorld
', - ) - }) - }) - it('should respect editor.options.parseOptions if defined to be `false`', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.options.parseOptions = { preserveWhitespace: false } - editor.commands.insertContent('\nHello\n World\n
\n') - cy.get('.tiptap').should('contain.html', 'Hello World
') - }) - }) - - it('should respect editor.options.parseOptions if defined to be `full`', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.options.parseOptions = { preserveWhitespace: 'full' } - editor.commands.insertContent('\nHello\n World\n
\n') - cy.get('.tiptap').should('contain.html', 'Hello\n World
') - }) - }) - - it('should respect editor.options.parseOptions if defined to be `true`', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.options.parseOptions = { preserveWhitespace: true } - editor.commands.insertContent('Hello\n World\n
') - cy.get('.tiptap').should('contain.html', 'Hello World
') - }) - }) -}) diff --git a/demos/src/Commands/InsertContentApplyingRules/React/index.spec.js b/demos/src/Commands/InsertContentApplyingRules/React/index.spec.js deleted file mode 100644 index 06ed52f282..0000000000 --- a/demos/src/Commands/InsertContentApplyingRules/React/index.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -context('/src/Commands/InsertContentApplyingRules/React/', () => { - beforeEach(() => { - cy.visit('/src/Commands/InsertContentApplyingRules/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should apply list InputRule', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertContent('-', { - applyInputRules: true, - }) - - editor.commands.insertContent(' ', { - applyInputRules: true, - }) - - cy.get('.tiptap').should('contain.html', 'This is an italic text
') - }) - }) -}) diff --git a/demos/src/Commands/SetContent/React/index.spec.js b/demos/src/Commands/SetContent/React/index.spec.js deleted file mode 100644 index 08d4f071f7..0000000000 --- a/demos/src/Commands/SetContent/React/index.spec.js +++ /dev/null @@ -1,202 +0,0 @@ -context('/src/Commands/SetContent/React/', () => { - beforeEach(() => { - cy.visit('/src/Commands/SetContent/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').type('{selectall}{backspace}') - }) - - it('should insert raw text content', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Hello World.') - cy.get('.tiptap').should('contain.html', 'Hello World.
') - }) - }) - - it('should insert raw JSON content', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent({ type: 'paragraph', content: [{ type: 'text', text: 'Hello World.' }] }) - cy.get('.tiptap').should('contain.html', 'Hello World.
') - }) - }) - - it('should insert a Prosemirror Node as content', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent(editor.schema.node('paragraph', null, editor.schema.text('Hello World.'))) - cy.get('.tiptap').should('contain.html', 'Hello World.
') - }) - }) - - it('should insert a Prosemirror Fragment as content', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent( - editor.schema.node('doc', null, editor.schema.node('paragraph', null, editor.schema.text('Hello World.'))) - .content, - ) - cy.get('.tiptap').should('contain.html', 'Hello World.
') - }) - }) - - it('should emit updates', () => { - cy.get('.tiptap').then(([{ editor }]) => { - let updateCount = 0 - const callback = () => { - updateCount += 1 - } - - editor.on('update', callback) - // emit an update - editor.commands.setContent('Hello World.', { emitUpdate: true }) - expect(updateCount).to.equal(1) - - updateCount = 0 - // do not emit an update - editor.commands.setContent('Hello World again.', { emitUpdate: false }) - expect(updateCount).to.equal(0) - editor.off('update', callback) - }) - }) - - it('should insert more complex html content', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent( - 'This is a paragraph.
List Item A
List Item B
Subchild
This is a paragraph.
List Item A
List Item B
Subchild
Hello\n\tworld\n\t\thow\n\t\t\tnice.
') - cy.get('.tiptap').should('contain.html', 'Hello world how nice.
') - }) - }) - - it('should keep newlines and tabs when preserveWhitespace = full', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Hello\n\tworld\n\t\thow\n\t\t\tnice.
', { - parseOptions: { preserveWhitespace: 'full' }, - }) - cy.get('.tiptap').should('contain.html', 'Hello\n\tworld\n\t\thow\n\t\t\tnice.
') - }) - }) - - it('should overwrite existing content', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Initial Content
') - cy.get('.tiptap').should('contain.html', 'Initial Content
') - }) - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Overwritten Content
') - cy.get('.tiptap').should('contain.html', 'Overwritten Content
') - }) - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Content without tags') - cy.get('.tiptap').should('contain.html', 'Content without tags
') - }) - }) - - it('should insert mentions', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('@John Doe
') - cy.get('.tiptap').should( - 'contain.html', - '@John Doe', - ) - }) - }) - - it('should remove newlines and tabs between html fragments', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Hello World
') - cy.get('.tiptap').should('contain.html', 'Hello World
') - }) - }) - - // TODO I'm not certain about this behavior and what it should do... - // This exists in insertContentAt as well - it('should keep newlines and tabs between html fragments when preserveWhitespace = full', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Hello World
', { - parseOptions: { - preserveWhitespace: 'full', - }, - }) - cy.get('.tiptap').should('contain.html', '\n\t
Hello World
') - }) - }) - - it('should allow inserting nothing', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - cy.get('.tiptap').should('contain.html', '') - }) - }) - - it('should allow inserting nothing when preserveWhitespace = full', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('', { parseOptions: { preserveWhitespace: 'full' } }) - cy.get('.tiptap').should('contain.html', '') - }) - }) - - it('should allow inserting a partial HTML tag', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('foo') - cy.get('.tiptap').should('contain.html', '
foo
') - }) - }) - - it('should allow inserting a partial HTML tag when preserveWhitespace = full', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('foo', { parseOptions: { preserveWhitespace: 'full' } }) - cy.get('.tiptap').should('contain.html', '
foo
') - }) - }) - - it('will remove an incomplete HTML tag', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('foofoo
') - }) - }) - - // TODO I'm not certain about this behavior and what it should do... - // This exists in insertContentAt as well - it('should allow inserting an incomplete HTML tag when preserveWhitespace = full', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('foofoo<p
') - }) - }) - - it('should allow inserting a list', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('ABC
123
ABC
123
Hello\n World\n
\n', { - parseOptions: { - preserveWhitespace: false, - }, - }) - cy.get('.tiptap').should('contain.html', 'Hello World
') - }) - }) -}) diff --git a/demos/src/Demos/CollaborationSplitPane/React/index.spec.js b/demos/src/Demos/CollaborationSplitPane/React/index.spec.js deleted file mode 100644 index 1c41e139a1..0000000000 --- a/demos/src/Demos/CollaborationSplitPane/React/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Demos/CollaborationSplitPane/React/', () => { - beforeEach(() => { - cy.visit('/src/Demos/CollaborationSplitPane/React/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should have a ydoc', () => { - cy.get('.tiptap').then(([{ editor }]) => { - /** - * @type {import('yjs').Doc} - */ - const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document - - // eslint-disable-next-line - expect(yDoc).to.not.be.null - }) - }) -}) diff --git a/demos/src/Demos/CollaborationSplitPane/index.spec.ts b/demos/src/Demos/CollaborationSplitPane/index.spec.ts new file mode 100644 index 0000000000..9e9f91c5b0 --- /dev/null +++ b/demos/src/Demos/CollaborationSplitPane/index.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test' + +const demoName = 'CollaborationSplitPane' +const frameworkPaths = ['React'] +const demoPath = '/src/Demos' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('should have a working tiptap instance', async ({ page }) => { + const editor = page.locator('.tiptap').first() + await editor.waitFor() + + const hasEditor = await editor.evaluate((el: any) => !!el.editor) + + expect(hasEditor).toBe(true) + }) + + test('should have a ydoc', async ({ page }) => { + const editor = page.locator('.tiptap').first() + await editor.waitFor() + + const hasYDoc = await editor.evaluate((el: any) => { + const extension = el.editor.extensionManager.extensions.find((a: any) => a.name === 'collaboration') + + return !!extension?.options.document + }) + + expect(hasYDoc).toBe(true) + }) + }) + }) +}) diff --git a/demos/src/Demos/SingleRoomCollab/React/index.spec.js b/demos/src/Demos/SingleRoomCollab/React/index.spec.js deleted file mode 100644 index b5c9124944..0000000000 --- a/demos/src/Demos/SingleRoomCollab/React/index.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -context('/src/Demos/SingleRoomCollab/React/', () => { - beforeEach(() => { - cy.visit('/src/Demos/SingleRoomCollab/React/') - }) - - /* it('should show the current room with participants', () => { - cy.wait(6000) - cy.get('.editor__status') - .should('contain', 'rooms.') - .should('contain', 'users online') - }) - - it('should allow user to change name', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('John Doe') - cy.get('.editor__name > button').click() - cy.wait(6000) - cy.get('.editor__name').should('contain', 'John Doe') - }) - }) */ -}) diff --git a/demos/src/Demos/SingleRoomCollab/index.spec.ts b/demos/src/Demos/SingleRoomCollab/index.spec.ts new file mode 100644 index 0000000000..1a6bd5ee58 --- /dev/null +++ b/demos/src/Demos/SingleRoomCollab/index.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'SingleRoomCollab' +const frameworkPaths = ['React'] +const demoPath = '/src/Demos' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('should have a working tiptap instance', async ({ page }) => { + const editor = await getEditor(page) + const hasEditor = await editor.evaluate((el: any) => !!el.editor) + + expect(hasEditor).toBe(true) + }) + }) + }) +}) diff --git a/demos/src/Examples/Accessibility/React/index.spec.js b/demos/src/Examples/Accessibility/React/index.spec.js deleted file mode 100644 index da9ae57d83..0000000000 --- a/demos/src/Examples/Accessibility/React/index.spec.js +++ /dev/null @@ -1,80 +0,0 @@ -context('/src/Examples/AutolinkValidation/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/AutolinkValidation/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').type('{selectall}{backspace}') - }) - - const validLinks = [ - // [rawTextInput, textThatShouldBeLinked] - ['https://tiptap.dev ', 'https://tiptap.dev'], - ['http://tiptap.dev ', 'http://tiptap.dev'], - ['https://www.tiptap.dev/ ', 'https://www.tiptap.dev/'], - ['http://www.tiptap.dev/ ', 'http://www.tiptap.dev/'], - ['[http://www.example.com/] ', 'http://www.example.com/'], - ['(http://www.example.com/) ', 'http://www.example.com/'], - ] - - const invalidLinks = [ - 'tiptap.dev', - 'www.tiptap.dev', - // If you don't type a space, don't autolink - 'https://tiptap.dev', - ] - - validLinks.forEach(([rawTextInput, textThatShouldBeLinked]) => { - it(`should autolink ${rawTextInput}`, () => { - cy.get('.tiptap').type(rawTextInput) - cy.get('.tiptap a').contains(textThatShouldBeLinked) - }) - }) - - invalidLinks.forEach(rawTextInput => { - it(`should not autolink ${rawTextInput}`, () => { - cy.get('.tiptap').type(`{selectall}{backspace}${rawTextInput}`) - cy.get('.tiptap a').should('not.exist') - }) - }) - - it('should not relink unset links after entering second link', () => { - cy.get('.tiptap').type('https://tiptap.dev {home}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev ') - cy.get('[data-testid=unsetLink]').click() - cy.get('.tiptap').find('a').should('have.length', 0) - cy.get('.tiptap').type('{end}http://www.example.com/ ') - cy.get('.tiptap').find('a').should('have.length', 1).should('have.attr', 'href', 'http://www.example.com/') - }) - - it('should not relink unset links after hitting next paragraph', () => { - cy.get('.tiptap').type('https://tiptap.dev {home}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev ') - cy.get('[data-testid=unsetLink]').click() - cy.get('.tiptap').find('a').should('have.length', 0) - cy.get('.tiptap').type('{end}typing other text should prevent the link from relinking when hitting enter{enter}') - cy.get('.tiptap').find('a').should('have.length', 0) - }) - - it('should not relink unset links after modifying', () => { - cy.get('.tiptap').type('https://tiptap.dev {home}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev ') - cy.get('[data-testid=unsetLink]').click() - cy.get('.tiptap').find('a').should('have.length', 0) - cy.get('.tiptap').type('{home}').type('{rightArrow}'.repeat('https://'.length)).type('blah') - cy.get('.tiptap').should('have.text', 'https://blahtiptap.dev ') - cy.get('.tiptap').find('a').should('have.length', 0) - }) - - it('should autolink after hitting enter (new paragraph)', () => { - cy.get('.tiptap').type('https://tiptap.dev{enter}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev') - cy.get('.tiptap').find('a').should('have.length', 1).should('have.attr', 'href', 'https://tiptap.dev') - }) - - it('should autolink after hitting shift-enter (hardbreak)', () => { - cy.get('.tiptap').type('https://tiptap.dev{shift+enter}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev') - cy.get('.tiptap').find('a').should('have.length', 1).should('have.attr', 'href', 'https://tiptap.dev') - }) -}) diff --git a/demos/src/Examples/AutolinkValidation/React/index.spec.js b/demos/src/Examples/AutolinkValidation/React/index.spec.js deleted file mode 100644 index da9ae57d83..0000000000 --- a/demos/src/Examples/AutolinkValidation/React/index.spec.js +++ /dev/null @@ -1,80 +0,0 @@ -context('/src/Examples/AutolinkValidation/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/AutolinkValidation/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').type('{selectall}{backspace}') - }) - - const validLinks = [ - // [rawTextInput, textThatShouldBeLinked] - ['https://tiptap.dev ', 'https://tiptap.dev'], - ['http://tiptap.dev ', 'http://tiptap.dev'], - ['https://www.tiptap.dev/ ', 'https://www.tiptap.dev/'], - ['http://www.tiptap.dev/ ', 'http://www.tiptap.dev/'], - ['[http://www.example.com/] ', 'http://www.example.com/'], - ['(http://www.example.com/) ', 'http://www.example.com/'], - ] - - const invalidLinks = [ - 'tiptap.dev', - 'www.tiptap.dev', - // If you don't type a space, don't autolink - 'https://tiptap.dev', - ] - - validLinks.forEach(([rawTextInput, textThatShouldBeLinked]) => { - it(`should autolink ${rawTextInput}`, () => { - cy.get('.tiptap').type(rawTextInput) - cy.get('.tiptap a').contains(textThatShouldBeLinked) - }) - }) - - invalidLinks.forEach(rawTextInput => { - it(`should not autolink ${rawTextInput}`, () => { - cy.get('.tiptap').type(`{selectall}{backspace}${rawTextInput}`) - cy.get('.tiptap a').should('not.exist') - }) - }) - - it('should not relink unset links after entering second link', () => { - cy.get('.tiptap').type('https://tiptap.dev {home}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev ') - cy.get('[data-testid=unsetLink]').click() - cy.get('.tiptap').find('a').should('have.length', 0) - cy.get('.tiptap').type('{end}http://www.example.com/ ') - cy.get('.tiptap').find('a').should('have.length', 1).should('have.attr', 'href', 'http://www.example.com/') - }) - - it('should not relink unset links after hitting next paragraph', () => { - cy.get('.tiptap').type('https://tiptap.dev {home}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev ') - cy.get('[data-testid=unsetLink]').click() - cy.get('.tiptap').find('a').should('have.length', 0) - cy.get('.tiptap').type('{end}typing other text should prevent the link from relinking when hitting enter{enter}') - cy.get('.tiptap').find('a').should('have.length', 0) - }) - - it('should not relink unset links after modifying', () => { - cy.get('.tiptap').type('https://tiptap.dev {home}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev ') - cy.get('[data-testid=unsetLink]').click() - cy.get('.tiptap').find('a').should('have.length', 0) - cy.get('.tiptap').type('{home}').type('{rightArrow}'.repeat('https://'.length)).type('blah') - cy.get('.tiptap').should('have.text', 'https://blahtiptap.dev ') - cy.get('.tiptap').find('a').should('have.length', 0) - }) - - it('should autolink after hitting enter (new paragraph)', () => { - cy.get('.tiptap').type('https://tiptap.dev{enter}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev') - cy.get('.tiptap').find('a').should('have.length', 1).should('have.attr', 'href', 'https://tiptap.dev') - }) - - it('should autolink after hitting shift-enter (hardbreak)', () => { - cy.get('.tiptap').type('https://tiptap.dev{shift+enter}') - cy.get('.tiptap').should('have.text', 'https://tiptap.dev') - cy.get('.tiptap').find('a').should('have.length', 1).should('have.attr', 'href', 'https://tiptap.dev') - }) -}) diff --git a/demos/src/Examples/AutolinkValidation/Vue/index.spec.js b/demos/src/Examples/AutolinkValidation/Vue/index.spec.js deleted file mode 100644 index f7ec8eebb1..0000000000 --- a/demos/src/Examples/AutolinkValidation/Vue/index.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -context('/src/Examples/AutolinkValidation/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/AutolinkValidation/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').type('{selectall}{backspace}') - }) - - const validLinks = [ - // [rawTextInput, textThatShouldBeLinked] - ['https://tiptap.dev ', 'https://tiptap.dev'], - ['http://tiptap.dev ', 'http://tiptap.dev'], - ['https://www.tiptap.dev/ ', 'https://www.tiptap.dev/'], - ['http://www.tiptap.dev/ ', 'http://www.tiptap.dev/'], - ['[http://www.example.com/] ', 'http://www.example.com/'], - ['(http://www.example.com/) ', 'http://www.example.com/'], - ] - - const invalidLinks = [ - 'tiptap.dev', - 'www.tiptap.dev', - // If you don't type a space, don't autolink - 'https://tiptap.dev', - ] - - validLinks.forEach(([rawTextInput, textThatShouldBeLinked]) => { - it(`should autolink ${rawTextInput}`, () => { - cy.get('.tiptap').type(rawTextInput) - cy.get('.tiptap a').contains(textThatShouldBeLinked) - }) - }) - - invalidLinks.forEach(rawTextInput => { - it(`should not autolink ${rawTextInput}`, () => { - cy.get('.tiptap').type(`{selectall}{backspace}${rawTextInput}`) - cy.get('.tiptap a').should('not.exist') - }) - }) -}) diff --git a/demos/src/Examples/AutolinkValidation/index.spec.ts b/demos/src/Examples/AutolinkValidation/index.spec.ts new file mode 100644 index 0000000000..ffc7a519ac --- /dev/null +++ b/demos/src/Examples/AutolinkValidation/index.spec.ts @@ -0,0 +1,140 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'AutolinkValidation' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +const validLinks: [string, string][] = [ + ['https://tiptap.dev ', 'https://tiptap.dev'], + ['http://tiptap.dev ', 'http://tiptap.dev'], + ['https://www.tiptap.dev/ ', 'https://www.tiptap.dev/'], + ['http://www.tiptap.dev/ ', 'http://www.tiptap.dev/'], + ['[http://www.example.com/] ', 'http://www.example.com/'], + ['(http://www.example.com/) ', 'http://www.example.com/'], +] + +const invalidLinks = ['tiptap.dev', 'www.tiptap.dev', 'https://tiptap.dev'] + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.commands.clearContent() + }) + }) + + validLinks.forEach(([rawTextInput, textThatShouldBeLinked]) => { + test(`should autolink ${rawTextInput}`, async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type(rawTextInput) + + await expect(page.locator('.tiptap a').first()).toContainText(textThatShouldBeLinked) + }) + }) + + invalidLinks.forEach(rawTextInput => { + test(`should not autolink ${rawTextInput}`, async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type(rawTextInput) + + await expect(page.locator('.tiptap a')).toHaveCount(0) + }) + }) + + if (frameworkPath === 'React') { + test('should not relink unset links after entering second link', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('https://tiptap.dev ') + await editor.press('Home') + + await expect(page.locator('.tiptap')).toHaveText('https://tiptap.dev ') + + await page.locator('[data-testid=unsetLink]').click() + await expect(page.locator('.tiptap a')).toHaveCount(0) + + await editor.press('End') + await editor.type('http://www.example.com/ ') + + await expect(page.locator('.tiptap a')).toHaveCount(1) + await expect(page.locator('.tiptap a').first()).toHaveAttribute('href', 'http://www.example.com/') + }) + + test('should not relink unset links after hitting next paragraph', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('https://tiptap.dev ') + await editor.press('Home') + await expect(page.locator('.tiptap')).toHaveText('https://tiptap.dev ') + + await page.locator('[data-testid=unsetLink]').click() + await expect(page.locator('.tiptap a')).toHaveCount(0) + + await editor.press('End') + await editor.type('typing other text should prevent the link from relinking when hitting enter') + await editor.press('Enter') + + await expect(page.locator('.tiptap a')).toHaveCount(0) + }) + + test('should not relink unset links after modifying', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('https://tiptap.dev ') + await editor.press('Home') + await expect(page.locator('.tiptap')).toHaveText('https://tiptap.dev ') + + await page.locator('[data-testid=unsetLink]').click() + await expect(page.locator('.tiptap a')).toHaveCount(0) + + await editor.press('Home') + await Array.from({ length: 'https://'.length }).reduceFirst line
').run() + }) + await editor.click() + await editor.press('End') + await editor.press('Enter') + + await expect(page.locator('.tiptap p').nth(1)).toHaveText('') + await expect(page.locator('.tiptap .label').nth(1)).toHaveText('0') + }) + }) + }) +}) diff --git a/demos/src/Examples/Default/React/index.spec.js b/demos/src/Examples/Default/React/index.spec.js deleted file mode 100644 index 3f0762e354..0000000000 --- a/demos/src/Examples/Default/React/index.spec.js +++ /dev/null @@ -1,169 +0,0 @@ -context('/src/Examples/Default/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Default/React/') - - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Hello world
').selectAll().run() + }) + await clickButton(page, 'Paragraph') + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await clickButton(page, m.label) + + await expect(page.locator(`.tiptap ${m.tag}`)).toHaveText('Hello world') + }) + }) + + test('should clear marks when the button is pressed', async ({ page }) => { + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.chain().focus().setContent('Hello world
').selectAll().run() + }) + await clickButton(page, 'Paragraph') + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await clickButton(page, 'Bold') + + await expect(page.locator('.tiptap strong')).toHaveText('Hello world') + + await clickButton(page, 'Clear marks') + await expect(page.locator('.tiptap strong')).toHaveCount(0) + }) + + test('should clear nodes when the button is pressed', async ({ page }) => { + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor + .chain() + .focus() + .setContent('Hello world
A second item
A third item
Hello world
').selectAll().run() + }) + await clickButton(page, n.label) + + await expect(page.locator(`.tiptap ${n.tag}`)).toHaveText('Hello world') + }) + }) + + test('should add a hr when on the same line as a node', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('ArrowRight') + await clickButton(page, 'Horizontal rule') + + await expect(page.locator('.tiptap hr')).toHaveCount(1) + }) + + test('should add a hr when on a new line', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('ArrowRight') + await editor.press('Enter') + await clickButton(page, 'Horizontal rule') + + await expect(page.locator('.tiptap hr')).toHaveCount(1) + }) + + test('should add a br', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('ArrowRight') + await clickButton(page, 'Hard break') + + const brCount = await page.locator('.tiptap br').count() + + expect(brCount).toBeGreaterThan(0) + }) + + test('should undo', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('End') + await editor.type('~') + await expect(page.locator('.tiptap')).toContainText('~') + + await clickButton(page, 'Undo') + await expect(page.locator('.tiptap')).not.toContainText('~') + }) + + test('should redo', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('End') + await editor.type('~') + await expect(page.locator('.tiptap')).toContainText('~') + + await clickButton(page, 'Undo') + await expect(page.locator('.tiptap')).not.toContainText('~') + + await clickButton(page, 'Redo') + await expect(page.locator('.tiptap')).toContainText('~') + }) + }) + }) +}) diff --git a/demos/src/Examples/Drawing/Vue/index.spec.js b/demos/src/Examples/Drawing/Vue/index.spec.js deleted file mode 100644 index 8667eb5441..0000000000 --- a/demos/src/Examples/Drawing/Vue/index.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -context('/src/Examples/Drawing/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Drawing/Vue/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should have a svg canvas', () => { - cy.get('.tiptap svg').should('exist') - }) - - it('should draw on the svg canvas', () => { - cy.get('.tiptap svg').should('exist') - - cy.wait(500) - - cy.get('input').then(inputs => { - const color = inputs[0].value - const size = inputs[1].value - - cy.get('.tiptap svg') - .click() - .trigger('mousedown', { pageX: 100, pageY: 100, which: 1 }) - .trigger('mousemove', { pageX: 200, pageY: 200, which: 1 }) - .trigger('mouseup') - - cy.get('.tiptap svg path') - .should('exist') - .should('have.attr', 'stroke-width', size) - .should('have.attr', 'stroke', color.toUpperCase()) - }) - }) -}) diff --git a/demos/src/Examples/Drawing/index.spec.ts b/demos/src/Examples/Drawing/index.spec.ts new file mode 100644 index 0000000000..ad62b8eccc --- /dev/null +++ b/demos/src/Examples/Drawing/index.spec.ts @@ -0,0 +1,54 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Drawing' +const frameworkPaths = ['Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('should have a working tiptap instance', async ({ page }) => { + const editor = await getEditor(page) + const hasEditor = await editor.evaluate((el: any) => !!el.editor) + + expect(hasEditor).toBe(true) + }) + + test('should have a svg canvas', async ({ page }) => { + await expect(page.locator('.tiptap svg')).toBeVisible() + }) + + test('should draw on the svg canvas', async ({ page }) => { + await getEditor(page) + await expect(page.locator('.tiptap svg')).toBeVisible() + + const color = await page.locator('input').nth(0).inputValue() + const size = await page.locator('input').nth(1).inputValue() + + const svg = page.locator('.tiptap svg') + const box = await svg.boundingBox() + + if (!box) { + throw new Error('SVG bounding box not found') + } + + await page.mouse.move(box.x + 50, box.y + 50) + await page.mouse.down() + await page.mouse.move(box.x + 150, box.y + 150) + await page.mouse.up() + + const path = page.locator('.tiptap svg path').first() + await expect(path).toHaveAttribute('stroke-width', size) + await expect(path).toHaveAttribute('stroke', color.toUpperCase()) + }) + }) + }) +}) diff --git a/demos/src/Examples/EnterShortcuts/React/index.spec.js b/demos/src/Examples/EnterShortcuts/React/index.spec.js deleted file mode 100644 index b3cce063a5..0000000000 --- a/demos/src/Examples/EnterShortcuts/React/index.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -///Example Text
') - }) - }) - - it('should update the hint html when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { metaKey: true, key: 'Enter' }) - cy.get('.hint').should('contain', 'Meta-Enter was the last shortcut') - }) - - it('should update the hint html when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { shiftKey: true, key: 'Enter' }) - cy.get('.hint').should('contain', 'Shift-Enter was the last shortcut') - }) - - it('should update the hint html when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { ctrlKey: true, key: 'Enter' }) - cy.get('.hint').should('contain', 'Ctrl-Enter was the last shortcut') - }) -}) diff --git a/demos/src/Examples/EnterShortcuts/Vue/index.spec.js b/demos/src/Examples/EnterShortcuts/Vue/index.spec.js deleted file mode 100644 index cfa9657463..0000000000 --- a/demos/src/Examples/EnterShortcuts/Vue/index.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -context('/src/Examples/EnterShortcuts/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/EnterShortcuts/Vue/') - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should update the hint html when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { metaKey: true, key: 'Enter' }) - cy.get('.hint').should('contain', 'Meta-Enter was the last shortcut') - }) - - it('should update the hint html when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { shiftKey: true, key: 'Enter' }) - cy.get('.hint').should('contain', 'Shift-Enter was the last shortcut') - }) - - it('should update the hint html when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { ctrlKey: true, key: 'Enter' }) - cy.get('.hint').should('contain', 'Ctrl-Enter was the last shortcut') - }) -}) diff --git a/demos/src/Examples/EnterShortcuts/index.spec.ts b/demos/src/Examples/EnterShortcuts/index.spec.ts new file mode 100644 index 0000000000..bd6ffbf165 --- /dev/null +++ b/demos/src/Examples/EnterShortcuts/index.spec.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'EnterShortcuts' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + }) + }) + + test('should update the hint html on Meta+Enter', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('Meta+Enter') + + await expect(page.locator('.hint')).toContainText('Meta-Enter was the last shortcut') + }) + + test('should update the hint html on Shift+Enter', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('Shift+Enter') + + await expect(page.locator('.hint')).toContainText('Shift-Enter was the last shortcut') + }) + + test('should update the hint html on Ctrl+Enter', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('Control+Enter') + + await expect(page.locator('.hint')).toContainText('Ctrl-Enter was the last shortcut') + }) + }) + }) +}) diff --git a/demos/src/Examples/Formatting/React/index.spec.js b/demos/src/Examples/Formatting/React/index.spec.js deleted file mode 100644 index ce3c5594e7..0000000000 --- a/demos/src/Examples/Formatting/React/index.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -context('/src/Examples/Formatting/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Formatting/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').type('{selectall}{backspace}') - }) - - const marks = [{ label: 'Highlight', mark: 'mark' }] - - marks.forEach(m => { - it(`sets ${m.label}`, () => { - cy.get('.tiptap').type('Hello world.{selectall}') - cy.get('button').contains(m.label).click() - cy.get('.tiptap mark').should('exist') - }) - }) - - const alignments = [ - { label: 'Left', alignment: 'left' }, - { label: 'Center', alignment: 'center' }, - { label: 'Right', alignment: 'right' }, - { label: 'Justify', alignment: 'justify' }, - ] - - alignments.forEach(a => { - it(`sets ${a.label}`, () => { - cy.get('.tiptap').type('Hello world.{selectall}') - cy.get('button').contains(a.label).click() - if (a.alignment !== 'left') { - cy.get('.tiptap p').should('have.css', 'text-align', a.alignment) - } - }) - }) -}) diff --git a/demos/src/Examples/Formatting/Vue/index.spec.js b/demos/src/Examples/Formatting/Vue/index.spec.js deleted file mode 100644 index 2d5f8bed6c..0000000000 --- a/demos/src/Examples/Formatting/Vue/index.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -context('/src/Examples/Formatting/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Formatting/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').type('{selectall}{backspace}') - }) - - const marks = [{ label: 'Highlight', mark: 'mark' }] - - marks.forEach(m => { - it(`sets ${m.label}`, () => { - cy.get('.tiptap').type('Hello world.{selectall}') - cy.get('button').contains(m.label).click() - cy.get('.tiptap mark').should('exist') - }) - }) - - const alignments = [ - { label: 'Left', alignment: 'left' }, - { label: 'Center', alignment: 'center' }, - { label: 'Right', alignment: 'right' }, - { label: 'Justify', alignment: 'justify' }, - ] - - alignments.forEach(a => { - it(`sets ${a.label}`, () => { - cy.get('.tiptap').type('Hello world.{selectall}') - cy.get('button').contains(a.label).click() - if (a.alignment !== 'left') { - cy.get('.tiptap p').should('have.css', 'text-align', a.alignment) - } - }) - }) -}) diff --git a/demos/src/Examples/Formatting/index.spec.ts b/demos/src/Examples/Formatting/index.spec.ts new file mode 100644 index 0000000000..05cd2a3765 --- /dev/null +++ b/demos/src/Examples/Formatting/index.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from '@playwright/test' + +import { clickButton, getEditor } from '../../../test/helpers.js' + +const demoName = 'Formatting' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +const marks = [{ label: 'Highlight', mark: 'mark' }] + +const alignments = [ + { label: 'Left', alignment: 'left' }, + { label: 'Center', alignment: 'center' }, + { label: 'Right', alignment: 'right' }, + { label: 'Justify', alignment: 'justify' }, +] + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.chain().focus().clearContent().run() + }) + }) + + marks.forEach(m => { + test(`sets ${m.label}`, async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('Hello world.') + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await clickButton(page, m.label) + + await expect(page.locator(`.tiptap ${m.mark}`)).toBeVisible() + }) + }) + + alignments.forEach(a => { + test(`sets ${a.label}`, async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('Hello world.') + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await clickButton(page, a.label) + + if (a.alignment !== 'left') { + await expect(page.locator('.tiptap p').first()).toHaveCSS('text-align', a.alignment) + } + }) + }) + }) + }) +}) diff --git a/demos/src/Examples/Images/React/index.spec.js b/demos/src/Examples/Images/React/index.spec.js deleted file mode 100644 index 0a8e883ec6..0000000000 --- a/demos/src/Examples/Images/React/index.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -context('/src/Examples/Images/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Images/React/') - }) - - it('finds image elements inside editor', () => { - cy.get('.tiptap img').should('have.length', 2) - }) - - it('allows removing images', () => { - cy.get('.tiptap').should('be.visible') - cy.get('.tiptap img').should('have.length', 2).and('be.visible') - cy.get('.tiptap img').first().click() - cy.get('.tiptap img.ProseMirror-selectednode').should('exist') - cy.get('.tiptap').type('{backspace}') - cy.get('.tiptap img').should('have.length', 1) - }) - - it('allows images to be added via URL', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('https://placehold.co/400x400') - - cy.wait(1000) - cy.get('button').contains('Add image from URL').click({ force: false }) - cy.wait(1000) - cy.get('.tiptap img').should('have.length', 3) - }) - }) -}) diff --git a/demos/src/Examples/Images/Vue/index.spec.js b/demos/src/Examples/Images/Vue/index.spec.js deleted file mode 100644 index 2661a9ff60..0000000000 --- a/demos/src/Examples/Images/Vue/index.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -context('/src/Examples/Images/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Images/Vue/') - }) - - // TODO: Write tests - it('finds image elements inside editor', () => { - cy.get('.tiptap img').should('have.length', 2) - }) - - it('allows removing images', () => { - cy.get('.tiptap').should('be.visible') - cy.get('.tiptap img').should('have.length', 2).and('be.visible') - cy.get('.tiptap img') - .first() - .should($img => { - expect($img[0].naturalWidth).to.be.greaterThan(0) - }) - .click() - cy.get('.tiptap img.ProseMirror-selectednode').should('exist') - cy.get('.tiptap').type('{backspace}') - cy.get('.tiptap img').should('have.length', 1) - }) - - it('allows images to be added via URL', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('https://placehold.co/400x400') - - cy.wait(1000) - cy.get('button').contains('Add image from URL').click({ force: false }) - cy.wait(1000) - cy.get('.tiptap img').should('have.length', 3) - }) - }) -}) diff --git a/demos/src/Examples/Images/index.spec.ts b/demos/src/Examples/Images/index.spec.ts new file mode 100644 index 0000000000..04b0dd748d --- /dev/null +++ b/demos/src/Examples/Images/index.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Images' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await getEditor(page) + }) + + test('removes a selected image with Backspace', async ({ page }) => { + const editor = await getEditor(page) + + await expect(page.locator('.tiptap img')).toHaveCount(2) + + await page.locator('.tiptap img').first().click() + await expect(page.locator('.tiptap img.ProseMirror-selectednode')).toBeVisible() + + await editor.press('Backspace') + + await expect(page.locator('.tiptap img')).toHaveCount(1) + }) + }) + }) +}) diff --git a/demos/src/Examples/InteractivityComponent/React/index.spec.js b/demos/src/Examples/InteractivityComponent/React/index.spec.js deleted file mode 100644 index 3a238d1449..0000000000 --- a/demos/src/Examples/InteractivityComponent/React/index.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -context('/src/Examples/InteractivityComponent/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/InteractivityComponent/React/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should render a custom node', () => { - cy.get('.tiptap .react-component').should('have.length', 1) - }) - - it('should handle count click inside custom node', () => { - cy.get('.tiptap .react-component button') - .should('have.text', 'This button has been clicked 0 times.') - .click() - .should('have.text', 'This button has been clicked 1 times.') - .click() - .should('have.text', 'This button has been clicked 2 times.') - .click() - .should('have.text', 'This button has been clicked 3 times.') - }) -}) diff --git a/demos/src/Examples/InteractivityComponent/Vue/index.spec.js b/demos/src/Examples/InteractivityComponent/Vue/index.spec.js deleted file mode 100644 index 653e8ea6e0..0000000000 --- a/demos/src/Examples/InteractivityComponent/Vue/index.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -context('/src/Examples/InteractivityComponent/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/InteractivityComponent/Vue/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should render a custom node', () => { - cy.get('.tiptap .vue-component').should('have.length', 1) - }) - - it('should handle count click inside custom node', () => { - cy.get('.tiptap .vue-component button') - .should('have.text', 'This button has been clicked 0 times.') - .click() - .should('have.text', 'This button has been clicked 1 times.') - .click() - .should('have.text', 'This button has been clicked 2 times.') - .click() - .should('have.text', 'This button has been clicked 3 times.') - }) -}) diff --git a/demos/src/Examples/InteractivityComponent/index.spec.ts b/demos/src/Examples/InteractivityComponent/index.spec.ts new file mode 100644 index 0000000000..00c2fadce8 --- /dev/null +++ b/demos/src/Examples/InteractivityComponent/index.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'InteractivityComponent' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + const componentClass = frameworkPath === 'React' ? '.react-component' : '.vue-component' + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('should render a custom node', async ({ page }) => { + await getEditor(page) + await expect(page.locator(`.tiptap ${componentClass}`)).toHaveCount(1) + }) + + test('should handle count click inside custom node', async ({ page }) => { + await getEditor(page) + const button = page.locator(`.tiptap ${componentClass} button`).first() + + await expect(button).toHaveText('This button has been clicked 0 times.') + await button.click() + await expect(button).toHaveText('This button has been clicked 1 times.') + await button.click() + await expect(button).toHaveText('This button has been clicked 2 times.') + await button.click() + await expect(button).toHaveText('This button has been clicked 3 times.') + }) + }) + }) +}) diff --git a/demos/src/Examples/InteractivityComponentContent/React/index.spec.js b/demos/src/Examples/InteractivityComponentContent/React/index.spec.js deleted file mode 100644 index a3f82cb786..0000000000 --- a/demos/src/Examples/InteractivityComponentContent/React/index.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -context('/src/Examples/InteractivityComponentContent/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/InteractivityComponentContent/React/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.ProseMirror').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should render a custom node', () => { - cy.get('.ProseMirror .react-component').should('have.length', 1) - }) - - it('should allow text editing inside component', () => { - cy.get('.ProseMirror .react-component .content div') - .invoke('attr', 'contentEditable', true) - .invoke('text', '') - .type('Hello World!') - .should('have.text', 'Hello World!') - }) - - it('should allow text editing inside component with markdown text', () => { - cy.get('.ProseMirror .react-component .content div') - .invoke('attr', 'contentEditable', true) - .invoke('text', '') - .type('Hello World! This is **bold**.') - .should('have.text', 'Hello World! This is bold.') - - cy.get('.ProseMirror .react-component .content strong').should('exist') - }) - - it('should remove node via selectall', () => { - cy.get('.ProseMirror .react-component').should('have.length', 1) - - cy.get('.ProseMirror').type('{selectall}{backspace}') - - cy.get('.ProseMirror .react-component').should('have.length', 0) - }) -}) diff --git a/demos/src/Examples/InteractivityComponentContent/Vue/index.spec.js b/demos/src/Examples/InteractivityComponentContent/Vue/index.spec.js deleted file mode 100644 index afcfe70ab6..0000000000 --- a/demos/src/Examples/InteractivityComponentContent/Vue/index.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -context('/src/Examples/InteractivityComponentContent/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/InteractivityComponentContent/Vue/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should render a custom node', () => { - cy.get('.tiptap .vue-component').should('have.length', 1) - }) - - it('should allow text editing inside component', () => { - cy.get('.tiptap .vue-component .content') - .invoke('attr', 'contentEditable', true) - .invoke('text', '') - .type('Hello World!') - .should('have.text', 'Hello World!') - }) - - it('should allow text editing inside component with markdown text', () => { - cy.get('.tiptap .vue-component .content') - .invoke('attr', 'contentEditable', true) - .invoke('text', '') - .type('Hello World! This is **bold**.') - .should('have.text', 'Hello World! This is bold.') - - cy.get('.tiptap .vue-component .content strong').should('exist') - }) - - it('should remove node via selectall', () => { - cy.get('.tiptap .vue-component').should('have.length', 1) - - cy.get('.tiptap').type('{selectall}{backspace}') - - cy.get('.tiptap .vue-component').should('have.length', 0) - }) -}) diff --git a/demos/src/Examples/InteractivityComponentContent/index.spec.ts b/demos/src/Examples/InteractivityComponentContent/index.spec.ts new file mode 100644 index 0000000000..3458e1350c --- /dev/null +++ b/demos/src/Examples/InteractivityComponentContent/index.spec.ts @@ -0,0 +1,83 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'InteractivityComponentContent' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + const componentClass = frameworkPath === 'React' ? '.react-component' : '.vue-component' + const nodeName = frameworkPath === 'React' ? 'reactComponent' : 'vueComponent' + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('should render a custom node', async ({ page }) => { + await getEditor(page) + await expect(page.locator(`.ProseMirror ${componentClass}`)).toHaveCount(1) + }) + + test('should allow text editing inside component', async ({ page }) => { + const editor = await getEditor(page) + + await editor.evaluate((el: any, name: string) => { + const e = el.editor + e.state.doc.descendants((node: any, pos: number) => { + if (node.type.name !== name) { + return true + } + const from = pos + 1 + const to = pos + node.nodeSize - 1 + e.chain().focus().setTextSelection({ from, to }).deleteSelection().insertContent('Hello World!').run() + return false + }) + }, nodeName) + + const content = page.locator(`.ProseMirror ${componentClass} .content`).first() + await expect(content).toHaveText('Hello World!') + }) + + test('should allow text editing inside component with markdown text', async ({ page }) => { + const editor = await getEditor(page) + + await editor.evaluate((el: any, name: string) => { + const e = el.editor + e.state.doc.descendants((node: any, pos: number) => { + if (node.type.name !== name) { + return true + } + const from = pos + 1 + const to = pos + node.nodeSize - 1 + e.chain().focus().setTextSelection({ from, to }).deleteSelection().run() + return false + }) + }, nodeName) + + const content = page.locator(`.ProseMirror ${componentClass} .content`).first() + + await content.click() + await page.keyboard.type('Hello World! This is **bold**.') + + await expect(content).toHaveText('Hello World! This is bold.') + await expect(page.locator(`.ProseMirror ${componentClass} .content strong`)).toBeVisible() + }) + + test('should remove node via selectall', async ({ page }) => { + const editor = await getEditor(page) + + await expect(page.locator(`.ProseMirror ${componentClass}`)).toHaveCount(1) + + await editor.evaluate((el: any) => { + el.editor.chain().focus().selectAll().deleteSelection().run() + }) + + await expect(page.locator(`.ProseMirror ${componentClass}`)).toHaveCount(0) + }) + }) + }) +}) diff --git a/demos/src/Examples/InteractivityComponentProvideInject/Vue/index.spec.js b/demos/src/Examples/InteractivityComponentProvideInject/Vue/index.spec.js deleted file mode 100644 index 6965081a87..0000000000 --- a/demos/src/Examples/InteractivityComponentProvideInject/Vue/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Examples/InteractivityComponentProvideInject/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/InteractivityComponentProvideInject/Vue/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should render a custom node', () => { - cy.get('.tiptap .vue-component').should('have.length', 1) - }) - - it('should have global and all injected values', () => { - const expectedTexts = ['globalValue', 'appValue', 'indexValue', 'editorValue'] - - cy.get('.tiptap .vue-component p').each((p, index) => { - cy.wrap(p).should('have.text', expectedTexts[index]) - }) - }) -}) diff --git a/demos/src/Examples/InteractivityComponentProvideInject/index.spec.ts b/demos/src/Examples/InteractivityComponentProvideInject/index.spec.ts new file mode 100644 index 0000000000..beaea1a9a2 --- /dev/null +++ b/demos/src/Examples/InteractivityComponentProvideInject/index.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'InteractivityComponentProvideInject' +const frameworkPaths = ['Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('should have a working tiptap instance', async ({ page }) => { + const editor = await getEditor(page) + const hasEditor = await editor.evaluate((el: any) => !!el.editor) + + expect(hasEditor).toBe(true) + }) + + test('should render a custom node', async ({ page }) => { + await getEditor(page) + await expect(page.locator('.tiptap .vue-component')).toHaveCount(1) + }) + + test('should have global and all injected values', async ({ page }) => { + await getEditor(page) + const expectedTexts = ['globalValue', 'appValue', 'indexValue', 'editorValue'] + const paragraphs = page.locator('.tiptap .vue-component p') + + await expectedTexts.reduceExample Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('enter should make a new paragraph', () => { - cy.get('.tiptap').type('First Paragraph{enter}Second Paragraph').find('p').should('have.length', 2) - }) - - it('backspace should remove the last paragraph', () => { - cy.get('.tiptap').type('{enter}').find('p').should('have.length', 2) - - cy.get('.tiptap').type('{backspace}').find('p').should('have.length', 1) - }) -}) diff --git a/demos/src/Examples/Minimal/Vue/index.spec.js b/demos/src/Examples/Minimal/Vue/index.spec.js deleted file mode 100644 index 1c392f580b..0000000000 --- a/demos/src/Examples/Minimal/Vue/index.spec.js +++ /dev/null @@ -1,35 +0,0 @@ -context('/src/Examples/Minimal/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Minimal/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('text should be wrapped in a paragraph by default', () => { - cy.get('.tiptap').type('Example Text').find('p').should('contain', 'Example Text') - }) - - it('should parse paragraphs correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('enter should make a new paragraph', () => { - cy.get('.tiptap').type('First Paragraph{enter}Second Paragraph').find('p').should('have.length', 2) - }) - - it('backspace should remove the last paragraph', () => { - cy.get('.tiptap').type('{enter}').find('p').should('have.length', 2) - - cy.get('.tiptap').type('{backspace}').find('p').should('have.length', 1) - }) -}) diff --git a/demos/src/Examples/Minimal/index.spec.ts b/demos/src/Examples/Minimal/index.spec.ts new file mode 100644 index 0000000000..6ff4b8300f --- /dev/null +++ b/demos/src/Examples/Minimal/index.spec.ts @@ -0,0 +1,68 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Minimal' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.commands.clearContent() + }) + }) + + test('text should be wrapped in a paragraph by default', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('Example Text') + + await expect(page.locator('.tiptap p')).toContainText('Example Text') + }) + + test('should parse paragraphs correctly', async ({ page }) => { + const editor = await getEditor(page) + + const result = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + const first = el.editor.getHTML() + el.editor.commands.setContent('Example Text
') + return [first, el.editor.getHTML()] + }) + + expect(result).toEqual(['Example Text
', 'Example Text
']) + }) + + test('enter should make a new paragraph', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('First Paragraph') + await editor.press('Enter') + await editor.type('Second Paragraph') + + await expect(page.locator('.tiptap p')).toHaveCount(2) + }) + + test('backspace should remove the last paragraph', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.press('Enter') + await expect(page.locator('.tiptap p')).toHaveCount(2) + + await editor.press('Backspace') + await expect(page.locator('.tiptap p')).toHaveCount(1) + }) + }) + }) +}) diff --git a/demos/src/Examples/MultiMention/React/index.spec.js b/demos/src/Examples/MultiMention/React/index.spec.js deleted file mode 100644 index db81691eb6..0000000000 --- a/demos/src/Examples/MultiMention/React/index.spec.js +++ /dev/null @@ -1,226 +0,0 @@ -context('/src/Examples/MultiMention/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/MultiMention/React/') - }) - - describe('Person mentions (@)', () => { - it('should insert a person mention', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('@Lea Thompson
') - cy.get('.tiptap').should( - 'contain.html', - '@Lea Thompson', - ) - }) - }) - - it("should open a dropdown menu when I type '@'", () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - }) - - it('should display the correct person options in the dropdown menu', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button').should('have.length', 5) - cy.get('.dropdown-menu button:nth-child(1)') - .should('contain.text', 'Lea Thompson') - .and('have.class', 'is-selected') - cy.get('.dropdown-menu button:nth-child(2)').should('contain.text', 'Cyndi Lauper') - cy.get('.dropdown-menu button:nth-child(3)').should('contain.text', 'Tom Cruise') - cy.get('.dropdown-menu button:nth-child(4)').should('contain.text', 'Madonna') - cy.get('.dropdown-menu button:nth-child(5)').should('contain.text', 'Jerry Hall') - }) - - it('should insert Cyndi Lauper mention when clicking on her option', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button:nth-child(2)').contains('Cyndi Lauper').click() - - cy.get('.tiptap').should( - 'contain.html', - '@Cyndi Lauper', - ) - }) - - it('should close the dropdown menu when I move the cursor outside the editor', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{moveToStart}') - cy.get('.dropdown-menu').should('not.exist') - }) - - it('should close the dropdown menu when I press the escape key', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{esc}') - cy.get('.dropdown-menu').should('not.exist') - }) - - it('should insert Tom Cruise when selecting his option with the arrow keys and pressing the enter key', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{downarrow}{downarrow}') - cy.get('.dropdown-menu button:nth-child(3)').should('have.class', 'is-selected') - cy.get('.tiptap').type('{enter}') - - cy.get('.tiptap').should( - 'contain.html', - '@Tom Cruise', - ) - }) - - it('should show a "No result" message when I search for a person that is not in the list', () => { - cy.get('.tiptap').type('{selectall}{backspace}@nonexistent') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu').should('contain.text', 'No result') - }) - - it('should only show the Madonna option in the dropdown when I type "@mado"', () => { - cy.get('.tiptap').type('{selectall}{backspace}@mado') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button').should('have.length', 1) - cy.get('.dropdown-menu button:nth-child(1)').should('contain.text', 'Madonna') - }) - - it('should insert Madonna when I type "@mado" and hit enter', () => { - cy.get('.tiptap').type('{selectall}{backspace}@mado{enter}') - cy.get('.tiptap').should( - 'contain.html', - '@Madonna', - ) - }) - }) - - describe('Movie mentions (#)', () => { - it('should insert a movie mention', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent( - '#The Matrix
', - ) - cy.get('.tiptap').should( - 'contain.html', - '#The Matrix', - ) - }) - }) - - it("should open a dropdown menu when I type '#'", () => { - cy.get('.tiptap').type('{selectall}{backspace}#') - cy.get('.dropdown-menu').should('exist') - }) - - it('should display the correct movie options in the dropdown menu', () => { - cy.get('.tiptap').type('{selectall}{backspace}#') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button').should('have.length', 3) - cy.get('.dropdown-menu button:nth-child(1)') - .should('contain.text', 'Dirty Dancing') - .and('have.class', 'is-selected') - cy.get('.dropdown-menu button:nth-child(2)').should('contain.text', 'Pirates of the Caribbean') - cy.get('.dropdown-menu button:nth-child(3)').should('contain.text', 'The Matrix') - }) - - it('should insert Pirates of the Caribbean mention when clicking on its option', () => { - cy.get('.tiptap').type('{selectall}{backspace}#') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button:nth-child(2)').contains('Pirates of the Caribbean').click() - - cy.get('.tiptap').should( - 'contain.html', - '#Pirates of the Caribbean', - ) - }) - - it('should close the dropdown menu when I move the cursor outside the editor', () => { - cy.get('.tiptap').type('{selectall}{backspace}#') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{moveToStart}') - cy.get('.dropdown-menu').should('not.exist') - }) - - it('should close the dropdown menu when I press the escape key', () => { - cy.get('.tiptap').type('{selectall}{backspace}#') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{esc}') - cy.get('.dropdown-menu').should('not.exist') - }) - - it('should insert The Matrix when selecting its option with the arrow keys and pressing the enter key', () => { - cy.get('.tiptap').type('{selectall}{backspace}#') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{downarrow}{downarrow}') - cy.get('.dropdown-menu button:nth-child(3)').should('have.class', 'is-selected') - cy.get('.tiptap').type('{enter}') - - cy.get('.tiptap').should( - 'contain.html', - '#The Matrix', - ) - }) - - it('should show a "No result" message when I search for a movie that is not in the list', () => { - cy.get('.tiptap').type('{selectall}{backspace}#nonexistent') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu').should('contain.text', 'No result') - }) - - it('should only show the Dirty Dancing option in the dropdown when I type "#dir"', () => { - cy.get('.tiptap').type('{selectall}{backspace}#dir') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button').should('have.length', 1) - cy.get('.dropdown-menu button:nth-child(1)').should('contain.text', 'Dirty Dancing') - }) - - it('should insert Dirty Dancing when I type "#dir" and hit enter', () => { - cy.get('.tiptap').type('{selectall}{backspace}#dir{enter}') - cy.get('.tiptap').should( - 'contain.html', - '#Dirty Dancing', - ) - }) - }) - - describe('Interaction between mention types', () => { - it('should support both mention types in the same document', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent( - '@Madonna starred in #Dirty Dancing
', - ) - - cy.get('.tiptap').should( - 'contain.html', - '@Madonna', - ) - cy.get('.tiptap').should( - 'contain.html', - '#Dirty Dancing', - ) - }) - }) - - it('should allow switching between mention types', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button:nth-child(1)').should('contain.text', 'Lea Thompson') - - // Close the dropdown by moving cursor - cy.get('.tiptap').type('{moveToStart}') - cy.get('.dropdown-menu').should('not.exist') - - // Open a new dropdown with # - cy.get('.tiptap').type('{selectall}{backspace}#') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button:nth-child(1)').should('contain.text', 'Dirty Dancing') - }) - - it('should insert both types of mentions in sequence', () => { - cy.get('.tiptap').type('{selectall}{backspace}@mado{enter} likes #the{enter}') - - cy.get('.tiptap').should( - 'contain.html', - '@Madonna likes #The Matrix', - ) - }) - }) -}) diff --git a/demos/src/Examples/MultiMention/Vue/index.spec.js b/demos/src/Examples/MultiMention/Vue/index.spec.js deleted file mode 100644 index 279adbb022..0000000000 --- a/demos/src/Examples/MultiMention/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Nodes/Mention/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Mention/Vue/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Examples/MultiMention/index.spec.ts b/demos/src/Examples/MultiMention/index.spec.ts new file mode 100644 index 0000000000..cb4ed6e05f --- /dev/null +++ b/demos/src/Examples/MultiMention/index.spec.ts @@ -0,0 +1,336 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'MultiMention' +const frameworkPaths = ['React'] +const demoPath = '/src/Examples' + +async function clearEditor(page: any) { + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.chain().focus().clearContent().run() + }) + await editor.click() +} + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test.describe('Person mentions (@)', () => { + test('should insert a person mention', async ({ page }) => { + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.commands.setContent( + '@Lea Thompson
', + ) + }) + + const html = await editor.evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '@Lea Thompson', + ) + }) + + test("should open a dropdown menu when I type '@'", async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@') + + await expect(page.locator('.dropdown-menu')).toBeVisible() + }) + + test('should display the correct person options in the dropdown menu', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@') + + const dropdown = page.locator('.dropdown-menu') + await expect(dropdown).toBeVisible() + await expect(dropdown.locator('button')).toHaveCount(5) + + const buttons = dropdown.locator('button') + + await expect(buttons.nth(0)).toContainText('Lea Thompson') + await expect(buttons.nth(0)).toHaveClass(/is-selected/) + await expect(buttons.nth(1)).toContainText('Cyndi Lauper') + await expect(buttons.nth(2)).toContainText('Tom Cruise') + await expect(buttons.nth(3)).toContainText('Madonna') + await expect(buttons.nth(4)).toContainText('Jerry Hall') + }) + + test('should insert Cyndi Lauper mention when clicking on her option', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@') + + await page.locator('.dropdown-menu button').nth(1).click() + + const html = await page + .locator('.tiptap') + .first() + .evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '@Cyndi Lauper', + ) + }) + + test('should close the dropdown menu when I move the cursor outside the editor', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@') + await expect(page.locator('.dropdown-menu')).toBeVisible() + + await page.locator('.tiptap').press('Home') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + }) + + test('should close the dropdown menu when I press the escape key', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@') + await expect(page.locator('.dropdown-menu')).toBeVisible() + + await page.locator('.tiptap').press('Escape') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + }) + + test('should insert Tom Cruise when selecting his option with the arrow keys and pressing enter', async ({ + page, + }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@') + await expect(page.locator('.dropdown-menu')).toBeVisible() + + await page.locator('.tiptap').press('ArrowDown') + await page.locator('.tiptap').press('ArrowDown') + + await expect(page.locator('.dropdown-menu button').nth(2)).toHaveClass(/is-selected/) + await page.locator('.tiptap').press('Enter') + + const html = await page + .locator('.tiptap') + .first() + .evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '@Tom Cruise', + ) + }) + + test('should show "No result" when searching for a non-existent person', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@nonexistent') + + await expect(page.locator('.dropdown-menu')).toContainText('No result') + }) + + test('should only show the Madonna option when I type "@mado"', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@mado') + + await expect(page.locator('.dropdown-menu button')).toHaveCount(1) + await expect(page.locator('.dropdown-menu button').first()).toContainText('Madonna') + }) + + test('should insert Madonna when I type "@mado" and hit enter', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@mado') + await page.locator('.tiptap').press('Enter') + + const html = await page + .locator('.tiptap') + .first() + .evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '@Madonna', + ) + }) + }) + + test.describe('Movie mentions (#)', () => { + test('should insert a movie mention', async ({ page }) => { + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.commands.setContent( + '#The Matrix
', + ) + }) + + const html = await editor.evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '#The Matrix', + ) + }) + + test("should open a dropdown menu when I type '#'", async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#') + + await expect(page.locator('.dropdown-menu')).toBeVisible() + }) + + test('should display the correct movie options in the dropdown menu', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#') + + const dropdown = page.locator('.dropdown-menu') + await expect(dropdown).toBeVisible() + await expect(dropdown.locator('button')).toHaveCount(3) + + const buttons = dropdown.locator('button') + + await expect(buttons.nth(0)).toContainText('Dirty Dancing') + await expect(buttons.nth(0)).toHaveClass(/is-selected/) + await expect(buttons.nth(1)).toContainText('Pirates of the Caribbean') + await expect(buttons.nth(2)).toContainText('The Matrix') + }) + + test('should insert Pirates of the Caribbean when clicking on its option', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#') + + await page.locator('.dropdown-menu button').nth(1).click() + + const html = await page + .locator('.tiptap') + .first() + .evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '#Pirates of the Caribbean', + ) + }) + + test('should close the dropdown menu when I move the cursor outside the editor', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#') + await expect(page.locator('.dropdown-menu')).toBeVisible() + + await page.locator('.tiptap').press('Home') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + }) + + test('should close the dropdown menu when I press the escape key', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#') + await expect(page.locator('.dropdown-menu')).toBeVisible() + + await page.locator('.tiptap').press('Escape') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + }) + + test('should insert The Matrix via arrow keys and enter', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#') + await expect(page.locator('.dropdown-menu')).toBeVisible() + + await page.locator('.tiptap').press('ArrowDown') + await page.locator('.tiptap').press('ArrowDown') + + await expect(page.locator('.dropdown-menu button').nth(2)).toHaveClass(/is-selected/) + await page.locator('.tiptap').press('Enter') + + const html = await page + .locator('.tiptap') + .first() + .evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '#The Matrix', + ) + }) + + test('should show "No result" when searching for a non-existent movie', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#nonexistent') + + await expect(page.locator('.dropdown-menu')).toContainText('No result') + }) + + test('should only show Dirty Dancing when typing "#dir"', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#dir') + + await expect(page.locator('.dropdown-menu button')).toHaveCount(1) + await expect(page.locator('.dropdown-menu button').first()).toContainText('Dirty Dancing') + }) + + test('should insert Dirty Dancing when typing "#dir" and hit enter', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('#dir') + await page.locator('.tiptap').press('Enter') + + const html = await page + .locator('.tiptap') + .first() + .evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '#Dirty Dancing', + ) + }) + }) + + test.describe('Interaction between mention types', () => { + test('should support both mention types in the same document', async ({ page }) => { + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.commands.setContent( + '@Madonna starred in #Dirty Dancing
', + ) + }) + + const html = await editor.evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain( + '@Madonna', + ) + expect(html).toContain( + '#Dirty Dancing', + ) + }) + + test('should allow switching between mention types', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@') + await expect(page.locator('.dropdown-menu')).toBeVisible() + await expect(page.locator('.dropdown-menu button').first()).toContainText('Lea Thompson') + + await page.locator('.tiptap').press('Home') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + + await clearEditor(page) + await page.locator('.tiptap').type('#') + await expect(page.locator('.dropdown-menu')).toBeVisible() + await expect(page.locator('.dropdown-menu button').first()).toContainText('Dirty Dancing') + }) + + test('should insert both types of mentions in sequence', async ({ page }) => { + await clearEditor(page) + await page.locator('.tiptap').type('@mado') + await page.locator('.tiptap').press('Enter') + await page.locator('.tiptap').type(' likes #the') + await page.locator('.tiptap').press('Enter') + + const html = await page + .locator('.tiptap') + .first() + .evaluate((el: HTMLElement) => el.innerHTML) + + expect(html).toContain('@Madonna') + expect(html).toContain('#The Matrix') + }) + }) + }) + }) +}) diff --git a/demos/src/Examples/NodePos/React/index.spec.js b/demos/src/Examples/NodePos/React/index.spec.js deleted file mode 100644 index d73dfb5de0..0000000000 --- a/demos/src/Examples/NodePos/React/index.spec.js +++ /dev/null @@ -1,139 +0,0 @@ -context('/src/Examples/NodePos/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/NodePos/React/') - }) - - it('should get paragraphs', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-paragraphs"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 16) - }) - }) - - it('should get list items', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-listitems"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 12) - }) - }) - - it('should get bullet lists', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-bulletlists"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 3) - }) - }) - - it('should get ordered lists', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-orderedlists"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - }) - }) - - it('should get blockquotes', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-blockquotes"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 3) - }) - }) - - it('should get images', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-images"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 4) - }) - }) - - it('should get first blockquote', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-first-blockquote"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]') - .should('contain', 'Here we have a paragraph inside a blockquote.') - .should('not.contain', 'Here we have another paragraph inside a blockquote.') - }) - }) - - describe('when querying by attribute', () => { - it('should get square image', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-squared-image"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/200x200') - }) - }) - - it('should get landsape image', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-landscape-image"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/260x200') - }) - }) - - it('should get all landscape images', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-all-landscape-images"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 2) - cy.get('div[data-testid="found-node"]').eq(0).should('contain', 'https://placehold.co/260x200') - cy.get('div[data-testid="found-node"]').eq(1).should('contain', 'https://placehold.co/260x200') - }) - }) - - it('should get first landscape image with querySelectorAll', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-first-landscape-image-with-all-query"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/260x200') - }) - }) - - it('should get portrait image inside blockquote', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-portrait-image-inside-blockquote"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]').should('contain', 'https://placehold.co/100x200') - }) - }) - }) - - it('should find complex nodes', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-first-node"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]').should('contain', 'heading').should('contain', '{"level":1}') - - cy.get('button[data-testid="find-last-node"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]').should('contain', 'image') - - cy.get('button[data-testid="find-last-node-of-first-bullet-list"]').click() - cy.get('div[data-testid="found-nodes"]').should('exist') - cy.get('div[data-testid="found-node"]').should('have.length', 1) - cy.get('div[data-testid="found-node"]').should('contain', 'listItem').should('contain', 'Unsorted 3') - }) - }) - - it('should not find nodes that do not exist in document', () => { - cy.get('.tiptap').then(() => { - cy.get('button[data-testid="find-nonexistent-node"]').click() - cy.get('div[data-testid="found-nodes"]').should('not.exist') - cy.get('div[data-testid="found-node"]').should('have.length', 0) - }) - }) -}) diff --git a/demos/src/Examples/Performance/React/index.spec.js b/demos/src/Examples/Performance/React/index.spec.js deleted file mode 100644 index 89eb4b510f..0000000000 --- a/demos/src/Examples/Performance/React/index.spec.js +++ /dev/null @@ -1,12 +0,0 @@ -context('/src/Examples/Performance/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Performance/React/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) -}) diff --git a/demos/src/Examples/ResizableImages/React/index.spec.js b/demos/src/Examples/ResizableImages/React/index.spec.js deleted file mode 100644 index 83a4c77c4d..0000000000 --- a/demos/src/Examples/ResizableImages/React/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Nodes/Image/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Image/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should add an img tag with the correct URL', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('foobar.png') - - cy.get('button:first').click() - - cy.window().its('prompt').should('be.called') - - cy.get('.tiptap').find('img').should('have.attr', 'src', 'foobar.png') - }) - }) -}) diff --git a/demos/src/Examples/ResizableImages/Vue/index.spec.js b/demos/src/Examples/ResizableImages/Vue/index.spec.js deleted file mode 100644 index 6ff06dd117..0000000000 --- a/demos/src/Examples/ResizableImages/Vue/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Nodes/Image/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Image/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should add an img tag with the correct URL', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('foobar.png') - - cy.get('button:first').click() - - cy.window().its('prompt').should('be.called') - - cy.get('.tiptap').find('img').should('have.attr', 'src', 'foobar.png') - }) - }) -}) diff --git a/demos/src/Examples/Savvy/React/index.spec.js b/demos/src/Examples/Savvy/React/index.spec.js deleted file mode 100644 index 4731809c17..0000000000 --- a/demos/src/Examples/Savvy/React/index.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -context('/src/Examples/Savvy/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Savvy/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - const tests = [ - ['(c)', '©'], - ['->', '→'], - ['>>', '»'], - ['1/2', '½'], - ['!=', '≠'], - ['--', '—'], - ['1x1', '1×1'], - [':-) ', '🙂'], - ['<3 ', '❤️'], - ['>:P ', '😜'], - ] - - tests.forEach(test => { - it(`should parse ${test[0]} correctly`, () => { - cy.get('.tiptap').type(`${test[0]} `).should('contain', test[1]) - }) - }) - - it('should parse hex colors correctly', () => { - cy.get('.tiptap').type('#FD9170').find('.color') - }) -}) diff --git a/demos/src/Examples/Savvy/Vue/index.spec.js b/demos/src/Examples/Savvy/Vue/index.spec.js deleted file mode 100644 index 6254388279..0000000000 --- a/demos/src/Examples/Savvy/Vue/index.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -context('/src/Examples/Savvy/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Savvy/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - const tests = [ - ['(c)', '©'], - ['->', '→'], - ['>>', '»'], - ['1/2', '½'], - ['!=', '≠'], - ['--', '—'], - ['1x1', '1×1'], - [':-) ', '🙂'], - ['<3 ', '❤️'], - ['>:P ', '😜'], - ] - - tests.forEach(test => { - it(`should parse ${test[0]} correctly`, () => { - cy.get('.tiptap').type(`${test[0]} `).should('contain', test[1]) - }) - }) - - it('should parse hex colors correctly', () => { - cy.get('.tiptap').type('#FD9170').find('.color') - }) -}) diff --git a/demos/src/Examples/Savvy/index.spec.ts b/demos/src/Examples/Savvy/index.spec.ts new file mode 100644 index 0000000000..0ceddaf144 --- /dev/null +++ b/demos/src/Examples/Savvy/index.spec.ts @@ -0,0 +1,57 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Savvy' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +const cases: [string, string][] = [ + ['(c)', '©'], + ['->', '→'], + ['>>', '»'], + ['1/2', '½'], + ['!=', '≠'], + ['--', '—'], + ['1x1', '1×1'], + [':-) ', '🙂'], + ['<3 ', '❤️'], + ['>:P ', '😜'], +] + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.chain().focus().clearContent().run() + }) + }) + + cases.forEach(([input, expected]) => { + test(`should parse ${input} correctly`, async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type(`${input} `) + + await expect(page.locator('.tiptap')).toContainText(expected) + }) + }) + + test('should parse hex colors correctly', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('#FD9170') + + await expect(page.locator('.tiptap .color').first()).toBeVisible() + }) + }) + }) +}) diff --git a/demos/src/Examples/StaticRendering/React/index.spec.js b/demos/src/Examples/StaticRendering/React/index.spec.js deleted file mode 100644 index fab3fcfed0..0000000000 --- a/demos/src/Examples/StaticRendering/React/index.spec.js +++ /dev/null @@ -1,12 +0,0 @@ -context('/src/Examples/StaticRendering/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/StaticRendering/React/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) -}) diff --git a/demos/src/Examples/StaticRenderingAdvanced/React/index.spec.js b/demos/src/Examples/StaticRenderingAdvanced/React/index.spec.js deleted file mode 100644 index 2e47998b2f..0000000000 --- a/demos/src/Examples/StaticRenderingAdvanced/React/index.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -context('/src/Examples/StaticRenderingAdvanced/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/StaticRenderingAdvanced/React/') - }) - - it('should render the content as HTML', () => { - cy.get('p').should('exist') - }) -}) diff --git a/demos/src/Examples/Tables/React/index.spec.js b/demos/src/Examples/Tables/React/index.spec.js deleted file mode 100644 index 852d276c61..0000000000 --- a/demos/src/Examples/Tables/React/index.spec.js +++ /dev/null @@ -1,103 +0,0 @@ -context('/src/Examples/Tables/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Tables/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - cy.get('button').contains('Insert table').click() - }) - }) - - it('adds a table with three columns and three rows', () => { - cy.get('.tiptap table').should('exist') - - cy.get('.tiptap table tr').should('exist').should('have.length', 3) - - cy.get('.tiptap table th').should('exist').should('have.length', 3) - - cy.get('.tiptap table td').should('exist').should('have.length', 6) - }) - - it('adds & delete columns', () => { - cy.get('button').contains('Add column before').click() - cy.get('.tiptap table th').should('have.length', 4) - - cy.get('button').contains('Add column after').click() - cy.get('.tiptap table th').should('have.length', 5) - - cy.get('button').contains('Delete column').click().click() - cy.get('.tiptap table th').should('have.length', 3) - }) - - it('adds & delete rows', () => { - cy.get('button').contains('Add row before').click() - cy.get('.tiptap table tr').should('have.length', 4) - - cy.get('button').contains('Add row after').click() - cy.get('.tiptap table tr').should('have.length', 5) - - cy.get('button').contains('Delete row').click().click() - cy.get('.tiptap table tr').should('have.length', 3) - }) - - it('should delete table', () => { - cy.get('button').contains('Delete table').click() - cy.get('.tiptap table').should('not.exist') - }) - - it('should merge cells', () => { - cy.get('.tiptap').type('{shift}{rightArrow}') - cy.get('button').contains('Merge cells').click() - cy.get('.tiptap table th').should('have.length', 2) - }) - - it('should split cells', () => { - cy.get('.tiptap').type('{shift}{rightArrow}') - cy.get('button').contains('Merge cells').click() - cy.get('.tiptap table th').should('have.length', 2) - cy.get('button').contains('Split cell').click() - cy.get('.tiptap table th').should('have.length', 3) - }) - - it('should toggle header columns', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.toggleHeaderColumn() - cy.get('.tiptap table th').should('have.length', 5) - }) - }) - - it('should toggle header row', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.toggleHeaderRow() - cy.get('.tiptap table th').should('have.length', 0) - }) - }) - - it('should merge split', () => { - cy.get('.tiptap').type('{shift}{rightArrow}') - cy.get('button').contains('Merge cells').click() - cy.get('.tiptap th[colspan="2"]').should('exist') - cy.get('button').contains('Merge or split').click() - cy.get('.tiptap th[colspan="2"]').should('not.exist') - }) - - it('should set cell attribute', () => { - cy.get('.tiptap').type('{downArrow}') - cy.get('button').contains('Set cell attribute').click() - cy.get('.tiptap table td[style]').should('have.attr', 'style', 'background-color: #FAF594') - }) - - it('should move focus to next or prev cell', () => { - cy.get('.tiptap').type('Column 1') - cy.get('button').contains('Go to next cell').click() - cy.get('.tiptap').type('Column 2') - cy.get('button').contains('Go to previous cell').click() - - cy.get('.tiptap th').then(elements => { - expect(elements[0].innerText).to.equal('Column 1') - expect(elements[1].innerText).to.equal('Column 2') - }) - }) -}) diff --git a/demos/src/Examples/Tables/Vue/index.spec.js b/demos/src/Examples/Tables/Vue/index.spec.js deleted file mode 100644 index 2d6bb047c0..0000000000 --- a/demos/src/Examples/Tables/Vue/index.spec.js +++ /dev/null @@ -1,103 +0,0 @@ -context('/src/Examples/Tables/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Tables/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - cy.get('button').contains('Insert table').click() - }) - }) - - it('adds a table with three columns and three rows', () => { - cy.get('.tiptap table').should('exist') - - cy.get('.tiptap table tr').should('exist').should('have.length', 3) - - cy.get('.tiptap table th').should('exist').should('have.length', 3) - - cy.get('.tiptap table td').should('exist').should('have.length', 6) - }) - - it('adds & delete columns', () => { - cy.get('button').contains('Add column before').click() - cy.get('.tiptap table th').should('have.length', 4) - - cy.get('button').contains('Add column after').click() - cy.get('.tiptap table th').should('have.length', 5) - - cy.get('button').contains('Delete column').click().click() - cy.get('.tiptap table th').should('have.length', 3) - }) - - it('adds & delete rows', () => { - cy.get('button').contains('Add row before').click() - cy.get('.tiptap table tr').should('have.length', 4) - - cy.get('button').contains('Add row after').click() - cy.get('.tiptap table tr').should('have.length', 5) - - cy.get('button').contains('Delete row').click().click() - cy.get('.tiptap table tr').should('have.length', 3) - }) - - it('should delete table', () => { - cy.get('button').contains('Delete table').click() - cy.get('.tiptap table').should('not.exist') - }) - - it('should merge cells', () => { - cy.get('.tiptap').type('{shift}{rightArrow}') - cy.get('button').contains('Merge cells').click() - cy.get('.tiptap table th').should('have.length', 2) - }) - - it('should split cells', () => { - cy.get('.tiptap').type('{shift}{rightArrow}') - cy.get('button').contains('Merge cells').click() - cy.get('.tiptap table th').should('have.length', 2) - cy.get('button').contains('Split cell').click() - cy.get('.tiptap table th').should('have.length', 3) - }) - - it('should toggle header columns', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.toggleHeaderColumn() - cy.get('.tiptap table th').should('have.length', 5) - }) - }) - - it('should toggle header row', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.toggleHeaderRow() - cy.get('.tiptap table th').should('have.length', 0) - }) - }) - - it('should merge split', () => { - cy.get('.tiptap').type('{shift}{rightArrow}') - cy.get('button').contains('Merge cells').click() - cy.get('.tiptap th[colspan="2"]').should('exist') - cy.get('button').contains('Merge or split').click() - cy.get('.tiptap th[colspan="2"]').should('not.exist') - }) - - it('should set cell attribute', () => { - cy.get('.tiptap').type('{downArrow}') - cy.get('button').contains('Set cell attribute').click() - cy.get('.tiptap table td[style]').should('have.attr', 'style', 'background-color: #FAF594') - }) - - it('should move focus to next or prev cell', () => { - cy.get('.tiptap').type('Column 1') - cy.get('button').contains('Go to next cell').click() - cy.get('.tiptap').type('Column 2') - cy.get('button').contains('Go to previous cell').click() - - cy.get('.tiptap th').then(elements => { - expect(elements[0].innerText).to.equal('Column 1') - expect(elements[1].innerText).to.equal('Column 2') - }) - }) -}) diff --git a/demos/src/Examples/Tasks/React/index.spec.js b/demos/src/Examples/Tasks/React/index.spec.js deleted file mode 100644 index 9c55adec45..0000000000 --- a/demos/src/Examples/Tasks/React/index.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -context('/src/Examples/Tasks/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Tasks/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should always use task items', () => { - cy.get('.tiptap input[type="checkbox"]').should('have.length', 1) - }) - - it('should create new tasks', () => { - cy.get('.tiptap').type('Cook food{enter}Eat food{enter}Clean dishes') - cy.get('.tiptap input[type="checkbox"]').should('have.length', 3) - }) - - it('should check and uncheck tasks on click', () => { - cy.get('.tiptap').type('Cook food{enter}Eat food{enter}Clean dishes') - cy.get('.tiptap').find('input[type="checkbox"]').eq(0).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 1) - cy.get('.tiptap').find('input[type="checkbox"]').eq(1).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 2) - cy.get('.tiptap').find('input[type="checkbox"]').eq(0).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 1) - cy.get('.tiptap').find('input[type="checkbox"]').eq(1).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 0) - }) -}) diff --git a/demos/src/Examples/Tasks/Vue/index.spec.js b/demos/src/Examples/Tasks/Vue/index.spec.js deleted file mode 100644 index d2d97e41f3..0000000000 --- a/demos/src/Examples/Tasks/Vue/index.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -context('/src/Examples/Tasks/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Tasks/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should always use task items', () => { - cy.get('.tiptap input[type="checkbox"]').should('have.length', 1) - }) - - it('should create new tasks', () => { - cy.get('.tiptap').type('Cook food{enter}Eat food{enter}Clean dishes') - cy.get('.tiptap input[type="checkbox"]').should('have.length', 3) - }) - - it('should check and uncheck tasks on click', () => { - cy.get('.tiptap').type('Cook food{enter}Eat food{enter}Clean dishes') - cy.get('.tiptap').find('input[type="checkbox"]').eq(0).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 1) - cy.get('.tiptap').find('input[type="checkbox"]').eq(1).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 2) - cy.get('.tiptap').find('input[type="checkbox"]').eq(0).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 1) - cy.get('.tiptap').find('input[type="checkbox"]').eq(1).click({ force: true }) - cy.get('.tiptap').find('input[type="checkbox"]:checked').should('have.length', 0) - }) -}) diff --git a/demos/src/Examples/Tasks/index.spec.ts b/demos/src/Examples/Tasks/index.spec.ts new file mode 100644 index 0000000000..3e2c3c7107 --- /dev/null +++ b/demos/src/Examples/Tasks/index.spec.ts @@ -0,0 +1,66 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Tasks' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + + await editor.evaluate((el: any) => { + el.editor.commands.clearContent() + }) + }) + + test('should always use task items', async ({ page }) => { + await expect(page.locator('.tiptap input[type="checkbox"]')).toHaveCount(1) + }) + + test('should create new tasks', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('Cook food') + await editor.press('Enter') + await editor.type('Eat food') + await editor.press('Enter') + await editor.type('Clean dishes') + + await expect(page.locator('.tiptap input[type="checkbox"]')).toHaveCount(3) + }) + + test('should check and uncheck tasks on click', async ({ page }) => { + const editor = await getEditor(page) + + await editor.click() + await editor.type('Cook food') + await editor.press('Enter') + await editor.type('Eat food') + await editor.press('Enter') + await editor.type('Clean dishes') + + const checkboxes = page.locator('.tiptap input[type="checkbox"]') + + await checkboxes.nth(0).check({ force: true }) + await expect(page.locator('.tiptap input[type="checkbox"]:checked')).toHaveCount(1) + + await checkboxes.nth(1).check({ force: true }) + await expect(page.locator('.tiptap input[type="checkbox"]:checked')).toHaveCount(2) + + await checkboxes.nth(0).uncheck({ force: true }) + await expect(page.locator('.tiptap input[type="checkbox"]:checked')).toHaveCount(1) + + await checkboxes.nth(1).uncheck({ force: true }) + await expect(page.locator('.tiptap input[type="checkbox"]:checked')).toHaveCount(0) + }) + }) + }) +}) diff --git a/demos/src/Examples/TextDirection/React/index.spec.js b/demos/src/Examples/TextDirection/React/index.spec.js deleted file mode 100644 index b30860dac6..0000000000 --- a/demos/src/Examples/TextDirection/React/index.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -context('/src/Examples/TextDirection/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/TextDirection/React/') - }) - - it('should apply text direction attributes', () => { - cy.get('.tiptap p').first().should('have.attr', 'dir', 'auto') - }) - - it('should change global direction', () => { - cy.get('button').contains('RTL').click() - cy.get('.tiptap p').first().should('have.attr', 'dir', 'rtl') - }) - - it('should set direction on selection', () => { - cy.get('.tiptap p').first().click() - cy.get('button').contains('Set LTR').click() - cy.get('.tiptap p').first().should('have.attr', 'dir', 'ltr') - }) - - it('should unset direction', () => { - cy.get('button').contains('None').click() - cy.get('.tiptap p').first().should('not.have.attr', 'dir') - }) -}) diff --git a/demos/src/Examples/Transition/Vue/index.spec.js b/demos/src/Examples/Transition/Vue/index.spec.js deleted file mode 100644 index 39100317df..0000000000 --- a/demos/src/Examples/Transition/Vue/index.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -context('/src/Examples/Transition/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Transition/Vue/') - }) - - it('should have two buttons and no active tiptap instance', () => { - cy.get('.tiptap').should('not.exist') - - cy.get('#toggle-direct-editor').should('exist') - cy.get('#toggle-nested-editor').should('exist') - }) - - it('clicking the buttons should show two editors', () => { - cy.get('#toggle-direct-editor').click() - cy.get('#toggle-nested-editor').click() - - cy.get('.tiptap').should('exist') - cy.get('.tiptap').should('be.visible') - }) - - it('clicking the buttons again should hide the editors', () => { - cy.get('#toggle-direct-editor').click() - cy.get('#toggle-nested-editor').click() - - cy.get('.tiptap').should('exist') - cy.get('.tiptap').should('be.visible') - - cy.get('#toggle-direct-editor').click() - cy.get('#toggle-nested-editor').click() - - cy.get('.tiptap').should('not.exist') - }) -}) diff --git a/demos/src/Examples/TypographyRTL/React/index.spec.js b/demos/src/Examples/TypographyRTL/React/index.spec.js deleted file mode 100644 index 6e88f08c6a..0000000000 --- a/demos/src/Examples/TypographyRTL/React/index.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -context('/src/Examples/TypographyRTL/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/TypographyRTL/React/') - }) - - describe('Automatic RTL detection', () => { - beforeEach(() => { - cy.get('.editor-auto .tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should use RTL double quotes when textDirection is rtl', () => { - cy.get('.editor-auto .tiptap').type('"hello"').should('contain', '”hello“') - }) - - it('should use RTL single quotes when textDirection is rtl', () => { - cy.get('.editor-auto .tiptap').type("'world'").should('contain', '’world‘') - }) - }) - - describe('Explicit RTL configuration', () => { - beforeEach(() => { - cy.get('.editor-explicit .tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should use RTL double quotes when configured', () => { - cy.get('.editor-explicit .tiptap').type('"hello"').should('contain', '”hello“') - }) - - it('should use RTL single quotes when configured', () => { - cy.get('.editor-explicit .tiptap').type("'world'").should('contain', '’world‘') - }) - }) -}) diff --git a/demos/src/Examples/TypographyRTL/index.spec.ts b/demos/src/Examples/TypographyRTL/index.spec.ts new file mode 100644 index 0000000000..7aa1159b52 --- /dev/null +++ b/demos/src/Examples/TypographyRTL/index.spec.ts @@ -0,0 +1,76 @@ +import { expect, test } from '@playwright/test' + +const demoName = 'TypographyRTL' +const frameworkPaths = ['React'] +const demoPath = '/src/Examples' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await page.locator('.editor-auto .tiptap').first().waitFor() + }) + + test.describe('Automatic RTL detection', () => { + test.beforeEach(async ({ page }) => { + await page + .locator('.editor-auto .tiptap') + .first() + .evaluate((el: any) => { + el.editor.commands.clearContent() + }) + }) + + test('should use RTL double quotes when textDirection is rtl', async ({ page }) => { + const editor = page.locator('.editor-auto .tiptap').first() + + await editor.click() + await editor.type('"hello"') + + await expect(editor).toContainText('”hello“') + }) + + test('should use RTL single quotes when textDirection is rtl', async ({ page }) => { + const editor = page.locator('.editor-auto .tiptap').first() + + await editor.click() + await editor.type("'world'") + + await expect(editor).toContainText('’world‘') + }) + }) + + test.describe('Explicit RTL configuration', () => { + test.beforeEach(async ({ page }) => { + await page + .locator('.editor-explicit .tiptap') + .first() + .evaluate((el: any) => { + el.editor.commands.clearContent() + }) + }) + + test('should use RTL double quotes when configured', async ({ page }) => { + const editor = page.locator('.editor-explicit .tiptap').first() + + await editor.click() + await editor.type('"hello"') + + await expect(editor).toContainText('”hello“') + }) + + test('should use RTL single quotes when configured', async ({ page }) => { + const editor = page.locator('.editor-explicit .tiptap').first() + + await editor.click() + await editor.type("'world'") + + await expect(editor).toContainText('’world‘') + }) + }) + }) + }) +}) diff --git a/demos/src/Experiments/CollaborationAnnotation/Vue/index.spec.js b/demos/src/Experiments/CollaborationAnnotation/Vue/index.spec.js deleted file mode 100644 index a34c1b6cef..0000000000 --- a/demos/src/Experiments/CollaborationAnnotation/Vue/index.spec.js +++ /dev/null @@ -1,70 +0,0 @@ -context('/src/Experiments/CollaborationAnnotation/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Experiments/CollaborationAnnotation/Vue/') - }) - - /* it('renders two editors', () => { - cy.get('.tiptap').should('have.length', 2) - }) */ - - // TODO: Fix those tests in the future - // Current problem is that tiptap seems to mismatch a transformation somewhere inside those tests - // So to fix this, we should look for a different way to simulate the annotation process - - /* it('sets an annotation in editor 1 and shows annotations in both editors', () => { - cy.window().then(win => { - cy.stub(win, 'prompt', () => 'This is a test comment') - cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}') - cy.get('button').contains('Comment').eq(0).click() - cy.get('.editor-1 .tiptap').type('{end}') - cy.get('.tiptap .annotation').should('have.length', 2) - cy.get('.comment').should('exist').contains('This is a test comment') - }) - }) */ - - /* it('updates an existing annotation', () => { - let commentIndex = 0 - - cy.window().then(win => { - cy.stub(win, 'prompt', () => { - switch (commentIndex) { - case 0: - commentIndex += 1 - return 'This is a test comment' - - case 1: - commentIndex += 1 - return 'This is the new comment' - - default: - return '' - } - }) - - cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}') - cy.get('button').contains('Comment').eq(0).click() - cy.wait(1000) - cy.get('.editor-1 .tiptap').find('.annotation').click() - cy.get('.comment').should('exist').contains('This is a test comment') - cy.get('button').contains('Update').click() - cy.wait(1000) - cy.get('.comment').should('exist').contains('This is the new comment') - }) - }) */ - - /* it('deletes an existing annotation', () => { - cy.window().then(win => { - cy.stub(win, 'prompt', () => 'This is a test comment') - - cy.get('.editor-1 .tiptap').type('{selectall}{backspace}Hello world{selectall}') - cy.get('button').contains('Comment').eq(0).click() - cy.wait(1000) - cy.get('.editor-1 .tiptap').find('.annotation').click() - cy.get('.comment').should('exist').contains('This is a test comment') - cy.get('button').contains('Remove').click() - cy.get('.tiptap .annotation').should('not.exist') - cy.wait(1000) - cy.get('.comment').should('not.exist') - }) - }) */ -}) diff --git a/demos/src/Experiments/CollaborationAnnotation/index.spec.ts b/demos/src/Experiments/CollaborationAnnotation/index.spec.ts new file mode 100644 index 0000000000..515110c7d4 --- /dev/null +++ b/demos/src/Experiments/CollaborationAnnotation/index.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test' + +const demoName = 'CollaborationAnnotation' +const frameworkPaths = ['Vue'] +const demoPath = '/src/Experiments' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('renders two editors', async ({ page }) => { + await expect(page.locator('.tiptap')).toHaveCount(2) + }) + }) + }) +}) diff --git a/demos/src/Experiments/Commands/Vue/index.spec.js b/demos/src/Experiments/Commands/Vue/index.spec.js deleted file mode 100644 index 4e864c8b45..0000000000 --- a/demos/src/Experiments/Commands/Vue/index.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -context('/src/Experiments/Commands/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Experiments/Commands/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should open a popup after typing a slash', () => { - const items = [{ tag: 'h1' }, { tag: 'h2' }, { tag: 'strong' }, { tag: 'em' }] - - items.forEach((item, i) => { - cy.get('.tiptap').type('{selectall}{backspace}/') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button').eq(i).click() - cy.get('.tiptap').type(`I am a ${item.tag}`) - cy.get(`.tiptap ${item.tag}`).should('exist').should('have.text', `I am a ${item.tag}`) - }) - }) - - it('should close the popup without any command via esc', () => { - cy.get('.tiptap').type('{selectall}{backspace}/') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{esc}') - cy.get('.dropdown-menu').should('not.exist') - }) - - it('should open the popup when the cursor is after a slash', () => { - cy.get('.tiptap').type('{selectall}{backspace}/') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{leftArrow}') - cy.get('.dropdown-menu').should('not.exist') - cy.get('.tiptap').type('{rightArrow}') - cy.get('.dropdown-menu').should('exist') - }) -}) diff --git a/demos/src/Experiments/Commands/index.spec.ts b/demos/src/Experiments/Commands/index.spec.ts new file mode 100644 index 0000000000..2e3df1fb5c --- /dev/null +++ b/demos/src/Experiments/Commands/index.spec.ts @@ -0,0 +1,58 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Commands' +const frameworkPaths = ['Vue'] +const demoPath = '/src/Experiments' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + }) + + test('opens a popup after typing a slash and inserts the chosen item', async ({ page }) => { + const items = [{ tag: 'h1' }, { tag: 'h2' }, { tag: 'strong' }, { tag: 'em' }] + const editor = await getEditor(page) + const dropdown = page.locator('.dropdown-menu') + + await items.reduce(async (prev, item, i) => { + await prev + await editor.click() + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.type('/') + await expect(dropdown).toBeVisible() + await dropdown.locator('button').nth(i).click() + await editor.evaluate((el: any, text: string) => el.editor.commands.insertContent(text), `I am a ${item.tag}`) + await expect(page.locator(`.tiptap ${item.tag}`)).toHaveText(`I am a ${item.tag}`) + }, Promise.resolve()) + }) + + test('closes the popup without any command via esc', async ({ page }) => { + const editor = await getEditor(page) + await editor.click() + await editor.type('/') + await expect(page.locator('.dropdown-menu')).toBeVisible() + await page.keyboard.press('Escape') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + }) + + test('opens the popup when the cursor is after a slash', async ({ page }) => { + const editor = await getEditor(page) + await editor.click() + await editor.type('/') + await expect(page.locator('.dropdown-menu')).toBeVisible() + await page.keyboard.press('ArrowLeft') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + await page.keyboard.press('ArrowRight') + await expect(page.locator('.dropdown-menu')).toBeVisible() + }) + }) + }) +}) diff --git a/demos/src/Experiments/IsolatingClear/React/index.spec.js b/demos/src/Experiments/IsolatingClear/React/index.spec.js deleted file mode 100644 index 087d1b1100..0000000000 --- a/demos/src/Experiments/IsolatingClear/React/index.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -context('/src/Experiments/IsolatingClear/React/', () => { - beforeEach(() => { - cy.visit('/src/Experiments/IsolatingClear/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set the background color of the selected text', () => { - cy.get('[data-testid="setPurple"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'background-color: #958DF1') - }) - - it('should remove the background color of the selected text', () => { - cy.get('[data-testid="setPurple"]').click() - - cy.get('.tiptap span').should('exist') - - cy.get('[data-testid="unsetBackgroundColor"]').click() - - cy.get('.tiptap span').should('not.exist') - }) - - it('should change background color with color picker', () => { - cy.get('input[type=color]').invoke('val', '#ff0000').trigger('input') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'background-color: #ff0000') - }) - - it('should match background color and color picker color values', () => { - cy.get('[data-testid="setPurple"]').click() - - cy.get('input[type=color]').should('have.value', '#958df1') - }) - - it('should preserve background color on new lines', () => { - cy.get('[data-testid="setPurple"]').click() - cy.get('.ProseMirror').type('Example Text{enter}') - - cy.get('[data-testid="setPurple"]').should('have.class', 'is-active') - }) - - it('should unset background color on new lines after unset clicked', () => { - cy.get('[data-testid="setPurple"]').click() - cy.get('.ProseMirror').type('Example Text{enter}') - cy.get('[data-testid="unsetBackgroundColor"]').click() - cy.get('.ProseMirror').type('Example Text') - - cy.get('[data-testid="setPurple"]').should('not.have.class', 'is-active') - }) -}) diff --git a/demos/src/Extensions/BackgroundColor/Vue/index.spec.js b/demos/src/Extensions/BackgroundColor/Vue/index.spec.js deleted file mode 100644 index f11565066f..0000000000 --- a/demos/src/Extensions/BackgroundColor/Vue/index.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -context('/src/Extensions/BackgroundColor/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/BackgroundColor/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set the background color of the selected text', () => { - cy.get('[data-testid="setPurple"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'background-color: #958DF1') - }) - - it('should remove the background color of the selected text', () => { - cy.get('[data-testid="setPurple"]').click() - - cy.get('.tiptap span').should('exist') - - cy.get('[data-testid="unsetBackgroundColor"]').click() - - cy.get('.tiptap span').should('not.exist') - }) - - it('should change background color with color picker', () => { - cy.get('input[type=color]').invoke('val', '#ff0000').trigger('input') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'background-color: #ff0000') - }) - - it('should match background color and color picker color values', () => { - cy.get('[data-testid="setPurple"]').click() - - cy.get('input[type=color]').should('have.value', '#958df1') - }) - - it('should preserve background color on new lines', () => { - cy.get('[data-testid="setPurple"]').click() - cy.get('.ProseMirror').type('Example Text{enter}') - - cy.get('[data-testid="setPurple"]').should('have.class', 'is-active') - }) - - it('should unset background color on new lines after unset clicked', () => { - cy.get('[data-testid="setPurple"]').click() - cy.get('.ProseMirror').type('Example Text{enter}') - cy.get('[data-testid="unsetBackgroundColor"]').click() - cy.get('.ProseMirror').type('Example Text') - - cy.get('[data-testid="setPurple"]').should('not.have.class', 'is-active') - }) -}) diff --git a/demos/src/Extensions/Collaboration/React/index.spec.js b/demos/src/Extensions/Collaboration/React/index.spec.js deleted file mode 100644 index 51140ce7eb..0000000000 --- a/demos/src/Extensions/Collaboration/React/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Extensions/Collaboration/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Collaboration/React/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should have a ydoc', () => { - cy.get('.tiptap').then(([{ editor }]) => { - /** - * @type {import('yjs').Doc} - */ - const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document - - // eslint-disable-next-line - expect(yDoc).to.not.be.null - }) - }) -}) diff --git a/demos/src/Extensions/Collaboration/Vue/index.spec.js b/demos/src/Extensions/Collaboration/Vue/index.spec.js deleted file mode 100644 index 4643167bfe..0000000000 --- a/demos/src/Extensions/Collaboration/Vue/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Extensions/Collaboration/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Collaboration/Vue/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should have a ydoc', () => { - cy.get('.tiptap').then(([{ editor }]) => { - /** - * @type {import('yjs').Doc} - */ - const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document - - // eslint-disable-next-line - expect(yDoc).to.not.be.null - }) - }) -}) diff --git a/demos/src/Extensions/CollaborationCaret/React/index.spec.js b/demos/src/Extensions/CollaborationCaret/React/index.spec.js deleted file mode 100644 index 8845449a6d..0000000000 --- a/demos/src/Extensions/CollaborationCaret/React/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Extensions/CollaborationCaret/React', () => { - beforeEach(() => { - cy.visit('/src/Extensions/CollaborationCaret/React/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should have a ydoc', () => { - cy.get('.tiptap').then(([{ editor }]) => { - /** - * @type {import('yjs').Doc} - */ - const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document - - // eslint-disable-next-line - expect(yDoc).to.not.be.null - }) - }) -}) diff --git a/demos/src/Extensions/CollaborationCaret/Vue/index.spec.js b/demos/src/Extensions/CollaborationCaret/Vue/index.spec.js deleted file mode 100644 index 8e2f9a1c40..0000000000 --- a/demos/src/Extensions/CollaborationCaret/Vue/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Extensions/CollaborationCaret/Vue', () => { - beforeEach(() => { - cy.visit('/src/Extensions/CollaborationCaret/Vue/') - }) - - it('should have a working tiptap instance', () => { - cy.get('.tiptap').then(([{ editor }]) => { - // eslint-disable-next-line - expect(editor).to.not.be.null - }) - }) - - it('should have a ydoc', () => { - cy.get('.tiptap').then(([{ editor }]) => { - /** - * @type {import('yjs').Doc} - */ - const yDoc = editor.extensionManager.extensions.find(a => a.name === 'collaboration').options.document - - // eslint-disable-next-line - expect(yDoc).to.not.be.null - }) - }) -}) diff --git a/demos/src/Extensions/CollaborationWithMenus/React/index.spec.js b/demos/src/Extensions/CollaborationWithMenus/React/index.spec.js deleted file mode 100644 index 20e367cad5..0000000000 --- a/demos/src/Extensions/CollaborationWithMenus/React/index.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -///Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set the color of the selected text', () => { - cy.get('[data-testid="setPurple"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'color: #958DF1') - }) - - it('should remove the color of the selected text', () => { - cy.get('[data-testid="setPurple"]').click() - - cy.get('.tiptap span').should('exist') - - cy.get('[data-testid="unsetColor"]').click() - - cy.get('.tiptap span').should('not.exist') - }) - - it('should change text color with color picker', () => { - cy.get('input[type=color]').invoke('val', '#ff0000').trigger('input') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'color: #ff0000') - }) - - it('should match text and color picker color values', () => { - cy.get('[data-testid="setPurple"]').click() - - cy.get('input[type=color]').should('have.value', '#958df1') - }) - - it('should preserve color on new lines', () => { - cy.get('[data-testid="setPurple"]').click() - cy.get('.ProseMirror').type('Example Text{enter}') - - cy.get('[data-testid="setPurple"]').should('have.class', 'is-active') - }) - - it('should unset color on new lines after unset clicked', () => { - cy.get('[data-testid="setPurple"]').click() - cy.get('.ProseMirror').type('Example Text{enter}') - cy.get('[data-testid="unsetColor"]').click() - cy.get('.ProseMirror').type('Example Text') - - cy.get('[data-testid="setPurple"]').should('not.have.class', 'is-active') - }) -}) diff --git a/demos/src/Extensions/Color/Vue/index.spec.js b/demos/src/Extensions/Color/Vue/index.spec.js deleted file mode 100644 index 0ed3c79e03..0000000000 --- a/demos/src/Extensions/Color/Vue/index.spec.js +++ /dev/null @@ -1,40 +0,0 @@ -context('/src/Extensions/Color/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Color/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set the color of the selected text', () => { - cy.get('button:first').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'color: #958DF1') - }) - - it('should remove the color of the selected text', () => { - cy.get('button:first').click() - - cy.get('.tiptap span').should('exist') - - cy.get('button:last').click() - - cy.get('.tiptap span').should('not.exist') - }) - - it('should change text color with color picker', () => { - cy.get('input[type=color]').invoke('val', '#ff0000').trigger('input') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'color: #ff0000') - }) - - it('should match text and color picker color values', () => { - cy.get('button:first').click() - - cy.get('input[type=color]').should('have.value', '#958df1') - }) -}) diff --git a/demos/src/Extensions/Dropcursor/React/index.spec.js b/demos/src/Extensions/Dropcursor/React/index.spec.js deleted file mode 100644 index 8ce9a2eb10..0000000000 --- a/demos/src/Extensions/Dropcursor/React/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Examples/Dropcursor/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Dropcursor/React/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Extensions/Dropcursor/Vue/index.spec.js b/demos/src/Extensions/Dropcursor/Vue/index.spec.js deleted file mode 100644 index cae05a8646..0000000000 --- a/demos/src/Extensions/Dropcursor/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Examples/Dropcursor/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Dropcursor/Vue/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Extensions/FloatingMenu/React/index.spec.js b/demos/src/Extensions/FloatingMenu/React/index.spec.js deleted file mode 100644 index 67d08e69b2..0000000000 --- a/demos/src/Extensions/FloatingMenu/React/index.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -context('/src/Extensions/FloatingMenu/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/FloatingMenu/React/') - }) - - it('should not render a floating menu on non-empty nodes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.chain().setContent('Example Text
').focus().run() - const floatingMenu = cy.get('[data-testID="floating-menu"]') - - floatingMenu.should('not.exist') - }) - }) - - it('should render a floating menu on empty nodes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.chain().setContent('').focus().run() - const floatingMenu = cy.get('[data-testID="floating-menu"]') - - floatingMenu.should('exist') - }) - }) -}) diff --git a/demos/src/Extensions/FloatingMenu/index.spec.ts b/demos/src/Extensions/FloatingMenu/index.spec.ts new file mode 100644 index 0000000000..3c1e1783fe --- /dev/null +++ b/demos/src/Extensions/FloatingMenu/index.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'FloatingMenu' +const frameworkPaths = ['React'] +const demoPath = '/src/Extensions' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('does not render a floating menu on non-empty nodes', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.chain().setContent('Example Text
').focus().run()) + await expect(page.locator('[data-testid="floating-menu"]')).toHaveCount(0) + }) + + test('renders a floating menu on empty nodes', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.chain().setContent('').focus().run()) + await expect(page.locator('[data-testid="floating-menu"]')).toBeVisible() + }) + }) + }) +}) diff --git a/demos/src/Extensions/Focus/React/index.spec.js b/demos/src/Extensions/Focus/React/index.spec.js deleted file mode 100644 index e6f0cf357b..0000000000 --- a/demos/src/Extensions/Focus/React/index.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -context('/src/Extensions/Focus/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Focus/React/') - }) - - it('should have class', () => { - cy.get('.tiptap p:first').should('have.class', 'has-focus') - }) -}) diff --git a/demos/src/Extensions/Focus/Vue/index.spec.js b/demos/src/Extensions/Focus/Vue/index.spec.js deleted file mode 100644 index 303fb498a1..0000000000 --- a/demos/src/Extensions/Focus/Vue/index.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -context('/src/Extensions/Focus/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Focus/Vue/') - }) - - it('should have class', () => { - cy.get('.tiptap p:first').should('have.class', 'has-focus') - }) -}) diff --git a/demos/src/Extensions/Focus/index.spec.ts b/demos/src/Extensions/Focus/index.spec.ts new file mode 100644 index 0000000000..b2865c021c --- /dev/null +++ b/demos/src/Extensions/Focus/index.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test' + +const demoName = 'Focus' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Extensions' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('first paragraph has has-focus class', async ({ page }) => { + await expect(page.locator('.tiptap p').first()).toHaveClass(/has-focus/) + }) + }) + }) +}) diff --git a/demos/src/Extensions/FontFamily/React/index.spec.js b/demos/src/Extensions/FontFamily/React/index.spec.js deleted file mode 100644 index 34d22df758..0000000000 --- a/demos/src/Extensions/FontFamily/React/index.spec.js +++ /dev/null @@ -1,52 +0,0 @@ -context('/src/Extensions/FontFamily/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/FontFamily/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set the font-family of the selected text', () => { - cy.get('[data-test-id="monospace"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-family: monospace') - }) - - it('should remove the font-family of the selected text', () => { - cy.get('[data-test-id="monospace"]').click() - - cy.get('.tiptap span').should('exist') - - cy.get('[data-test-id="unsetFontFamily"]').click() - - cy.get('.tiptap span').should('not.exist') - }) - - it('should allow CSS variables as a font-family', () => { - cy.get('[data-test-id="css-variable"]') - .should('not.have.class', 'is-active') - .click() - .should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-family: var(--title-font-family)') - }) - - it('should allow fonts containing multiple font families', () => { - cy.get('[data-test-id="comic-sans"]') - .should('not.have.class', 'is-active') - .click() - .should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-family: "Comic Sans MS", "Comic Sans"') - }) - - it('should allow fonts containing a space and number as a font-family', () => { - cy.get('[data-test-id="exo2"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-family: "Exo 2"') - }) -}) diff --git a/demos/src/Extensions/FontFamily/Vue/index.spec.js b/demos/src/Extensions/FontFamily/Vue/index.spec.js deleted file mode 100644 index 890d7d82c2..0000000000 --- a/demos/src/Extensions/FontFamily/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Extensions/FontFamily/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/FontFamily/Vue/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Extensions/FontSize/React/index.spec.js b/demos/src/Extensions/FontSize/React/index.spec.js deleted file mode 100644 index 56adcb4b61..0000000000 --- a/demos/src/Extensions/FontSize/React/index.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -context('/src/Extensions/FontSize/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/FontSize/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set the font-size of the selected text', () => { - cy.get('[data-test-id="28px"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-size: 28px') - }) - - it('should remove the font-size of the selected text', () => { - cy.get('[data-test-id="28px"]').click() - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-size: 28px') - cy.get('[data-test-id="unsetFontSize"]').click() - cy.get('.tiptap').get('span').should('not.exist') - }) -}) diff --git a/demos/src/Extensions/FontSize/Vue/index.spec.js b/demos/src/Extensions/FontSize/Vue/index.spec.js deleted file mode 100644 index 426c28345b..0000000000 --- a/demos/src/Extensions/FontSize/Vue/index.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -context('/src/Extensions/FontSize/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/FontSize/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set the font-size of the selected text', () => { - cy.get('[data-test-id="28px"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-size: 28px') - }) - - it('should remove the font-size of the selected text', () => { - cy.get('[data-test-id="28px"]').click() - cy.get('.tiptap').find('span').should('have.attr', 'style', 'font-size: 28px') - cy.get('[data-test-id="unsetFontSize"]').click() - cy.get('.tiptap').get('span').should('not.exist') - }) -}) diff --git a/demos/src/Extensions/Gapcursor/React/index.spec.js b/demos/src/Extensions/Gapcursor/React/index.spec.js deleted file mode 100644 index b618f2d5b9..0000000000 --- a/demos/src/Extensions/Gapcursor/React/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Examples/Gapcursor/React/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Gapcursor/React/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Extensions/Gapcursor/Vue/index.spec.js b/demos/src/Extensions/Gapcursor/Vue/index.spec.js deleted file mode 100644 index 106b7ee4c6..0000000000 --- a/demos/src/Extensions/Gapcursor/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Examples/Gapcursor/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Examples/Gapcursor/Vue/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Extensions/InvisibleCharacters/React/index.spec.js b/demos/src/Extensions/InvisibleCharacters/React/index.spec.js deleted file mode 100644 index 5679a023b4..0000000000 --- a/demos/src/Extensions/InvisibleCharacters/React/index.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -context('/src/Extensions/InvisibleCharacters/React/', () => { - before(() => { - cy.visit('/src/Extensions/InvisibleCharacters/React/') - }) - - it('should have invisible characters', () => { - cy.get('[class*="tiptap-invisible-character"]').should('exist') - }) -}) diff --git a/demos/src/Extensions/InvisibleCharacters/Vue/index.spec.js b/demos/src/Extensions/InvisibleCharacters/Vue/index.spec.js deleted file mode 100644 index 62fd21d80b..0000000000 --- a/demos/src/Extensions/InvisibleCharacters/Vue/index.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -context('/src/Extensions/InvisibleCharacters/Vue/', () => { - before(() => { - cy.visit('/src/Extensions/InvisibleCharacters/Vue/') - }) - - it('should have invisible characters', () => { - cy.get('[class*="tiptap-invisible-character"]').should('exist') - }) -}) diff --git a/demos/src/Extensions/LineHeight/React/index.spec.js b/demos/src/Extensions/LineHeight/React/index.spec.js deleted file mode 100644 index c7c6d8a5da..0000000000 --- a/demos/src/Extensions/LineHeight/React/index.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -context('/src/Extensions/LineHeight/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/LineHeight/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set line-height 1.5 for the selected text', () => { - cy.get('[data-test-id="1.5"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 1.5') - }) - - it('should set line-height 2.0 for the selected text', () => { - cy.get('[data-test-id="2.0"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 2.0') - }) - - it('should set line-height 4.0 for the selected text', () => { - cy.get('[data-test-id="4.0"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 4.0') - }) - - it('should remove the line-height of the selected text', () => { - cy.get('[data-test-id="1.5"]').click() - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 1.5') - - cy.get('[data-test-id="unsetLineHeight"]').click() - cy.get('.tiptap span').should('not.exist') - }) -}) diff --git a/demos/src/Extensions/LineHeight/Vue/index.spec.js b/demos/src/Extensions/LineHeight/Vue/index.spec.js deleted file mode 100644 index e4f57a46b0..0000000000 --- a/demos/src/Extensions/LineHeight/Vue/index.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -context('/src/Extensions/LineHeight/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/LineHeight/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - cy.get('.tiptap').type('{selectall}') - }) - - it('should set line-height 1.5 for the selected text', () => { - cy.get('[data-test-id="1.5"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 1.5') - }) - - it('should set line-height 2.0 for the selected text', () => { - cy.get('[data-test-id="2.0"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 2.0') - }) - - it('should set line-height 4.0 for the selected text', () => { - cy.get('[data-test-id="4.0"]').should('not.have.class', 'is-active').click().should('have.class', 'is-active') - - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 4.0') - }) - - it('should remove the line-height of the selected text', () => { - cy.get('[data-test-id="1.5"]').click() - cy.get('.tiptap').find('span').should('have.attr', 'style', 'line-height: 1.5') - - cy.get('[data-test-id="unsetLineHeight"]').click() - cy.get('.tiptap span').should('not.exist') - }) -}) diff --git a/demos/src/Extensions/Mathematics/React/index.spec.js b/demos/src/Extensions/Mathematics/React/index.spec.js deleted file mode 100644 index 62e2e27d95..0000000000 --- a/demos/src/Extensions/Mathematics/React/index.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -context('/src/Extensions/Mathematics/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Mathematics/React/') - }) - - it('should include katex-rendered inline and block nodes', () => { - cy.get('.tiptap').then(() => { - cy.get('.tiptap span[data-type="inline-math"]').should('have.length', 21) - cy.get('.tiptap div[data-type="block-math"]').should('have.length', 1) - - cy.get('.tiptap span[data-type="inline-math"] .katex').should('have.length', 21) - cy.get('.tiptap div[data-type="block-math"] .katex').should('have.length', 1) - }) - }) -}) diff --git a/demos/src/Extensions/Mathematics/Vue/index.spec.js b/demos/src/Extensions/Mathematics/Vue/index.spec.js deleted file mode 100644 index 547b634943..0000000000 --- a/demos/src/Extensions/Mathematics/Vue/index.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -context('/src/Extensions/Mathematics/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Mathematics/Vue/') - }) - - it('should include katex-rendered inline and block nodes', () => { - cy.get('.tiptap').then(() => { - cy.get('.tiptap span[data-type="inline-math"]').should('have.length', 21) - cy.get('.tiptap div[data-type="block-math"]').should('have.length', 1) - - cy.get('.tiptap span[data-type="inline-math"] .katex').should('have.length', 21) - cy.get('.tiptap div[data-type="block-math"] .katex').should('have.length', 1) - }) - }) -}) diff --git a/demos/src/Extensions/Selection/React/index.spec.js b/demos/src/Extensions/Selection/React/index.spec.js deleted file mode 100644 index 4021043e39..0000000000 --- a/demos/src/Extensions/Selection/React/index.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -context('/src/Extensions/Selection/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Selection/React/') - }) - - it('should have class', () => { - cy.get('.tiptap span:first').should('have.class', 'selection') - }) -}) diff --git a/demos/src/Extensions/Selection/Vue/index.spec.js b/demos/src/Extensions/Selection/Vue/index.spec.js deleted file mode 100644 index 0ffdabcf01..0000000000 --- a/demos/src/Extensions/Selection/Vue/index.spec.js +++ /dev/null @@ -1,9 +0,0 @@ -context('/src/Extensions/Selection/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/Selection/Vue/') - }) - - it('should have class', () => { - cy.get('.tiptap span:first').should('have.class', 'selection') - }) -}) diff --git a/demos/src/Extensions/Selection/index.spec.ts b/demos/src/Extensions/Selection/index.spec.ts new file mode 100644 index 0000000000..9c0d9cb583 --- /dev/null +++ b/demos/src/Extensions/Selection/index.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test' + +const demoName = 'Selection' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Extensions' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('first span has selection class', async ({ page }) => { + await expect(page.locator('.tiptap span').first()).toHaveClass(/selection/) + }) + }) + }) +}) diff --git a/demos/src/Extensions/TableOfContents/Vue/index.spec.js b/demos/src/Extensions/TableOfContents/Vue/index.spec.js deleted file mode 100644 index 839eb83420..0000000000 --- a/demos/src/Extensions/TableOfContents/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Extensions/TableOfContents/Vue', () => { - before(() => { - cy.visit('/src/Extensions/TableOfContents/Vue') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Extensions/TextAlign/React/index.spec.js b/demos/src/Extensions/TextAlign/React/index.spec.js deleted file mode 100644 index 5acdcd41c3..0000000000 --- a/demos/src/Extensions/TextAlign/React/index.spec.js +++ /dev/null @@ -1,127 +0,0 @@ -context('/src/Extensions/TextAlign/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/TextAlign/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should parse a null alignment correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse left align text correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse center align text correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse right align text correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse left justify text correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should keep the text aligned when toggling headings', () => { - const alignments = ['center', 'right', 'justify'] - const headings = [1, 2] - - cy.get('.tiptap').then(([{ editor }]) => { - alignments.forEach(alignment => { - headings.forEach(level => { - editor.commands.setContent(`Example Text
`) - editor.commands.toggleHeading({ level }) - expect(editor.getHTML()).to.eq(`Example Text
') - }) - }) - - it('should parse a null alignment correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse left align text correctly (and not render)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse center align text correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse right align text correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse left justify text correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should keep the text aligned when toggling headings', () => { - const alignments = ['center', 'right', 'justify'] - const headings = [1, 2] - - cy.get('.tiptap').then(([{ editor }]) => { - alignments.forEach(alignment => { - headings.forEach(level => { - editor.commands.setContent(`Example Text
`) - editor.commands.toggleHeading({ level }) - expect(editor.getHTML()).to.eq(`Example Text
') + }) + + test('parses a null alignment correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + ;['left', 'center', 'right', 'justify'].forEach(alignment => { + test(`parses ${alignment} align text correctly`, async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any, a: string) => { + el.editor.commands.setContent(`Example Text
`) + return el.editor.getHTML() + }, alignment) + expect(html).toBe(`Example Text
`) + }) + }) + + test('keeps the text aligned when toggling headings', async ({ page }) => { + const editor = await getEditor(page) + const result = await editor.evaluate((el: any) => { + const alignments = ['center', 'right', 'justify'] + const headings = [1, 2] + const out: string[] = [] + alignments.forEach(alignment => { + headings.forEach(level => { + el.editor.commands.setContent(`Example Text
`) + el.editor.commands.toggleHeading({ level }) + out.push(el.editor.getHTML()) + }) + }) + return out + }) + + const alignments = ['center', 'right', 'justify'] + const headings = [1, 2] + const expected: string[] = [] + alignments.forEach(alignment => { + headings.forEach(level => { + expected.push(`Mistake
') - }) - }) - - it('should make the last change undone', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('.tiptap').should('not.contain', 'Mistake') - }) - - it('should make the last change undone with the keyboard shortcut', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'z' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - }) - - it('should make the last change undone with the keyboard shortcut (russian)', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'я' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - }) - - it('should apply the last undone change again with the keyboard shortcut', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'z' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - - cy.get('.tiptap').trigger('keydown', { modKey: true, shiftKey: true, key: 'z' }) - - cy.get('.tiptap').should('contain', 'Mistake') - }) - - it('should apply the last undone change again with the keyboard shortcut (russian)', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'я' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - - cy.get('.tiptap').trigger('keydown', { modKey: true, shiftKey: true, key: 'я' }) - - cy.get('.tiptap').should('contain', 'Mistake') - }) - - it('should apply the last undone change again', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('.tiptap').should('not.contain', 'Mistake') - - cy.get('button:first').should('have.attr', 'disabled') - - cy.get('button:nth-child(2)').click() - - cy.get('.tiptap').should('contain', 'Mistake') - }) - - it('should disable undo button when there are no more changes to undo', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('button:first').should('have.attr', 'disabled') - }) - - it('should disable redo button when there are no more changes to redo', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:nth-child(2)').should('have.attr', 'disabled') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('button:nth-child(2)').should('not.have.attr', 'disabled') - }) -}) diff --git a/demos/src/Extensions/UndoRedo/Vue/index.spec.js b/demos/src/Extensions/UndoRedo/Vue/index.spec.js deleted file mode 100644 index 09cf592395..0000000000 --- a/demos/src/Extensions/UndoRedo/Vue/index.spec.js +++ /dev/null @@ -1,90 +0,0 @@ -context('/src/Extensions/UndoRedo/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/UndoRedo/Vue/') - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Mistake
') - }) - }) - - it('should make the last change undone', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('.tiptap').should('not.contain', 'Mistake') - }) - - it('should make the last change undone with the keyboard shortcut', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'z' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - }) - - it('should make the last change undone with the keyboard shortcut (russian)', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'я' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - }) - - it('should apply the last undone change again with the keyboard shortcut', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'z' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - - cy.get('.tiptap').trigger('keydown', { modKey: true, shiftKey: true, key: 'z' }) - - cy.get('.tiptap').should('contain', 'Mistake') - }) - - it('should apply the last undone change again with the keyboard shortcut (russian)', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'я' }) - - cy.get('.tiptap').should('not.contain', 'Mistake') - - cy.get('.tiptap').trigger('keydown', { modKey: true, shiftKey: true, key: 'я' }) - - cy.get('.tiptap').should('contain', 'Mistake') - }) - - it('should apply the last undone change again', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('.tiptap').should('not.contain', 'Mistake') - - cy.get('button:first').should('have.attr', 'disabled') - - cy.get('button:nth-child(2)').click() - - cy.get('.tiptap').should('contain', 'Mistake') - }) - - it('should disable undo button when there are no more changes to undo', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('button:first').should('have.attr', 'disabled') - }) - - it('should disable redo button when there are no more changes to redo', () => { - cy.get('.tiptap').should('contain', 'Mistake') - - cy.get('button:nth-child(2)').should('have.attr', 'disabled') - - cy.get('button:first').should('not.have.attr', 'disabled') - - cy.get('button:first').click() - - cy.get('button:nth-child(2)').should('not.have.attr', 'disabled') - }) -}) diff --git a/demos/src/Extensions/UndoRedo/index.spec.ts b/demos/src/Extensions/UndoRedo/index.spec.ts new file mode 100644 index 0000000000..98ca0e0e98 --- /dev/null +++ b/demos/src/Extensions/UndoRedo/index.spec.ts @@ -0,0 +1,82 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'UndoRedo' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Extensions' + +const isMac = process.platform === 'darwin' +const mod = isMac ? 'Meta' : 'Control' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await setEditorContent(page, 'Mistake
') + }) + + test('undoes the last change via button', async ({ page }) => { + const editor = await getEditor(page) + await expect(editor).toContainText('Mistake') + + const undoBtn = page.locator('button').first() + await expect(undoBtn).not.toHaveAttribute('disabled', '') + await undoBtn.click() + + await expect(editor).not.toContainText('Mistake') + }) + + test('undoes the last change with the keyboard shortcut', async ({ page }) => { + const editor = await getEditor(page) + await editor.click() + await page.keyboard.press(`${mod}+z`) + await expect(editor).not.toContainText('Mistake') + }) + + test('redoes the last undone change with the keyboard shortcut', async ({ page }) => { + const editor = await getEditor(page) + await editor.click() + await page.keyboard.press(`${mod}+z`) + await expect(editor).not.toContainText('Mistake') + + await page.keyboard.press(`${mod}+Shift+z`) + await expect(editor).toContainText('Mistake') + }) + + test('redoes the last undone change via buttons', async ({ page }) => { + const editor = await getEditor(page) + const undoBtn = page.locator('button').nth(0) + const redoBtn = page.locator('button').nth(1) + + await expect(editor).toContainText('Mistake') + await undoBtn.click() + await expect(editor).not.toContainText('Mistake') + await expect(undoBtn).toHaveAttribute('disabled', '') + + await redoBtn.click() + await expect(editor).toContainText('Mistake') + }) + + test('disables undo button when there are no more changes to undo', async ({ page }) => { + const undoBtn = page.locator('button').first() + await expect(undoBtn).not.toHaveAttribute('disabled', '') + await undoBtn.click() + await expect(undoBtn).toHaveAttribute('disabled', '') + }) + + test('disables redo button when there are no more changes to redo', async ({ page }) => { + const undoBtn = page.locator('button').nth(0) + const redoBtn = page.locator('button').nth(1) + + await expect(redoBtn).toHaveAttribute('disabled', '') + await expect(undoBtn).not.toHaveAttribute('disabled', '') + await undoBtn.click() + await expect(redoBtn).not.toHaveAttribute('disabled', '') + }) + }) + }) +}) diff --git a/demos/src/Extensions/UniqueID/React/index.spec.js b/demos/src/Extensions/UniqueID/React/index.spec.js deleted file mode 100644 index c550baf264..0000000000 --- a/demos/src/Extensions/UniqueID/React/index.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -context('/src/Extensions/UniqueID/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/UniqueID/React/') - }) - - it('has a heading with an unique ID', () => { - cy.get('.ProseMirror h1').should('have.attr', 'data-id') - }) - - it('has a paragraph with an unique ID', () => { - cy.get('.ProseMirror p').should('have.attr', 'data-id') - }) -}) diff --git a/demos/src/Extensions/UniqueID/Vue/index.spec.js b/demos/src/Extensions/UniqueID/Vue/index.spec.js deleted file mode 100644 index b8798541c6..0000000000 --- a/demos/src/Extensions/UniqueID/Vue/index.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -context('/src/Extensions/UniqueID/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/UniqueID/React/') - }) - - it('has a heading with an unique ID', () => { - cy.get('.ProseMirror h1').should('have.attr', 'data-id') - }) - - it('has a paragraph with an unique ID', () => { - cy.get('.ProseMirror p').should('have.attr', 'data-id') - }) -}) diff --git a/demos/src/Extensions/UniqueIDWithYdoc/React/index.spec.js b/demos/src/Extensions/UniqueIDWithYdoc/React/index.spec.js deleted file mode 100644 index c550baf264..0000000000 --- a/demos/src/Extensions/UniqueIDWithYdoc/React/index.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -context('/src/Extensions/UniqueID/React/', () => { - beforeEach(() => { - cy.visit('/src/Extensions/UniqueID/React/') - }) - - it('has a heading with an unique ID', () => { - cy.get('.ProseMirror h1').should('have.attr', 'data-id') - }) - - it('has a paragraph with an unique ID', () => { - cy.get('.ProseMirror p').should('have.attr', 'data-id') - }) -}) diff --git a/demos/src/GuideContent/ExportHTML/React/index.spec.js b/demos/src/GuideContent/ExportHTML/React/index.spec.js deleted file mode 100644 index 6bd49a9bb7..0000000000 --- a/demos/src/GuideContent/ExportHTML/React/index.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -context('/src/GuideContent/ExportHTML/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/ExportHTML/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should return html', () => { - cy.get('.tiptap').then(([{ editor }]) => { - const html = editor.getHTML() - - expect(html).to.equal('Example Text
') - }) - }) -}) diff --git a/demos/src/GuideContent/ExportHTML/Vue/index.spec.js b/demos/src/GuideContent/ExportHTML/Vue/index.spec.js deleted file mode 100644 index 25d8bd7bd9..0000000000 --- a/demos/src/GuideContent/ExportHTML/Vue/index.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -context('/src/GuideContent/ExportHTML/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/ExportHTML/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should return html', () => { - cy.get('.tiptap').then(([{ editor }]) => { - const html = editor.getHTML() - - expect(html).to.equal('Example Text
') - }) - }) -}) diff --git a/demos/src/GuideContent/ExportJSON/React/index.spec.js b/demos/src/GuideContent/ExportJSON/React/index.spec.js deleted file mode 100644 index 617a2f2fa9..0000000000 --- a/demos/src/GuideContent/ExportJSON/React/index.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -context('/src/GuideContent/ExportJSON/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/ExportJSON/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should return json', () => { - cy.get('.tiptap').then(([{ editor }]) => { - const json = editor.getJSON() - - expect(json).to.deep.equal({ - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Example Text', - }, - ], - }, - ], - }) - }) - }) -}) diff --git a/demos/src/GuideContent/ExportJSON/Vue/index.spec.js b/demos/src/GuideContent/ExportJSON/Vue/index.spec.js deleted file mode 100644 index 56facb0d7e..0000000000 --- a/demos/src/GuideContent/ExportJSON/Vue/index.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -context('/src/GuideContent/ExportJSON/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/ExportJSON/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should return json', () => { - cy.get('.tiptap').then(([{ editor }]) => { - const json = editor.getJSON() - - expect(json).to.deep.equal({ - type: 'doc', - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'Example Text', - }, - ], - }, - ], - }) - }) - }) -}) diff --git a/demos/src/GuideContent/GenerateHTML/React/index.spec.js b/demos/src/GuideContent/GenerateHTML/React/index.spec.js deleted file mode 100644 index dee4b8a483..0000000000 --- a/demos/src/GuideContent/GenerateHTML/React/index.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -context('/src/GuideContent/GenerateHTML/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/GenerateHTML/React/') - }) - - it('should render the content as an HTML string', () => { - cy.get('pre code').should('exist') - - cy.get('pre code').should('contain', 'Example Text
') - }) -}) diff --git a/demos/src/GuideContent/GenerateHTML/Vue/index.spec.js b/demos/src/GuideContent/GenerateHTML/Vue/index.spec.js deleted file mode 100644 index b5afcf7509..0000000000 --- a/demos/src/GuideContent/GenerateHTML/Vue/index.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -context('/src/GuideContent/GenerateHTML/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/GenerateHTML/Vue/') - }) - - it('should render the content as an HTML string', () => { - cy.get('pre code').should('exist') - - cy.get('pre code').should('contain', 'Example Text
') - }) -}) diff --git a/demos/src/GuideContent/GenerateJSON/React/index.spec.js b/demos/src/GuideContent/GenerateJSON/React/index.spec.js deleted file mode 100644 index b84b11b592..0000000000 --- a/demos/src/GuideContent/GenerateJSON/React/index.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -context('/src/GuideContent/GenerateJSON/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/GenerateJSON/React/') - }) - - it('should render the content as an HTML string', () => { - cy.get('pre code').should('exist') - - cy.get('pre code').should( - 'contain', - `{ - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "text": "Example " - }, - { - "type": "text", - "marks": [ - { - "type": "bold" - } - ], - "text": "Text" - } - ] - } - ] -}`, - ) - }) -}) diff --git a/demos/src/GuideContent/GenerateJSON/Vue/index.spec.js b/demos/src/GuideContent/GenerateJSON/Vue/index.spec.js deleted file mode 100644 index 90a0678c54..0000000000 --- a/demos/src/GuideContent/GenerateJSON/Vue/index.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -context('/src/GuideContent/GenerateJSON/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/GenerateJSON/Vue/') - }) - - it('should render the content as an HTML string', () => { - cy.get('pre code').should('exist') - - cy.get('pre code').should( - 'contain', - `{ - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "type": "text", - "text": "Example " - }, - { - "type": "text", - "marks": [ - { - "type": "bold" - } - ], - "text": "Text" - } - ] - } - ] -}`, - ) - }) -}) diff --git a/demos/src/GuideContent/GenerateText/React/index.spec.js b/demos/src/GuideContent/GenerateText/React/index.spec.js deleted file mode 100644 index 5e4f805a64..0000000000 --- a/demos/src/GuideContent/GenerateText/React/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/GuideContent/GenerateText/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/GenerateText/React/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/GuideContent/GenerateText/Vue/index.spec.js b/demos/src/GuideContent/GenerateText/Vue/index.spec.js deleted file mode 100644 index e63003cce4..0000000000 --- a/demos/src/GuideContent/GenerateText/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/GuideContent/GenerateText/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/GenerateText/Vue/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/GuideContent/ReadOnly/React/index.spec.js b/demos/src/GuideContent/ReadOnly/React/index.spec.js deleted file mode 100644 index 711805b5f7..0000000000 --- a/demos/src/GuideContent/ReadOnly/React/index.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -context('/src/GuideContent/ReadOnly/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/ReadOnly/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should be read-only', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.setEditable(false) - cy.get('.tiptap').type('Edited: ') - - cy.get('.tiptap p:first').should('not.contain', 'Edited: ') - - cy.get('.tiptap').invoke('attr', 'tabindex').should('not.exist') - }) - }) - - it('should be editable', () => { - cy.get('#editable').click() - cy.get('.tiptap').then(() => { - cy.get('.tiptap').type('Edited: ') - - cy.get('.tiptap p:first').should('contain', 'Edited: ') - - cy.get('.tiptap').invoke('attr', 'tabindex').should('eq', '0') - }) - }) -}) diff --git a/demos/src/GuideContent/ReadOnly/Vue/index.spec.js b/demos/src/GuideContent/ReadOnly/Vue/index.spec.js deleted file mode 100644 index c031076fa7..0000000000 --- a/demos/src/GuideContent/ReadOnly/Vue/index.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -context('/src/GuideContent/ReadOnly/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/ReadOnly/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should be read-only', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.setEditable(false) - cy.get('.tiptap').type('Edited: ') - - cy.get('.tiptap p:first').should('not.contain', 'Edited: ') - }) - }) - - it('should be editable', () => { - cy.get('#editable').click() - cy.get('.tiptap').then(() => { - cy.get('.tiptap').type('Edited: ') - - cy.get('.tiptap p:first').should('contain', 'Edited: ') - }) - }) -}) diff --git a/demos/src/GuideContent/ReadOnly/index.spec.ts b/demos/src/GuideContent/ReadOnly/index.spec.ts new file mode 100644 index 0000000000..80dae32510 --- /dev/null +++ b/demos/src/GuideContent/ReadOnly/index.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'ReadOnly' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/GuideContent' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + }) + + test('is read-only', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.setEditable(false)) + await editor.click() + await page.keyboard.type('Edited: ') + + await expect(page.locator('.tiptap p').first()).not.toContainText('Edited:') + }) + + test('is editable', async ({ page }) => { + await page.locator('#editable').click() + const editor = await getEditor(page) + await editor.click() + await page.keyboard.type('Edited: ') + + await expect(page.locator('.tiptap p').first()).toContainText('Edited:') + }) + }) + }) +}) diff --git a/demos/src/GuideContent/StaticRenderHTML/React/index.spec.js b/demos/src/GuideContent/StaticRenderHTML/React/index.spec.js deleted file mode 100644 index 075f3832b1..0000000000 --- a/demos/src/GuideContent/StaticRenderHTML/React/index.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -context('/src/GuideContent/StaticRenderHTML/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/StaticRenderHTML/React/') - }) - - it('should render the content as an HTML string', () => { - cy.get('pre code').should('exist') - - cy.get('pre code').should('contain', 'Example Text
') - }) -}) diff --git a/demos/src/GuideContent/StaticRenderHTML/Vue/index.spec.js b/demos/src/GuideContent/StaticRenderHTML/Vue/index.spec.js deleted file mode 100644 index 34bde8cc54..0000000000 --- a/demos/src/GuideContent/StaticRenderHTML/Vue/index.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -context('/src/GuideContent/StaticRenderHTML/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/StaticRenderHTML/Vue/') - }) - - it('should render the content as an HTML string', () => { - cy.get('pre code').should('exist') - - cy.get('pre code').should('contain', 'Example Text
') - }) -}) diff --git a/demos/src/GuideContent/StaticRenderReact/React/index.spec.js b/demos/src/GuideContent/StaticRenderReact/React/index.spec.js deleted file mode 100644 index 9b7e582ffd..0000000000 --- a/demos/src/GuideContent/StaticRenderReact/React/index.spec.js +++ /dev/null @@ -1,13 +0,0 @@ -context('/src/GuideContent/StaticRenderReact/React/', () => { - beforeEach(() => { - cy.visit('/src/GuideContent/StaticRenderReact/React/') - }) - - it('should render the content as HTML', () => { - cy.get('p').should('exist') - cy.get('p').should('contain', 'Example') - - cy.get('p strong').should('exist') - cy.get('p strong').should('contain', 'Text') - }) -}) diff --git a/demos/src/GuideGettingStarted/VModel/Vue/index.spec.js b/demos/src/GuideGettingStarted/VModel/Vue/index.spec.js deleted file mode 100644 index 8565a14cfa..0000000000 --- a/demos/src/GuideGettingStarted/VModel/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/GuideGettingStarted/VModel/Vue/', () => { - beforeEach(() => { - cy.visit('/src/GuideGettingStarted/VModel/Vue/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/GuideGettingStarted/VModel/index.spec.ts b/demos/src/GuideGettingStarted/VModel/index.spec.ts new file mode 100644 index 0000000000..646b5f126a --- /dev/null +++ b/demos/src/GuideGettingStarted/VModel/index.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'VModel' +const frameworkPaths = ['Vue'] +const demoPath = '/src/GuideGettingStarted' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('loads the editor', async ({ page }) => { + const editor = await getEditor(page) + await expect(editor).toBeVisible() + }) + }) + }) +}) diff --git a/demos/src/GuideMarkViews/ReactComponent/React/index.spec.js b/demos/src/GuideMarkViews/ReactComponent/React/index.spec.js deleted file mode 100644 index dc02ef282e..0000000000 --- a/demos/src/GuideMarkViews/ReactComponent/React/index.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -///Example Text
Example Text
Example Text
Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should transform b tags to strong tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('sould omit b tags with normal font weight inline style', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should transform any tag with bold inline style to strong tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text bold', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('strong').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text bold', () => { - cy.get('button:first').click() - cy.get('.tiptap').type('{selectall}') - cy.get('button:first').click() - cy.get('.tiptap strong').should('not.exist') - }) - - it('should make the selected text bold when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'b' }).find('strong').should('contain', 'Example Text') - }) - - it('should toggle the selected text bold when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'b' }).find('strong').should('contain', 'Example Text') - - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'b' }) - - cy.get('.tiptap strong').should('not.exist') - }) - - it('should make a bold text from the default markdown shortcut', () => { - cy.get('.tiptap').type('**Bold**').find('strong').should('contain', 'Bold') - }) - - it('should make a bold text from the alternative markdown shortcut', () => { - cy.get('.tiptap').type('__Bold__').find('strong').should('contain', 'Bold') - }) -}) diff --git a/demos/src/Marks/Bold/Vue/index.spec.js b/demos/src/Marks/Bold/Vue/index.spec.js deleted file mode 100644 index a2ad5d9077..0000000000 --- a/demos/src/Marks/Bold/Vue/index.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -context('/src/Marks/Bold/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Bold/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should transform b tags to strong tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('sould omit b tags with normal font weight inline style', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should transform any tag with bold inline style to strong tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text bold', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('strong').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text bold', () => { - cy.get('button:first').click() - cy.get('.tiptap').type('{selectall}') - cy.get('button:first').click() - cy.get('.tiptap strong').should('not.exist') - }) - - it('should make the selected text bold when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'b' }).find('strong').should('contain', 'Example Text') - }) - - it('should toggle the selected text bold when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'b' }).find('strong').should('contain', 'Example Text') - - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'b' }) - - cy.get('.tiptap strong').should('not.exist') - }) - - it('should make a bold text from the default markdown shortcut', () => { - cy.get('.tiptap').type('**Bold**').find('strong').should('contain', 'Bold') - }) - - it('should make a bold text from the alternative markdown shortcut', () => { - cy.get('.tiptap').type('__Bold__').find('strong').should('contain', 'Bold') - }) -}) diff --git a/demos/src/Marks/Bold/index.spec.ts b/demos/src/Marks/Bold/index.spec.ts new file mode 100644 index 0000000000..9149cde18a --- /dev/null +++ b/demos/src/Marks/Bold/index.spec.ts @@ -0,0 +1,94 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'Bold' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Marks' + +const isMac = process.platform === 'darwin' +const mod = isMac ? 'Meta' : 'Control' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await setEditorContent(page, 'Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('transforms b tags to strong tags', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('omits b tags with normal font weight inline style', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('transforms any tag with bold inline style to strong tags', async ({ page }) => { + const editor = await getEditor(page) + const results = await editor.evaluate((el: any) => { + const styles = ['bold', 'bolder', '500', '900'] + return styles.map(s => { + el.editor.commands.setContent(`Example Text
`) + return el.editor.getHTML() + }) + }) + results.forEach(html => expect(html).toBe('Example Text
')) + }) + + test('button makes the selected text bold', async ({ page }) => { + await page.locator('button').first().click() + await expect(page.locator('.tiptap strong')).toContainText('Example Text') + }) + + test('button toggles the selected text bold', async ({ page }) => { + const editor = await getEditor(page) + await page.locator('button').first().click() + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await page.locator('button').first().click() + await expect(page.locator('.tiptap strong')).toHaveCount(0) + }) + + test('keyboard shortcut makes selected text bold', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.commands.focus() + el.editor.commands.selectAll() + }) + await editor.press(`${mod}+b`) + await expect(page.locator('.tiptap strong')).toContainText('Example Text') + }) + + test('markdown shortcut creates bold text', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('**Bold**') + await expect(page.locator('.tiptap strong')).toContainText('Bold') + }) + + test('alternative markdown shortcut creates bold text', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('__Bold__') + await expect(page.locator('.tiptap strong')).toContainText('Bold') + }) + }) + }) +}) diff --git a/demos/src/Marks/Code/React/index.spec.js b/demos/src/Marks/Code/React/index.spec.js deleted file mode 100644 index e31f1d5be2..0000000000 --- a/demos/src/Marks/Code/React/index.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -context('/src/Marks/Code/React/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Code/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse code tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text')
- expect(editor.getHTML()).to.eq('Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse code tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text')
- expect(editor.getHTML()).to.eq('Example Text
Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses code tags correctly', async ({ page }) => { + const editor = await getEditor(page) + const html1 = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
Example Text
Example Text
').selectAll().run() - }) - }) - - it('the button should highlight the selected text', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('mark').should('contain', 'Example Text') - }) - - it('should highlight the text in a specific color', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.toggleHighlight({ color: 'red' }) - - cy.get('.tiptap').find('mark').should('contain', 'Example Text').should('have.attr', 'data-color', 'red') - }) - }) - - it('should update the attributes of existing marks', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .toggleHighlight({ color: 'rgb(255, 0, 0)' }) - .run() - - cy.get('.tiptap').find('mark').should('have.css', 'background-color', 'rgb(255, 0, 0)') - }) - }) - - it('should remove existing marks with the same attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .toggleHighlight({ color: 'rgb(255, 0, 0)' }) - .run() - - cy.get('.tiptap').find('mark').should('not.exist') - }) - }) - - it('is active for mark with any attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.chain().setContent('Example Text
').selectAll().run() - - expect(editor.isActive('highlight')).to.eq(true) - }) - }) - - it('is active for mark with same attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .run() - - const isActive = editor.isActive('highlight', { - color: 'rgb(255, 0, 0)', - }) - - expect(isActive).to.eq(true) - }) - }) - - it('isn’t active for mark with other attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .run() - - const isActive = editor.isActive('highlight', { - color: 'rgb(0, 0, 0)', - }) - - expect(isActive).to.eq(false) - }) - }) - - it('the button should toggle the selected text highlighted', () => { - cy.get('button:first').click() - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap').find('mark').should('not.exist') - }) - - it('should highlight the selected text when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, shiftKey: true, key: 'h' }) - .find('mark') - .should('contain', 'Example Text') - }) - - it('should toggle the selected text highlighted when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, shiftKey: true, key: 'h' }) - .trigger('keydown', { modKey: true, shiftKey: true, key: 'h' }) - .find('mark') - .should('not.exist') - }) -}) diff --git a/demos/src/Marks/Highlight/Vue/index.spec.js b/demos/src/Marks/Highlight/Vue/index.spec.js deleted file mode 100644 index 0f422e878e..0000000000 --- a/demos/src/Marks/Highlight/Vue/index.spec.js +++ /dev/null @@ -1,116 +0,0 @@ -context('/src/Marks/Highlight/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Highlight/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.chain().setContent('Example Text
').selectAll().run() - }) - }) - - it('the button should highlight the selected text', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('mark').should('contain', 'Example Text') - }) - - it('should highlight the text in a specific color', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.toggleHighlight({ color: 'red' }) - - cy.get('.tiptap').find('mark').should('contain', 'Example Text').should('have.attr', 'data-color', 'red') - }) - }) - - it('should update the attributes of existing marks', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .toggleHighlight({ color: 'rgb(255, 0, 0)' }) - .run() - - cy.get('.tiptap').find('mark').should('have.css', 'background-color', 'rgb(255, 0, 0)') - }) - }) - - it('should remove existing marks with the same attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .toggleHighlight({ color: 'rgb(255, 0, 0)' }) - .run() - - cy.get('.tiptap').find('mark').should('not.exist') - }) - }) - - it('is active for mark with any attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.chain().setContent('Example Text
').selectAll().run() - - expect(editor.isActive('highlight')).to.eq(true) - }) - }) - - it('is active for mark with same attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .run() - - const isActive = editor.isActive('highlight', { - color: 'rgb(255, 0, 0)', - }) - - expect(isActive).to.eq(true) - }) - }) - - it('isn’t active for mark with other attributes', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor - .chain() - .setContent('Example Text
') - .selectAll() - .run() - - const isActive = editor.isActive('highlight', { - color: 'rgb(0, 0, 0)', - }) - - expect(isActive).to.eq(false) - }) - }) - - it('the button should toggle the selected text highlighted', () => { - cy.get('button:first').click() - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap').find('mark').should('not.exist') - }) - - it('should highlight the selected text when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, shiftKey: true, key: 'h' }) - .find('mark') - .should('contain', 'Example Text') - }) - - it('should toggle the selected text highlighted when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, shiftKey: true, key: 'h' }) - .trigger('keydown', { modKey: true, shiftKey: true, key: 'h' }) - .find('mark') - .should('not.exist') - }) -}) diff --git a/demos/src/Marks/Highlight/index.spec.ts b/demos/src/Marks/Highlight/index.spec.ts new file mode 100644 index 0000000000..538230c21c --- /dev/null +++ b/demos/src/Marks/Highlight/index.spec.ts @@ -0,0 +1,117 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Highlight' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Marks' + +const isMac = process.platform === 'darwin' +const mod = isMac ? 'Meta' : 'Control' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.chain().setContent('Example Text
').selectAll().run() + }) + }) + + test('button highlights selected text', async ({ page }) => { + await page.locator('button').first().click() + await expect(page.locator('.tiptap mark')).toContainText('Example Text') + }) + + test('highlights text in a specific color', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.toggleHighlight({ color: 'red' })) + await expect(page.locator('.tiptap mark')).toHaveAttribute('data-color', 'red') + await expect(page.locator('.tiptap mark')).toContainText('Example Text') + }) + + test('updates attributes of existing marks', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor + .chain() + .setContent('Example Text
') + .selectAll() + .toggleHighlight({ color: 'rgb(255, 0, 0)' }) + .run() + }) + await expect(page.locator('.tiptap mark')).toHaveCSS('background-color', 'rgb(255, 0, 0)') + }) + + test('removes existing marks with same attributes', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor + .chain() + .setContent('Example Text
') + .selectAll() + .toggleHighlight({ color: 'rgb(255, 0, 0)' }) + .run() + }) + await expect(page.locator('.tiptap mark')).toHaveCount(0) + }) + + test('isActive: any attributes', async ({ page }) => { + const editor = await getEditor(page) + const isActive = await editor.evaluate((el: any) => { + el.editor.chain().setContent('Example Text
').selectAll().run() + return el.editor.isActive('highlight') + }) + expect(isActive).toBe(true) + }) + + test('isActive: same attributes', async ({ page }) => { + const editor = await getEditor(page) + const isActive = await editor.evaluate((el: any) => { + el.editor + .chain() + .setContent('Example Text
') + .selectAll() + .run() + return el.editor.isActive('highlight', { color: 'rgb(255, 0, 0)' }) + }) + expect(isActive).toBe(true) + }) + + test('isActive: other attributes', async ({ page }) => { + const editor = await getEditor(page) + const isActive = await editor.evaluate((el: any) => { + el.editor + .chain() + .setContent('Example Text
') + .selectAll() + .run() + return el.editor.isActive('highlight', { color: 'rgb(0, 0, 0)' }) + }) + expect(isActive).toBe(false) + }) + + test('button toggles highlight on selected text', async ({ page }) => { + const editor = await getEditor(page) + await page.locator('button').first().click() + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await page.locator('button').first().click() + await expect(page.locator('.tiptap mark')).toHaveCount(0) + }) + + test('keyboard shortcut highlights selected text', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.commands.focus() + el.editor.commands.selectAll() + }) + await editor.press(`${mod}+Shift+h`) + await expect(page.locator('.tiptap mark')).toContainText('Example Text') + }) + }) + }) +}) diff --git a/demos/src/Marks/Italic/React/index.spec.js b/demos/src/Marks/Italic/React/index.spec.js deleted file mode 100644 index d7de5491d9..0000000000 --- a/demos/src/Marks/Italic/React/index.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -context('/src/Marks/Italic/React/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Italic/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('i tags should be transformed to em tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('i tags with normal font style should be omitted', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('generic tags with italic style should be transformed to strong tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text italic', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('em').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text italic', () => { - cy.get('button:first').click() - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap em').should('not.exist') - }) - - it('the keyboard shortcut should make the selected text italic', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'i' }).find('em').should('contain', 'Example Text') - }) - - it('the keyboard shortcut should toggle the selected text italic', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, key: 'i' }) - .trigger('keydown', { modKey: true, key: 'i' }) - .find('em') - .should('not.exist') - }) -}) diff --git a/demos/src/Marks/Italic/Vue/index.spec.js b/demos/src/Marks/Italic/Vue/index.spec.js deleted file mode 100644 index 0ca43b51d5..0000000000 --- a/demos/src/Marks/Italic/Vue/index.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -context('/src/Marks/Italic/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Italic/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('i tags should be transformed to em tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('i tags with normal font style should be omitted', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('generic tags with italic style should be transformed to strong tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text italic', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('em').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text italic', () => { - cy.get('button:first').click() - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap em').should('not.exist') - }) - - it('the keyboard shortcut should make the selected text italic', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'i' }).find('em').should('contain', 'Example Text') - }) - - it('the keyboard shortcut should toggle the selected text italic', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, key: 'i' }) - .trigger('keydown', { modKey: true, key: 'i' }) - .find('em') - .should('not.exist') - }) -}) diff --git a/demos/src/Marks/Italic/index.spec.ts b/demos/src/Marks/Italic/index.spec.ts new file mode 100644 index 0000000000..3240ef3ac8 --- /dev/null +++ b/demos/src/Marks/Italic/index.spec.ts @@ -0,0 +1,83 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'Italic' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Marks' + +const isMac = process.platform === 'darwin' +const mod = isMac ? 'Meta' : 'Control' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await setEditorContent(page, 'Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('i tags transform to em tags', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('omits i tags with normal font style', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('generic tags with italic style transform to em tags', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('button makes selected text italic', async ({ page }) => { + await page.locator('button').first().click() + await expect(page.locator('.tiptap em')).toContainText('Example Text') + }) + + test('button toggles selected text italic', async ({ page }) => { + const editor = await getEditor(page) + await page.locator('button').first().click() + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await page.locator('button').first().click() + await expect(page.locator('.tiptap em')).toHaveCount(0) + }) + + test('keyboard shortcut makes selected text italic', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.commands.focus() + el.editor.commands.selectAll() + }) + await editor.press(`${mod}+i`) + await expect(page.locator('.tiptap em')).toContainText('Example Text') + }) + + test('markdown shortcut creates italic text', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('*Italic* ') + await expect(page.locator('.tiptap em')).toContainText('Italic') + }) + }) + }) +}) diff --git a/demos/src/Marks/Link/React/index.spec.js b/demos/src/Marks/Link/React/index.spec.js deleted file mode 100644 index 0ceb17890e..0000000000 --- a/demos/src/Marks/Link/React/index.spec.js +++ /dev/null @@ -1,165 +0,0 @@ -context('/src/Marks/Link/React/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Link/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example TextDEFAULT
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse a tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq( - '', - ) - }) - }) - - it('should parse a tags with target attribute correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq( - '', - ) - }) - }) - - it('should parse a tags with rel attribute correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq( - '', - ) - }) - }) - - it('the button should add a link to the selected text', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('https://tiptap.dev') - - cy.get('button:first').click() - - cy.window().its('prompt').should('be.called') - - cy.get('.tiptap') - .find('a') - .should('contain', 'Example TextDEFAULT') - .should('have.attr', 'href', 'https://tiptap.dev') - }) - }) - - it('should allow exiting the link once set', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - cy.get('.tiptap').type('{rightArrow}') - - cy.get('button:first').should('not.have.class', 'is-active') - }) - }) - - it('detects autolinking', () => { - cy.get('.tiptap') - .type('https://example.com ') - .find('a') - .should('contain', 'https://example.com') - .should('have.attr', 'href', 'https://example.com') - }) - - it('detects autolinking with numbers', () => { - cy.get('.tiptap') - .type('https://tiptap4u.com ') - .find('a') - .should('contain', 'https://tiptap4u.com') - .should('have.attr', 'href', 'https://tiptap4u.com') - }) - - it('uses the default protocol', () => { - cy.get('.tiptap') - .type('example.com ') - .find('a') - .should('contain', 'example.com') - .should('have.attr', 'href', 'https://example.com') - }) - - it('uses a non-default protocol if present', () => { - cy.get('.tiptap') - .type('http://example.com ') - .find('a') - .should('contain', 'http://example.com') - .should('have.attr', 'href', 'http://example.com') - }) - - it('detects a pasted URL within a text', () => { - cy.get('.tiptap') - .paste({ - pastePayload: 'some text https://example1.com around an url', - pasteType: 'text/plain', - }) - .find('a') - .should('contain', 'https://example1.com') - .should('have.attr', 'href', 'https://example1.com') - }) - - it('detects a pasted URL', () => { - cy.get('.tiptap') - .type('{backspace}') - .paste({ pastePayload: 'https://example2.com', pasteType: 'text/plain' }) - .find('a') - .should('contain', 'https://example2.com') - .should('have.attr', 'href', 'https://example2.com') - }) - - it('detects a pasted URL with query params', () => { - cy.get('.tiptap') - .type('{backspace}') - .paste({ pastePayload: 'https://example.com?paramA=nice¶mB=cool', pasteType: 'text/plain' }) - .find('a') - .should('contain', 'https://example.com?paramA=nice¶mB=cool') - .should('have.attr', 'href', 'https://example.com?paramA=nice¶mB=cool') - }) - - it('correctly detects multiple pasted URLs', () => { - cy.get('.tiptap').paste({ - pastePayload: 'https://example1.com, https://example2.com/foobar, (http://example3.com/foobar)', - pasteType: 'text/plain', - }) - - cy.get('.tiptap').find('a[href="https://example1.com"]').should('contain', 'https://example1.com') - - cy.get('.tiptap').find('a[href="https://example2.com/foobar"]').should('contain', 'https://example2.com/foobar') - - cy.get('.tiptap').find('a[href="http://example3.com/foobar"]').should('contain', 'http://example3.com/foobar') - }) - - it('should not allow links with disallowed protocols', () => { - const disallowedProtocols = ['ftp://example.com', 'file:///example.txt', 'mailto:test@example.com'] - - disallowedProtocols.forEach(url => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent(``) - expect(editor.getHTML()).to.not.include(url) - }) - }) - }) - - it('should not allow links with disallowed domains', () => { - const disallowedDomains = ['https://example-phishing.com', 'https://malicious-site.net'] - - disallowedDomains.forEach(url => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent(``) - expect(editor.getHTML()).to.not.include(url) - }) - }) - }) - - it('should not auto-link a URL from a disallowed domain', () => { - cy.get('.tiptap').type('https://example-phishing.com ') // disallowed domain - cy.get('.tiptap').should('not.have.descendants', 'a') - cy.get('.tiptap').should('contain.text', 'https://example-phishing.com') - }) -}) diff --git a/demos/src/Marks/Link/Vue/index.spec.js b/demos/src/Marks/Link/Vue/index.spec.js deleted file mode 100644 index ffb1136984..0000000000 --- a/demos/src/Marks/Link/Vue/index.spec.js +++ /dev/null @@ -1,132 +0,0 @@ -context('/src/Marks/Link/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Link/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example TextDEFAULT
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse a tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq( - '', - ) - }) - }) - - it('should parse a tags with target attribute correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq( - '', - ) - }) - }) - - it('should parse a tags with rel attribute correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq('') - }) - }) - - it('the button should add a link to the selected text', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('https://tiptap.dev') - - cy.get('button:first').click() - - cy.window().its('prompt').should('be.called') - - cy.get('.tiptap') - .find('a') - .should('contain', 'Example TextDEFAULT') - .should('have.attr', 'href', 'https://tiptap.dev') - }) - }) - - it('detects a pasted URL within a text', () => { - cy.get('.tiptap') - .paste({ pastePayload: 'some text https://example1.com around an url', pasteType: 'text/plain' }) - .find('a') - .should('contain', 'https://example1.com') - .should('have.attr', 'href', 'https://example1.com') - }) - - it('detects a pasted URL', () => { - cy.get('.tiptap') - .type('{backspace}') - .paste({ pastePayload: 'https://example2.com', pasteType: 'text/plain' }) - .find('a') - .should('contain', 'https://example2.com') - .should('have.attr', 'href', 'https://example2.com') - }) - - it('should allow exiting the link once set', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - cy.get('.tiptap').type('{rightArrow}') - - cy.get('button:first').should('not.have.class', 'is-active') - }) - }) - - it('detects autolinking', () => { - cy.get('.tiptap') - .type('https://example.com ') - .find('a') - .should('contain', 'https://example.com') - .should('have.attr', 'href', 'https://example.com') - }) - - it('detects autolinking with numbers', () => { - cy.get('.tiptap') - .type('https://tiptap4u.com ') - .find('a') - .should('contain', 'https://tiptap4u.com') - .should('have.attr', 'href', 'https://tiptap4u.com') - }) - - it('uses the default protocol', () => { - cy.get('.tiptap') - .type('example.com ') - .find('a') - .should('contain', 'example.com') - .should('have.attr', 'href', 'https://example.com') - }) - - it('uses a non-default protocol if present', () => { - cy.get('.tiptap') - .type('http://example.com ') - .find('a') - .should('contain', 'http://example.com') - .should('have.attr', 'href', 'http://example.com') - }) - - it('detects a pasted URL with query params', () => { - cy.get('.tiptap') - .type('{backspace}') - .paste({ pastePayload: 'https://example.com?paramA=nice¶mB=cool', pasteType: 'text/plain' }) - .find('a') - .should('contain', 'https://example.com?paramA=nice¶mB=cool') - .should('have.attr', 'href', 'https://example.com?paramA=nice¶mB=cool') - }) - - it('correctly detects multiple pasted URLs', () => { - cy.get('.tiptap').paste({ - pastePayload: 'https://example1.com, https://example2.com/foobar, (http://example3.com/foobar)', - pasteType: 'text/plain', - }) - - cy.get('.tiptap').find('a[href="https://example1.com"]').should('contain', 'https://example1.com') - - cy.get('.tiptap').find('a[href="https://example2.com/foobar"]').should('contain', 'https://example2.com/foobar') - - cy.get('.tiptap').find('a[href="http://example3.com/foobar"]').should('contain', 'http://example3.com/foobar') - }) -}) diff --git a/demos/src/Marks/Link/index.spec.ts b/demos/src/Marks/Link/index.spec.ts new file mode 100644 index 0000000000..a87a675799 --- /dev/null +++ b/demos/src/Marks/Link/index.spec.ts @@ -0,0 +1,143 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'Link' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Marks' + +async function paste(editor: ReturnTypeExample TextDEFAULT
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses a tags correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('') + return el.editor.getHTML() + }) + expect(html).toBe( + '', + ) + }) + + test('parses a tags with target attribute', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('') + return el.editor.getHTML() + }) + expect(html).toBe( + '', + ) + }) + + test('parses a tags with rel attribute', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('') + return el.editor.getHTML() + }) + expect(html).toBe('') + }) + + test('button adds a link to selected text', async ({ page }) => { + await page.locator('button').first().click() + await expect(page.locator('.tiptap a')).toContainText('Example TextDEFAULT') + await expect(page.locator('.tiptap a')).toHaveAttribute('href', 'https://tiptap.dev') + }) + + test('detects autolinking', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('https://example.com ') + await expect(page.locator('.tiptap a')).toHaveAttribute('href', 'https://example.com') + }) + + test('detects autolinking with numbers', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('https://tiptap4u.com ') + await expect(page.locator('.tiptap a')).toHaveAttribute('href', 'https://tiptap4u.com') + }) + + test('uses the default protocol', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('example.com ') + await expect(page.locator('.tiptap a')).toHaveAttribute('href', 'https://example.com') + }) + + test('uses a non-default protocol if present', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('http://example.com ') + await expect(page.locator('.tiptap a')).toHaveAttribute('href', 'http://example.com') + }) + + test('detects a pasted URL within text', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await paste(editor, 'some text https://example1.com around an url') + await expect(page.locator('.tiptap a')).toHaveAttribute('href', 'https://example1.com') + }) + + test('detects a pasted URL with query params', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await paste(editor, 'https://example.com?paramA=nice¶mB=cool') + await expect(page.locator('.tiptap a')).toHaveAttribute('href', 'https://example.com?paramA=nice¶mB=cool') + }) + + if (frameworkPath === 'React') { + test('disallows links with disallowed protocols', async ({ page }) => { + const editor = await getEditor(page) + const disallowed = ['ftp://example.com', 'file:///example.txt', 'mailto:test@example.com'] + const htmls = await editor.evaluate((el: any, urls: string[]) => { + return urls.map(url => { + el.editor.commands.setContent(``) + return el.editor.getHTML() + }) + }, disallowed) + htmls.forEach((html, i) => { + expect(html).not.toContain(disallowed[i]) + }) + }) + + test('does not auto-link a URL from a disallowed domain', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('https://example-phishing.com ') + await expect(page.locator('.tiptap a')).toHaveCount(0) + await expect(page.locator('.tiptap')).toContainText('https://example-phishing.com') + }) + } + }) + }) +}) diff --git a/demos/src/Marks/Strike/React/index.spec.js b/demos/src/Marks/Strike/React/index.spec.js deleted file mode 100644 index ab756815e4..0000000000 --- a/demos/src/Marks/Strike/React/index.spec.js +++ /dev/null @@ -1,75 +0,0 @@ -context('/src/Marks/Strike/React/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Strike/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse s tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
') - expect(editor.getHTML()).to.eq('Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse s tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
') - expect(editor.getHTML()).to.eq('Example Text
Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + ;[ + ['Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
', 'Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should transform inline style to sub tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('sould omit inline style with a different vertical align', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text bold', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('sub').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text bold', () => { - cy.get('button:first').click() - cy.get('.tiptap').type('{selectall}') - cy.get('button:first').click() - cy.get('.tiptap sub').should('not.exist') - }) -}) diff --git a/demos/src/Marks/Subscript/Vue/index.spec.js b/demos/src/Marks/Subscript/Vue/index.spec.js deleted file mode 100644 index 7acdfd049e..0000000000 --- a/demos/src/Marks/Subscript/Vue/index.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -context('/src/Marks/Subscript/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Subscript/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should transform inline style to sub tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('sould omit inline style with a different vertical align', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text bold', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('sub').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text bold', () => { - cy.get('button:first').click() - cy.get('.tiptap').type('{selectall}') - cy.get('button:first').click() - cy.get('.tiptap sub').should('not.exist') - }) -}) diff --git a/demos/src/Marks/Superscript/React/index.spec.js b/demos/src/Marks/Superscript/React/index.spec.js deleted file mode 100644 index 1bb63c4053..0000000000 --- a/demos/src/Marks/Superscript/React/index.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -context('/src/Marks/Superscript/React/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Superscript/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should transform inline style to sup tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('sould omit inline style with a different vertical align', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text bold', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('sup').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text bold', () => { - cy.get('button:first').click() - cy.get('.tiptap').type('{selectall}') - cy.get('button:first').click() - cy.get('.tiptap sup').should('not.exist') - }) -}) diff --git a/demos/src/Marks/Superscript/Vue/index.spec.js b/demos/src/Marks/Superscript/Vue/index.spec.js deleted file mode 100644 index d527eda8d1..0000000000 --- a/demos/src/Marks/Superscript/Vue/index.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -context('/src/Marks/Superscript/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Superscript/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should transform inline style to sup tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('sould omit inline style with a different vertical align', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should make the selected text bold', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('sup').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text bold', () => { - cy.get('button:first').click() - cy.get('.tiptap').type('{selectall}') - cy.get('button:first').click() - cy.get('.tiptap sup').should('not.exist') - }) -}) diff --git a/demos/src/Marks/TextStyle/React/index.spec.js b/demos/src/Marks/TextStyle/React/index.spec.js deleted file mode 100644 index c92bb3aef1..0000000000 --- a/demos/src/Marks/TextStyle/React/index.spec.js +++ /dev/null @@ -1,82 +0,0 @@ -context('/src/Marks/TextStyle/React/', () => { - beforeEach(() => { - cy.visit('/src/Marks/TextStyle/React/') - }) - - describe('mergeNestedSpanStyles', () => { - it('should merge styles of a span with one child span', () => { - cy.get('.tiptap > p:nth-child(4) > span') - .should('have.length', 1) - .and('have.text', 'red serif') - .and('have.attr', 'style', 'color: #FF0000; font-family: serif') - }) - it('should merge styles of a span with one nested child span into the descendant span', () => { - cy.get('.tiptap > p:nth-child(5) > span') - .should('have.length', 1) - .and('have.text', 'blue serif') - .and('have.attr', 'style', 'color: #0000FF; font-family: serif') - }) - it('should merge styles of a span with multiple child spans into all child spans', () => { - cy.get('.tiptap > p:nth-child(6) > span').should('have.length', 2) - cy.get('.tiptap > p:nth-child(6) > span:nth-child(1)') - .should('have.text', 'green serif ') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - cy.get('.tiptap > p:nth-child(6) > span:nth-child(2)') - .should('have.text', 'red serif') - .and('have.attr', 'style', 'color: #FF0000; font-family: serif') - }) - it('should merge styles of descendant spans into each descendant span when the parent span has no style', () => { - cy.get('.tiptap > p:nth-child(7) > span').should('have.length', 4) - cy.get('.tiptap > p:nth-child(7) > span:nth-child(1)') - .should('have.text', 'blue') - .and('have.attr', 'style', 'color: #0000FF') - cy.get('.tiptap > p:nth-child(7) > span:nth-child(2)') - .should('have.text', 'green ') - .and('have.attr', 'style', 'color: #00FF00') - cy.get('.tiptap > p:nth-child(7) > span:nth-child(3)') - .should('have.text', 'green serif') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - }) - it('should merge styles of a span with nested root text and descendant spans into each descendant span', () => { - cy.get('.tiptap > p:nth-child(8) > span').should('have.length', 4) - cy.get('.tiptap > p:nth-child(8) > span:nth-child(1)') - .should('have.text', 'blue ') - .and('have.attr', 'style', 'color: #0000FF') - cy.get('.tiptap > p:nth-child(8) > span:nth-child(2)') - .should('have.text', 'green ') - .and('have.attr', 'style', 'color: #00FF00') - cy.get('.tiptap > p:nth-child(8) > span:nth-child(3)') - .should('have.text', 'green serif ') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - cy.get('.tiptap > p:nth-child(8) > span:nth-child(4)') - .should('have.text', 'blue serif') - .and('have.attr', 'style', 'color: #0000FF; font-family: serif') - }) - it('should merge styles of descendant spans into each descendant span when the parent span has other tags', () => { - cy.get('.tiptap > p:nth-child(9) > span').should('have.length', 4) - cy.get('.tiptap > p:nth-child(9) > :nth-child(1)') - .should('have.prop', 'tagName', 'STRONG') - .and('have.text', 'strong ') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(2)') - .should('have.text', 'strong blue ') - .and('have.attr', 'style', 'color: #0000FF') - .find('strong') - .should('exist') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(3)') - .should('have.text', 'strong blue serif ') - .and('have.attr', 'style', 'color: #0000FF; font-family: serif; font-size: 24px') - .find('strong') - .should('exist') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(4)') - .should('have.text', 'strong green ') - .and('have.attr', 'style', 'color: #00FF00') - .find('strong') - .should('exist') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(5)') - .should('have.text', 'strong green serif') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - .find('strong') - .should('exist') - }) - }) -}) diff --git a/demos/src/Marks/TextStyle/Vue/index.spec.js b/demos/src/Marks/TextStyle/Vue/index.spec.js deleted file mode 100644 index 8bedf5707b..0000000000 --- a/demos/src/Marks/TextStyle/Vue/index.spec.js +++ /dev/null @@ -1,82 +0,0 @@ -context('/src/Marks/TextStyle/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/TextStyle/Vue/') - }) - - describe('mergeNestedSpanStyles', () => { - it('should merge styles of a span with one child span', () => { - cy.get('.tiptap > p:nth-child(4) > span') - .should('have.length', 1) - .and('have.text', 'red serif') - .and('have.attr', 'style', 'color: #FF0000; font-family: serif') - }) - it('should merge styles of a span with one nested child span into the descendant span', () => { - cy.get('.tiptap > p:nth-child(5) > span') - .should('have.length', 1) - .and('have.text', 'blue serif') - .and('have.attr', 'style', 'color: #0000FF; font-family: serif') - }) - it('should merge styles of a span with multiple child spans into all child spans', () => { - cy.get('.tiptap > p:nth-child(6) > span').should('have.length', 2) - cy.get('.tiptap > p:nth-child(6) > span:nth-child(1)') - .should('have.text', 'green serif ') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - cy.get('.tiptap > p:nth-child(6) > span:nth-child(2)') - .should('have.text', 'red serif') - .and('have.attr', 'style', 'color: #FF0000; font-family: serif') - }) - it('should merge styles of descendant spans into each descendant span when the parent span has no style', () => { - cy.get('.tiptap > p:nth-child(7) > span').should('have.length', 4) - cy.get('.tiptap > p:nth-child(7) > span:nth-child(1)') - .should('have.text', 'blue') - .and('have.attr', 'style', 'color: #0000FF') - cy.get('.tiptap > p:nth-child(7) > span:nth-child(2)') - .should('have.text', 'green ') - .and('have.attr', 'style', 'color: #00FF00') - cy.get('.tiptap > p:nth-child(7) > span:nth-child(3)') - .should('have.text', 'green serif') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - }) - it('should merge styles of a span with nested root text and descendant spans into each descendant span', () => { - cy.get('.tiptap > p:nth-child(8) > span').should('have.length', 4) - cy.get('.tiptap > p:nth-child(8) > span:nth-child(1)') - .should('have.text', 'blue ') - .and('have.attr', 'style', 'color: #0000FF') - cy.get('.tiptap > p:nth-child(8) > span:nth-child(2)') - .should('have.text', 'green ') - .and('have.attr', 'style', 'color: #00FF00') - cy.get('.tiptap > p:nth-child(8) > span:nth-child(3)') - .should('have.text', 'green serif ') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - cy.get('.tiptap > p:nth-child(8) > span:nth-child(4)') - .should('have.text', 'blue serif') - .and('have.attr', 'style', 'color: #0000FF; font-family: serif') - }) - it('should merge styles of descendant spans into each descendant span when the parent span has other tags', () => { - cy.get('.tiptap > p:nth-child(9) > span').should('have.length', 4) - cy.get('.tiptap > p:nth-child(9) > :nth-child(1)') - .should('have.prop', 'tagName', 'STRONG') - .and('have.text', 'strong ') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(2)') - .should('have.text', 'strong blue ') - .and('have.attr', 'style', 'color: #0000FF') - .find('strong') - .should('exist') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(3)') - .should('have.text', 'strong blue serif ') - .and('have.attr', 'style', 'color: #0000FF; font-family: serif; font-size: 24px') - .find('strong') - .should('exist') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(4)') - .should('have.text', 'strong green ') - .and('have.attr', 'style', 'color: #00FF00') - .find('strong') - .should('exist') - cy.get('.tiptap > p:nth-child(9) > span:nth-child(5)') - .should('have.text', 'strong green serif') - .and('have.attr', 'style', 'color: #00FF00; font-family: serif') - .find('strong') - .should('exist') - }) - }) -}) diff --git a/demos/src/Marks/Underline/React/index.spec.js b/demos/src/Marks/Underline/React/index.spec.js deleted file mode 100644 index 786d1eee93..0000000000 --- a/demos/src/Marks/Underline/React/index.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -context('/src/Marks/Underline/React/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Underline/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse u tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should transform any tag with text decoration underline to u tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should underline the selected text', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('u').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text underline', () => { - cy.get('button:first').click() - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap').find('u').should('not.exist') - }) - - it('should underline the selected text when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'u' }).find('u').should('contain', 'Example Text') - }) - - it('should toggle the selected text underline when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, key: 'u' }) - .trigger('keydown', { modKey: true, key: 'u' }) - .find('u') - .should('not.exist') - }) -}) diff --git a/demos/src/Marks/Underline/Vue/index.spec.js b/demos/src/Marks/Underline/Vue/index.spec.js deleted file mode 100644 index 7f4b0443e1..0000000000 --- a/demos/src/Marks/Underline/Vue/index.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -context('/src/Marks/Underline/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Marks/Underline/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse u tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should transform any tag with text decoration underline to u tags', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('the button should underline the selected text', () => { - cy.get('button:first').click() - - cy.get('.tiptap').find('u').should('contain', 'Example Text') - }) - - it('the button should toggle the selected text underline', () => { - cy.get('button:first').click() - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap').find('u').should('not.exist') - }) - - it('should underline the selected text when the keyboard shortcut is pressed', () => { - cy.get('.tiptap').trigger('keydown', { modKey: true, key: 'u' }).find('u').should('contain', 'Example Text') - }) - - it('should toggle the selected text underline when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, key: 'u' }) - .trigger('keydown', { modKey: true, key: 'u' }) - .find('u') - .should('not.exist') - }) -}) diff --git a/demos/src/Marks/Underline/index.spec.ts b/demos/src/Marks/Underline/index.spec.ts new file mode 100644 index 0000000000..8767988a9c --- /dev/null +++ b/demos/src/Marks/Underline/index.spec.ts @@ -0,0 +1,66 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'Underline' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Marks' + +const isMac = process.platform === 'darwin' +const mod = isMac ? 'Meta' : 'Control' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await setEditorContent(page, 'Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses u tags', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('transforms any tag with text-decoration underline to u tags', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('button underlines selected text', async ({ page }) => { + await page.locator('button').first().click() + await expect(page.locator('.tiptap u')).toContainText('Example Text') + }) + + test('button toggles underline on selected text', async ({ page }) => { + const editor = await getEditor(page) + await page.locator('button').first().click() + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + await page.locator('button').first().click() + await expect(page.locator('.tiptap u')).toHaveCount(0) + }) + + test('keyboard shortcut underlines selected text', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.commands.focus() + el.editor.commands.selectAll() + }) + await editor.press(`${mod}+u`) + await expect(page.locator('.tiptap u')).toContainText('Example Text') + }) + }) + }) +}) diff --git a/demos/src/Nodes/Blockquote/React/index.spec.js b/demos/src/Nodes/Blockquote/React/index.spec.js deleted file mode 100644 index 5a641b17ff..0000000000 --- a/demos/src/Nodes/Blockquote/React/index.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -context('/src/Nodes/Blockquote/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Blockquote/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse blockquote tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse blockquote tags without paragraphs correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text') - expect(editor.getHTML()).to.eq('
') - }) - }) - - it('the button should make the selected line a blockquote', () => { - cy.get('.tiptap blockquote').should('not.exist') - - cy.get('button:first').click() - - cy.get('.tiptap').find('blockquote').should('contain', 'Example Text') - }) - - it('the button should wrap all nodes in one blockquote', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - - cy.get('button:first').click() - - cy.get('.tiptap').find('blockquote').should('have.length', 1) - }) - - it('the button should toggle the blockquote', () => { - cy.get('.tiptap blockquote').should('not.exist') - - cy.get('button:first').click() - - cy.get('.tiptap').find('blockquote').should('contain', 'Example Text') - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap blockquote').should('not.exist') - }) - - it('should make the selected line a blockquote when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { shiftKey: true, modKey: true, key: 'b' }) - .find('blockquote') - .should('contain', 'Example Text') - }) - - it('should toggle the blockquote when the keyboard shortcut is pressed', () => { - cy.get('.tiptap blockquote').should('not.exist') - - cy.get('.tiptap') - .trigger('keydown', { shiftKey: true, modKey: true, key: 'b' }) - .find('blockquote') - .should('contain', 'Example Text') - - cy.get('.tiptap').type('{selectall}').trigger('keydown', { shiftKey: true, modKey: true, key: 'b' }) - - cy.get('.tiptap blockquote').should('not.exist') - }) - - it('should make a blockquote from markdown shortcuts', () => { - cy.get('.tiptap').type('> Quote').find('blockquote').should('contain', 'Quote') - }) -}) diff --git a/demos/src/Nodes/Blockquote/Vue/index.spec.js b/demos/src/Nodes/Blockquote/Vue/index.spec.js deleted file mode 100644 index 8b5ccd34f9..0000000000 --- a/demos/src/Nodes/Blockquote/Vue/index.spec.js +++ /dev/null @@ -1,83 +0,0 @@ -context('/src/Nodes/Blockquote/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Blockquote/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse blockquote tags correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('should parse blockquote tags without paragraphs correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text') - expect(editor.getHTML()).to.eq('
') - }) - }) - - it('the button should make the selected line a blockquote', () => { - cy.get('.tiptap blockquote').should('not.exist') - - cy.get('button:first').click() - - cy.get('.tiptap').find('blockquote').should('contain', 'Example Text') - }) - - it('the button should wrap all nodes in one blockquote', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - - cy.get('button:first').click() - - cy.get('.tiptap').find('blockquote').should('have.length', 1) - }) - - it('the button should toggle the blockquote', () => { - cy.get('.tiptap blockquote').should('not.exist') - - cy.get('button:first').click() - - cy.get('.tiptap').find('blockquote').should('contain', 'Example Text') - - cy.get('.tiptap').type('{selectall}') - - cy.get('button:first').click() - - cy.get('.tiptap blockquote').should('not.exist') - }) - - it('should make the selected line a blockquote when the keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { shiftKey: true, modKey: true, key: 'b' }) - .find('blockquote') - .should('contain', 'Example Text') - }) - - it('should toggle the blockquote when the keyboard shortcut is pressed', () => { - cy.get('.tiptap blockquote').should('not.exist') - - cy.get('.tiptap') - .trigger('keydown', { shiftKey: true, modKey: true, key: 'b' }) - .find('blockquote') - .should('contain', 'Example Text') - - cy.get('.tiptap').type('{selectall}').trigger('keydown', { shiftKey: true, modKey: true, key: 'b' }) - - cy.get('.tiptap blockquote').should('not.exist') - }) - - it('should make a blockquote from markdown shortcuts', () => { - cy.get('.tiptap').type('> Quote').find('blockquote').should('contain', 'Quote') - }) -}) diff --git a/demos/src/Nodes/Blockquote/index.spec.ts b/demos/src/Nodes/Blockquote/index.spec.ts new file mode 100644 index 0000000000..468f7f0f73 --- /dev/null +++ b/demos/src/Nodes/Blockquote/index.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'Blockquote' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Nodes' + +const isMac = process.platform === 'darwin' +const mod = isMac ? 'Meta' : 'Control' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await setEditorContent(page, 'Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses blockquote tags correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('') + return el.editor.getHTML() + }) + expect(html).toBe('Example Text
') + }) + + test('parses blockquote tags without paragraphs correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
Example Text') + return el.editor.getHTML() + }) + expect(html).toBe('
') + }) + + test('button makes the selected line a blockquote', async ({ page }) => { + await expect(page.locator('.tiptap blockquote')).toHaveCount(0) + await page.locator('button').first().click() + await expect(page.locator('.tiptap blockquote')).toContainText('Example Text') + }) + + test('button wraps all nodes in one blockquote', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
Example Text
Example Text
') + el.editor.commands.selectAll() + }) + await page.locator('button').first().click() + await expect(page.locator('.tiptap blockquote')).toHaveCount(1) + }) + + test('button toggles the blockquote', async ({ page }) => { + const editor = await getEditor(page) + await page.locator('button').first().click() + await expect(page.locator('.tiptap blockquote')).toContainText('Example Text') + const isActive = await editor.evaluate((el: any) => el.editor.isActive('blockquote')) + expect(isActive).toBe(true) + }) + + test('keyboard shortcut makes selected line a blockquote', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.commands.focus() + el.editor.commands.selectAll() + }) + await editor.press(`${mod}+Shift+b`) + await expect(page.locator('.tiptap blockquote')).toContainText('Example Text') + }) + + test('markdown shortcut creates blockquote', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('> Quote') + await expect(page.locator('.tiptap blockquote')).toContainText('Quote') + }) + }) + }) +}) diff --git a/demos/src/Nodes/BulletList/React/index.spec.js b/demos/src/Nodes/BulletList/React/index.spec.js deleted file mode 100644 index d17f13bffa..0000000000 --- a/demos/src/Nodes/BulletList/React/index.spec.js +++ /dev/null @@ -1,115 +0,0 @@ -context('/src/Nodes/BulletList/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/BulletList/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse unordered lists correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse unordered lists correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses unordered lists correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse code blocks correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text')
- expect(editor.getHTML()).to.eq('Example Text')
- })
- })
-
- it('should parse code blocks with language correctly', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.setContent('Example Text')
- expect(editor.getHTML()).to.eq('Example Text')
- })
- })
-
- it('the button should make the selected line a code block', () => {
- cy.get('button:first').click()
-
- cy.get('.tiptap').find('pre').should('contain', 'Example Text')
- })
-
- it('the button should toggle the code block', () => {
- cy.get('button:first').click()
-
- cy.get('.tiptap').find('pre').should('contain', 'Example Text')
-
- cy.get('.tiptap').type('{selectall}')
-
- cy.get('button:first').click()
-
- cy.get('.tiptap pre').should('not.exist')
- })
-
- it('the keyboard shortcut should make the selected line a code block', () => {
- cy.get('.tiptap')
- .trigger('keydown', { modKey: true, altKey: true, key: 'c' })
- .find('pre')
- .should('contain', 'Example Text')
- })
-
- it('the keyboard shortcut should toggle the code block', () => {
- cy.get('.tiptap')
- .trigger('keydown', { modKey: true, altKey: true, key: 'c' })
- .find('pre')
- .should('contain', 'Example Text')
-
- cy.get('.tiptap').type('{selectall}').trigger('keydown', { modKey: true, altKey: true, key: 'c' })
-
- cy.get('.tiptap pre').should('not.exist')
- })
-
- it('should parse the language from a HTML code block', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.setContent('body { display: none; }')
-
- cy.get('.tiptap').find('pre>code.language-css').should('have.length', 1)
- })
- })
-
- it('should make a code block from backtick markdown shortcuts', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('``` Code').find('pre>code').should('contain', 'Code')
- })
- })
-
- it('should make a code block from tilde markdown shortcuts', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('~~~ Code').find('pre>code').should('contain', 'Code')
- })
- })
-
- it('should make a code block for js with backticks', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('```js Code').find('pre>code.language-js').should('contain', 'Code')
- })
- })
-
- it('should make a code block for js with tildes', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('~~~js Code').find('pre>code.language-js').should('contain', 'Code')
- })
- })
-
- it('should make a code block from backtick markdown shortcuts followed by enter', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('```{enter}Code').find('pre>code').should('contain', 'Code')
- })
- })
-
- it('reverts the markdown shortcut when pressing backspace', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('``` {backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-
- it('removes the code block when pressing backspace', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap pre').should('not.exist')
-
- cy.get('.tiptap').type('Paragraph{enter}``` A{backspace}{backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-
- it('removes the code block when pressing backspace, even with blank lines', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap pre').should('not.exist')
-
- cy.get('.tiptap').type('Paragraph{enter}{enter}``` A{backspace}{backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-
- it('removes the code block when pressing backspace, even at start of document', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap pre').should('not.exist')
-
- cy.get('.tiptap').type('``` A{leftArrow}{backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-})
diff --git a/demos/src/Nodes/CodeBlock/Vue/index.spec.js b/demos/src/Nodes/CodeBlock/Vue/index.spec.js
deleted file mode 100644
index 72e341c152..0000000000
--- a/demos/src/Nodes/CodeBlock/Vue/index.spec.js
+++ /dev/null
@@ -1,156 +0,0 @@
-context('/src/Nodes/CodeBlock/Vue/', () => {
- beforeEach(() => {
- cy.visit('/src/Nodes/CodeBlock/Vue/')
- })
-
- beforeEach(() => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse code blocks correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text')
- expect(editor.getHTML()).to.eq('Example Text')
- })
- })
-
- it('should parse code blocks with language correctly', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.setContent('Example Text')
- expect(editor.getHTML()).to.eq('Example Text')
- })
- })
-
- it('the button should make the selected line a code block', () => {
- cy.get('button:first').click()
-
- cy.get('.tiptap').find('pre').should('contain', 'Example Text')
- })
-
- it('the button should toggle the code block', () => {
- cy.get('button:first').click()
-
- cy.get('.tiptap').find('pre').should('contain', 'Example Text')
-
- cy.get('.tiptap').type('{selectall}')
-
- cy.get('button:first').click()
-
- cy.get('.tiptap pre').should('not.exist')
- })
-
- it('the keyboard shortcut should make the selected line a code block', () => {
- cy.get('.tiptap')
- .trigger('keydown', { modKey: true, altKey: true, key: 'c' })
- .find('pre')
- .should('contain', 'Example Text')
- })
-
- it('the keyboard shortcut should toggle the code block', () => {
- cy.get('.tiptap')
- .trigger('keydown', { modKey: true, altKey: true, key: 'c' })
- .find('pre')
- .should('contain', 'Example Text')
-
- cy.get('.tiptap').type('{selectall}').trigger('keydown', { modKey: true, altKey: true, key: 'c' })
-
- cy.get('.tiptap pre').should('not.exist')
- })
-
- it('should parse the language from a HTML code block', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.setContent('body { display: none; }')
-
- cy.get('.tiptap').find('pre>code.language-css').should('have.length', 1)
- })
- })
-
- it('should make a code block from backtick markdown shortcuts', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('``` Code').find('pre>code').should('contain', 'Code')
- })
- })
-
- it('should make a code block from tilde markdown shortcuts', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('~~~ Code').find('pre>code').should('contain', 'Code')
- })
- })
-
- it('should make a code block for js with backticks', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('```js Code').find('pre>code.language-js').should('contain', 'Code')
- })
- })
-
- it('should make a code block for js with tildes', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('~~~js Code').find('pre>code.language-js').should('contain', 'Code')
- })
- })
-
- it('should make a code block from backtick markdown shortcuts followed by enter', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('```{enter}Code').find('pre>code').should('contain', 'Code')
- })
- })
-
- it('reverts the markdown shortcut when pressing backspace', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap').type('``` {backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-
- it('removes the code block when pressing backspace', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap pre').should('not.exist')
-
- cy.get('.tiptap').type('Paragraph{enter}``` A{backspace}{backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-
- it('removes the code block when pressing backspace, even with blank lines', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap pre').should('not.exist')
-
- cy.get('.tiptap').type('Paragraph{enter}{enter}``` A{backspace}{backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-
- it('removes the code block when pressing backspace, even at start of document', () => {
- cy.get('.tiptap').then(([{ editor }]) => {
- editor.commands.clearContent()
-
- cy.get('.tiptap pre').should('not.exist')
-
- cy.get('.tiptap').type('``` A{leftArrow}{backspace}')
-
- cy.get('.tiptap pre').should('not.exist')
- })
- })
-})
diff --git a/demos/src/Nodes/CodeBlock/index.spec.ts b/demos/src/Nodes/CodeBlock/index.spec.ts
new file mode 100644
index 0000000000..2ca8bc6ced
--- /dev/null
+++ b/demos/src/Nodes/CodeBlock/index.spec.ts
@@ -0,0 +1,108 @@
+import { expect, test } from '@playwright/test'
+
+import { getEditor, setEditorContent } from '../../../test/helpers.js'
+
+const demoName = 'CodeBlock'
+const frameworkPaths = ['React', 'Vue']
+const demoPath = '/src/Nodes'
+
+const isMac = process.platform === 'darwin'
+const mod = isMac ? 'Meta' : 'Control'
+
+test.describe(`${demoPath}/${demoName}`, () => {
+ frameworkPaths.forEach(frameworkPath => {
+ const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/`
+
+ test.describe(`${frameworkPath}`, () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto(fullDemoPath)
+ await setEditorContent(page, 'Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses code blocks correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text')
+ return el.editor.getHTML()
+ })
+ expect(html).toBe('Example Text')
+ })
+
+ test('parses code blocks with language correctly', async ({ page }) => {
+ const editor = await getEditor(page)
+ const html = await editor.evaluate((el: any) => {
+ el.editor.commands.setContent('Example Text')
+ return el.editor.getHTML()
+ })
+ expect(html).toBe('Example Text')
+ })
+
+ test('button makes the selected line a code block', async ({ page }) => {
+ await page.locator('button').first().click()
+ await expect(page.locator('.tiptap pre')).toContainText('Example Text')
+ })
+
+ test('button toggles the code block', async ({ page }) => {
+ const editor = await getEditor(page)
+ await page.locator('button').first().click()
+ await expect(page.locator('.tiptap pre')).toContainText('Example Text')
+ await editor.evaluate((el: any) => el.editor.commands.selectAll())
+ await page.locator('button').first().click()
+ await expect(page.locator('.tiptap pre')).toHaveCount(0)
+ })
+
+ test('keyboard shortcut creates a code block', async ({ page }) => {
+ const editor = await getEditor(page)
+ await editor.evaluate((el: any) => {
+ el.editor.commands.focus()
+ el.editor.commands.selectAll()
+ })
+ await editor.press(`${mod}+Alt+c`)
+ await expect(page.locator('.tiptap pre')).toContainText('Example Text')
+ })
+
+ test('parses language from HTML code block', async ({ page }) => {
+ const editor = await getEditor(page)
+ await editor.evaluate((el: any) =>
+ el.editor.commands.setContent('body { display: none; }'),
+ )
+ await expect(page.locator('.tiptap pre>code.language-css')).toHaveCount(1)
+ })
+
+ test('creates code block from backtick markdown shortcut', async ({ page }) => {
+ const editor = await getEditor(page)
+ await editor.evaluate((el: any) => el.editor.commands.clearContent())
+ await editor.click()
+ await page.keyboard.type('``` Code')
+ await expect(page.locator('.tiptap pre>code')).toContainText('Code')
+ })
+
+ test('creates code block from tilde markdown shortcut', async ({ page }) => {
+ const editor = await getEditor(page)
+ await editor.evaluate((el: any) => el.editor.commands.clearContent())
+ await editor.click()
+ await page.keyboard.type('~~~ Code')
+ await expect(page.locator('.tiptap pre>code')).toContainText('Code')
+ })
+
+ test('creates code block for js with backticks', async ({ page }) => {
+ const editor = await getEditor(page)
+ await editor.evaluate((el: any) => el.editor.commands.clearContent())
+ await editor.click()
+ await page.keyboard.type('```js Code')
+ await expect(page.locator('.tiptap pre>code.language-js')).toContainText('Code')
+ })
+
+ test('reverts the markdown shortcut when pressing backspace', async ({ page }) => {
+ const editor = await getEditor(page)
+ await editor.evaluate((el: any) => el.editor.commands.clearContent())
+ await editor.click()
+ await page.keyboard.type('``` ')
+ await page.keyboard.press('Backspace')
+ await expect(page.locator('.tiptap pre')).toHaveCount(0)
+ })
+ })
+ })
+})
diff --git a/demos/src/Nodes/Details/Vue/index.spec.js b/demos/src/Nodes/Details/Vue/index.spec.js
deleted file mode 100644
index 36836e601e..0000000000
--- a/demos/src/Nodes/Details/Vue/index.spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-context('/src/Nodes/Details/Vue/', () => {
- beforeEach(() => {
- cy.visit('/src/Nodes/Details/Vue/')
- cy.get('.ProseMirror').then(([{ editor }]) => {
- editor.commands.setContent('Example Text
') - cy.get('.ProseMirror').type('{selectall}') - }) - }) - - it('should parse details tags correctly', () => { - cy.get('.ProseMirror').then(([{ editor }]) => { - editor.commands.setContent('Content
Content
Content
Example Text
') - }) - }) - - it('should parse hard breaks correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example
Text
Example
Text
Example
Text
Example
Text
Example Text
') - }) - }) - - it('should parse hard breaks correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example
Text
Example
Text
Example
Text
Example
Text
Example Text
') + }) + + test('parses hard breaks correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example
Text
Example
Text
Example
Text
Example
Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - const headings = ['Example Text
') - }) - }) - - it('the button should make the selected line a h1', () => { - cy.get('.tiptap h1').should('not.exist') - - cy.get('button:nth-child(1)').click() - - cy.get('.tiptap').find('h1').should('contain', 'Example Text') - }) - - it('the button should make the selected line a h2', () => { - cy.get('.tiptap h2').should('not.exist') - - cy.get('button:nth-child(2)').click() - - cy.get('.tiptap').find('h2').should('contain', 'Example Text') - }) - - it('the button should make the selected line a h3', () => { - cy.get('.tiptap h3').should('not.exist') - - cy.get('button:nth-child(3)').click() - - cy.get('.tiptap').find('h3').should('contain', 'Example Text') - }) - - it('the button should toggle the heading', () => { - cy.get('.tiptap h1').should('not.exist') - - cy.get('button:nth-child(1)').click() - - cy.get('.tiptap').find('h1').should('contain', 'Example Text') - - cy.get('button:nth-child(1)').click() - - cy.get('.tiptap h1').should('not.exist') - }) - - it('should make the paragraph a h1 keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, altKey: true, key: '1' }) - .find('h1') - .should('contain', 'Example Text') - }) - - it('should make the paragraph a h2 keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, altKey: true, key: '2' }) - .find('h2') - .should('contain', 'Example Text') - }) - - it('should make the paragraph a h3 keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, altKey: true, key: '3' }) - .find('h3') - .should('contain', 'Example Text') - }) - - it('should make a h1 from the default markdown shortcut', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - - cy.get('.tiptap').type('# Headline').find('h1').should('contain', 'Headline') - }) - - it('should make a h2 from the default markdown shortcut', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - - cy.get('.tiptap').type('## Headline').find('h2').should('contain', 'Headline') - }) - - it('should make a h3 from the default markdown shortcut', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - - cy.get('.tiptap').type('### Headline').find('h3').should('contain', 'Headline') - }) -}) diff --git a/demos/src/Nodes/Heading/Vue/index.spec.js b/demos/src/Nodes/Heading/Vue/index.spec.js deleted file mode 100644 index b17ce5efc1..0000000000 --- a/demos/src/Nodes/Heading/Vue/index.spec.js +++ /dev/null @@ -1,111 +0,0 @@ -context('/src/Nodes/Heading/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Heading/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - const headings = ['Example Text
') - }) - }) - - it('the button should make the selected line a h1', () => { - cy.get('.tiptap h1').should('not.exist') - - cy.get('button:nth-child(1)').click() - - cy.get('.tiptap').find('h1').should('contain', 'Example Text') - }) - - it('the button should make the selected line a h2', () => { - cy.get('.tiptap h2').should('not.exist') - - cy.get('button:nth-child(2)').click() - - cy.get('.tiptap').find('h2').should('contain', 'Example Text') - }) - - it('the button should make the selected line a h3', () => { - cy.get('.tiptap h3').should('not.exist') - - cy.get('button:nth-child(3)').click() - - cy.get('.tiptap').find('h3').should('contain', 'Example Text') - }) - - it('the button should toggle the heading', () => { - cy.get('.tiptap h1').should('not.exist') - - cy.get('button:nth-child(1)').click() - - cy.get('.tiptap').find('h1').should('contain', 'Example Text') - - cy.get('button:nth-child(1)').click() - - cy.get('.tiptap h1').should('not.exist') - }) - - it('should make the paragraph a h1 keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, altKey: true, key: '1' }) - .find('h1') - .should('contain', 'Example Text') - }) - - it('should make the paragraph a h2 keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, altKey: true, key: '2' }) - .find('h2') - .should('contain', 'Example Text') - }) - - it('should make the paragraph a h3 keyboard shortcut is pressed', () => { - cy.get('.tiptap') - .trigger('keydown', { modKey: true, altKey: true, key: '3' }) - .find('h3') - .should('contain', 'Example Text') - }) - - it('should make a h1 from the default markdown shortcut', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - - cy.get('.tiptap').type('# Headline').find('h1').should('contain', 'Headline') - }) - - it('should make a h2 from the default markdown shortcut', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - - cy.get('.tiptap').type('## Headline').find('h2').should('contain', 'Headline') - }) - - it('should make a h3 from the default markdown shortcut', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - - cy.get('.tiptap').type('### Headline').find('h3').should('contain', 'Headline') - }) -}) diff --git a/demos/src/Nodes/Heading/index.spec.ts b/demos/src/Nodes/Heading/index.spec.ts new file mode 100644 index 0000000000..4472d4fffb --- /dev/null +++ b/demos/src/Nodes/Heading/index.spec.ts @@ -0,0 +1,81 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'Heading' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Nodes' + +const isMac = process.platform === 'darwin' +const mod = isMac ? 'Meta' : 'Control' + +const headings = ['Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + headings.forEach(html => { + test(`parses ${html} correctly`, async ({ page }) => { + const editor = await getEditor(page) + const result = await editor.evaluate((el: any, content: string) => { + el.editor.commands.setContent(content) + return el.editor.getHTML() + }, html) + expect(result).toBe(html) + }) + }) + + test('omits disabled heading levels', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
') + }) + ;[1, 2, 3].forEach(level => { + test(`button makes selected line h${level}`, async ({ page }) => { + await page + .locator('button') + .nth(level - 1) + .click() + await expect(page.locator(`.tiptap h${level}`)).toContainText('Example Text') + }) + + test(`keyboard shortcut makes paragraph h${level}`, async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => { + el.editor.commands.focus() + el.editor.commands.selectAll() + }) + await editor.press(`${mod}+Alt+${level}`) + await expect(page.locator(`.tiptap h${level}`)).toContainText('Example Text') + }) + + test(`creates h${level} from markdown shortcut`, async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type(`${'#'.repeat(level)} Headline`) + await expect(page.locator(`.tiptap h${level}`)).toContainText('Headline') + }) + }) + + test('button toggles the heading', async ({ page }) => { + await page.locator('button').nth(0).click() + await expect(page.locator('.tiptap h1')).toContainText('Example Text') + await page.locator('button').nth(0).click() + await expect(page.locator('.tiptap h1')).toHaveCount(0) + }) + }) + }) +}) diff --git a/demos/src/Nodes/HorizontalRule/React/index.spec.js b/demos/src/Nodes/HorizontalRule/React/index.spec.js deleted file mode 100644 index 30478b311c..0000000000 --- a/demos/src/Nodes/HorizontalRule/React/index.spec.js +++ /dev/null @@ -1,77 +0,0 @@ -context('/src/Nodes/HorizontalRule/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/HorizontalRule/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should parse horizontal rules correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
') - - // From the start of the document to the start of the second textblock. - editor.commands.setTextSelection({ from: 0, to: 15 }) - editor.commands.setHorizontalRule() - - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
Example Text
') - - // From the end of the first textblock to the start of the second textblock. - editor.commands.setTextSelection({ from: 13, to: 15 }) - editor.commands.setHorizontalRule() - - expect(editor.getHTML()).to.eq('Example Text
Example Text
') - }) - }) -}) diff --git a/demos/src/Nodes/HorizontalRule/Vue/index.spec.js b/demos/src/Nodes/HorizontalRule/Vue/index.spec.js deleted file mode 100644 index 2a495a6ba5..0000000000 --- a/demos/src/Nodes/HorizontalRule/Vue/index.spec.js +++ /dev/null @@ -1,77 +0,0 @@ -context('/src/Nodes/HorizontalRule/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/HorizontalRule/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - }) - }) - - it('should parse horizontal rules correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
') - - // From the start of the document to the start of the second textblock. - editor.commands.setTextSelection({ from: 0, to: 15 }) - editor.commands.setHorizontalRule() - - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
Example Text
') - - // From the end of the first textblock to the start of the second textblock. - editor.commands.setTextSelection({ from: 13, to: 15 }) - editor.commands.setHorizontalRule() - - expect(editor.getHTML()).to.eq('Example Text
Example Text
') - }) - }) -}) diff --git a/demos/src/Nodes/HorizontalRule/index.spec.ts b/demos/src/Nodes/HorizontalRule/index.spec.ts new file mode 100644 index 0000000000..f3a5eab49d --- /dev/null +++ b/demos/src/Nodes/HorizontalRule/index.spec.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test' + +import { getEditor, setEditorContent } from '../../../test/helpers.js' + +const demoName = 'HorizontalRule' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Nodes' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + await setEditorContent(page, 'Example Text
') + }) + + test('parses horizontal rules correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
Example Text
Example Text
') + el.editor.commands.setTextSelection({ from: 0, to: 15 }) + el.editor.commands.setHorizontalRule() + return el.editor.getHTML() + }) + expect(html1).toBe('Example Text
') + + const html2 = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
Example Text
') + el.editor.commands.setTextSelection({ from: 13, to: 15 }) + el.editor.commands.setHorizontalRule() + return el.editor.getHTML() + }) + expect(html2).toBe('Example Text
Example Text
') + }) + }) + }) +}) diff --git a/demos/src/Nodes/Image/React/index.spec.js b/demos/src/Nodes/Image/React/index.spec.js deleted file mode 100644 index 83a4c77c4d..0000000000 --- a/demos/src/Nodes/Image/React/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Nodes/Image/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Image/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should add an img tag with the correct URL', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('foobar.png') - - cy.get('button:first').click() - - cy.window().its('prompt').should('be.called') - - cy.get('.tiptap').find('img').should('have.attr', 'src', 'foobar.png') - }) - }) -}) diff --git a/demos/src/Nodes/Image/Vue/index.spec.js b/demos/src/Nodes/Image/Vue/index.spec.js deleted file mode 100644 index 6ff06dd117..0000000000 --- a/demos/src/Nodes/Image/Vue/index.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -context('/src/Nodes/Image/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Image/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should add an img tag with the correct URL', () => { - cy.window().then(win => { - cy.stub(win, 'prompt').returns('foobar.png') - - cy.get('button:first').click() - - cy.window().its('prompt').should('be.called') - - cy.get('.tiptap').find('img').should('have.attr', 'src', 'foobar.png') - }) - }) -}) diff --git a/demos/src/Nodes/ListItem/React/index.spec.js b/demos/src/Nodes/ListItem/React/index.spec.js deleted file mode 100644 index d64e8376c4..0000000000 --- a/demos/src/Nodes/ListItem/React/index.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -context('/src/Nodes/ListItem/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/ListItem/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('@John Doe
') - cy.get('.tiptap').should( - 'contain.html', - '@John Doe', - ) - }) - }) - - it('should insert multiple mentions', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent( - '@John Doe and @Jane Smith
', - ) - cy.get('.tiptap').should( - 'contain.html', - '@John Doe and @Jane Smith', - ) - }) - }) - - it("should open a dropdown menu when I type '@'", () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - }) - - it('should display the correct options in the dropdown menu', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button').should('have.length', 5) - cy.get('.dropdown-menu button:nth-child(1)').should('contain.text', 'Lea Thompson').and('have.class', 'is-selected') - cy.get('.dropdown-menu button:nth-child(2)').should('contain.text', 'Cyndi Lauper') - cy.get('.dropdown-menu button:nth-child(3)').should('contain.text', 'Tom Cruise') - cy.get('.dropdown-menu button:nth-child(4)').should('contain.text', 'Madonna') - cy.get('.dropdown-menu button:nth-child(5)').should('contain.text', 'Jerry Hall') - }) - - it('should insert Cyndi Lauper mention when clicking on her option', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button:nth-child(2)').contains('Cyndi Lauper').click() - - cy.get('.tiptap').should( - 'contain.html', - '@Cyndi Lauper', - ) - }) - - it('should close the dropdown menu when I move the cursor outside the editor', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{moveToStart}') - cy.get('.dropdown-menu').should('not.exist') - }) - - it('should close the dropdown menu when I press the exit key', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{esc}') - cy.get('.dropdown-menu').should('not.exist') - }) - - it('should insert Tom Cruise when selecting his option with the arrow keys and pressing the enter key', () => { - cy.get('.tiptap').type('{selectall}{backspace}@') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').type('{downarrow}{downarrow}') - cy.get('.dropdown-menu button:nth-child(3)').should('have.class', 'is-selected') - cy.get('.tiptap').type('{enter}') - - cy.get('.tiptap').should( - 'contain.html', - '@Tom Cruise', - ) - }) - - it('should show a "No result" message when I search for an option that is not in the list', () => { - cy.get('.tiptap').type('{selectall}{backspace}@nonexistent') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu').should('contain.text', 'No result') - }) - - it('should not hide the dropdown or insert any mention if I search for an option that is not in the list and hit enter', () => { - cy.get('.tiptap').type('{selectall}{backspace}@nonexistent') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu').should('contain.text', 'No result') - cy.get('.tiptap').type('{enter}') - cy.get('.dropdown-menu').should('exist') - cy.get('.tiptap').should('have.text', '@nonexistent') - cy.get('.tiptap span.mention').should('not.exist') - }) - - it('should only show the Madonna option in the dropdown when I type "@mado"', () => { - cy.get('.tiptap').type('{selectall}{backspace}@mado') - cy.get('.dropdown-menu').should('exist') - cy.get('.dropdown-menu button').should('have.length', 1) - cy.get('.dropdown-menu button:nth-child(1)').should('contain.text', 'Madonna') - }) - - it('should insert Madonna when I type "@mado" and hit enter', () => { - cy.get('.tiptap').type('{selectall}{backspace}@mado{enter}') - cy.get('.tiptap').should( - 'contain.html', - '@Madonna', - ) - }) -}) diff --git a/demos/src/Nodes/Mention/Vue/index.spec.js b/demos/src/Nodes/Mention/Vue/index.spec.js deleted file mode 100644 index 279adbb022..0000000000 --- a/demos/src/Nodes/Mention/Vue/index.spec.js +++ /dev/null @@ -1,7 +0,0 @@ -context('/src/Nodes/Mention/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Mention/Vue/') - }) - - // TODO: Write tests -}) diff --git a/demos/src/Nodes/Mention/index.spec.ts b/demos/src/Nodes/Mention/index.spec.ts new file mode 100644 index 0000000000..87e1bef07f --- /dev/null +++ b/demos/src/Nodes/Mention/index.spec.ts @@ -0,0 +1,109 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoPath = '/src/Nodes' +const demoName = 'Mention' + +test.describe(`${demoPath}/${demoName}`, () => { + test.describe('React', () => { + const fullDemoPath = `${demoPath}/${demoName}/React/` + + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + }) + + test('inserts a mention', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => + el.editor.commands.setContent( + '@John Doe
', + ), + ) + await expect(page.locator('.tiptap span.mention')).toHaveAttribute('data-id', '1') + await expect(page.locator('.tiptap span.mention')).toContainText('@John Doe') + }) + + test('opens a dropdown menu when @ is typed', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@') + await expect(page.locator('.dropdown-menu')).toBeVisible() + }) + + test('shows correct options in the dropdown menu', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@') + await expect(page.locator('.dropdown-menu button')).toHaveCount(5) + await expect(page.locator('.dropdown-menu button').nth(0)).toContainText('Lea Thompson') + await expect(page.locator('.dropdown-menu button').nth(0)).toHaveClass(/is-selected/) + }) + + test('inserts Cyndi Lauper mention when clicked', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@') + await page.locator('.dropdown-menu button').nth(1).click() + await expect(page.locator('.tiptap span.mention')).toHaveAttribute('data-id', 'Cyndi Lauper') + }) + + test('closes the dropdown menu on Escape', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@') + await expect(page.locator('.dropdown-menu')).toBeVisible() + await page.keyboard.press('Escape') + await expect(page.locator('.dropdown-menu')).toHaveCount(0) + }) + + test('inserts Tom Cruise via arrow keys and Enter', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Enter') + await expect(page.locator('.tiptap span.mention')).toHaveAttribute('data-id', 'Tom Cruise') + }) + + test('shows "No result" for unknown query', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@nonexistent') + await expect(page.locator('.dropdown-menu')).toContainText('No result') + }) + + test('filters to a single match for "@mado"', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@mado') + await expect(page.locator('.dropdown-menu button')).toHaveCount(1) + await expect(page.locator('.dropdown-menu button').first()).toContainText('Madonna') + }) + + test('inserts Madonna for "@mado" and Enter', async ({ page }) => { + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + await page.keyboard.type('@mado') + await page.keyboard.press('Enter') + await expect(page.locator('.tiptap span.mention')).toHaveAttribute('data-id', 'Madonna') + }) + }) + + test.describe('Vue', () => { + test('loads the editor', async ({ page }) => { + await page.goto(`${demoPath}/${demoName}/Vue/`) + const editor = await getEditor(page) + await expect(editor).toBeVisible() + }) + }) +}) diff --git a/demos/src/Nodes/OrderedList/React/index.spec.js b/demos/src/Nodes/OrderedList/React/index.spec.js deleted file mode 100644 index 71b4cd7ddf..0000000000 --- a/demos/src/Nodes/OrderedList/React/index.spec.js +++ /dev/null @@ -1,102 +0,0 @@ -context('/src/Nodes/OrderedList/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/OrderedList/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse ordered lists correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse ordered lists correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses ordered lists correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent('Example Text
Example Text
Example Text
Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('text should be wrapped in a paragraph by default', () => { - cy.get('.tiptap').type('Example Text').find('p').should('contain', 'Example Text') - }) - - it('enter should make a new paragraph', () => { - cy.get('.tiptap').type('First Paragraph{enter}Second Paragraph').find('p').should('have.length', 2) - - cy.get('.tiptap').find('p:first').should('contain', 'First Paragraph') - - cy.get('.tiptap').find('p:nth-child(2)').should('contain', 'Second Paragraph') - }) - - it('backspace should remove the second paragraph', () => { - cy.get('.tiptap').type('{enter}').find('p').should('have.length', 2) - - cy.get('.tiptap').type('{backspace}').find('p').should('have.length', 1) - }) -}) diff --git a/demos/src/Nodes/Paragraph/Vue/index.spec.js b/demos/src/Nodes/Paragraph/Vue/index.spec.js deleted file mode 100644 index aeefb1a37a..0000000000 --- a/demos/src/Nodes/Paragraph/Vue/index.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -context('/src/Nodes/Paragraph/Vue/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Paragraph/Vue/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('should parse paragraphs correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - - editor.commands.setContent('Example Text
') - - editor.commands.setContent('Example Text
') - expect(editor.getHTML()).to.eq('Example Text
') - }) - }) - - it('text should be wrapped in a paragraph by default', () => { - cy.get('.tiptap').type('Example Text').find('p').should('contain', 'Example Text') - }) - - it('enter should make a new paragraph', () => { - cy.get('.tiptap').type('First Paragraph{enter}Second Paragraph').find('p').should('have.length', 2) - - cy.get('.tiptap').find('p:first').should('contain', 'First Paragraph') - - cy.get('.tiptap').find('p:nth-child(2)').should('contain', 'Second Paragraph') - }) - - it('backspace should remove the second paragraph', () => { - cy.get('.tiptap').type('{enter}').find('p').should('have.length', 2) - - cy.get('.tiptap').type('{backspace}').find('p').should('have.length', 1) - }) -}) diff --git a/demos/src/Nodes/Paragraph/index.spec.ts b/demos/src/Nodes/Paragraph/index.spec.ts new file mode 100644 index 0000000000..0c0634adfd --- /dev/null +++ b/demos/src/Nodes/Paragraph/index.spec.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test' + +import { getEditor } from '../../../test/helpers.js' + +const demoName = 'Paragraph' +const frameworkPaths = ['React', 'Vue'] +const demoPath = '/src/Nodes' + +test.describe(`${demoPath}/${demoName}`, () => { + frameworkPaths.forEach(frameworkPath => { + const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/` + + test.describe(`${frameworkPath}`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(fullDemoPath) + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.clearContent()) + await editor.click() + }) + + test('parses paragraphs correctly', async ({ page }) => { + const editor = await getEditor(page) + const results = await editor.evaluate((el: any) => { + const inputs = [ + 'Example Text
', + 'Example Text
', + ] + return inputs.map(input => { + el.editor.commands.setContent(input) + return el.editor.getHTML() + }) + }) + results.forEach(html => expect(html).toBe('Example Text
')) + }) + + test('wraps text in a paragraph by default', async ({ page }) => { + await page.keyboard.type('Example Text') + await expect(page.locator('.tiptap p')).toContainText('Example Text') + }) + + test('enter creates a new paragraph', async ({ page }) => { + await page.keyboard.type('First Paragraph') + await page.keyboard.press('Enter') + await page.keyboard.type('Second Paragraph') + await expect(page.locator('.tiptap p')).toHaveCount(2) + await expect(page.locator('.tiptap p').nth(0)).toContainText('First Paragraph') + await expect(page.locator('.tiptap p').nth(1)).toContainText('Second Paragraph') + }) + + test('backspace removes the second paragraph', async ({ page }) => { + await page.keyboard.press('Enter') + await expect(page.locator('.tiptap p')).toHaveCount(2) + await page.keyboard.press('Backspace') + await expect(page.locator('.tiptap p')).toHaveCount(1) + }) + }) + }) +}) diff --git a/demos/src/Nodes/Table/React/index.spec.js b/demos/src/Nodes/Table/React/index.spec.js deleted file mode 100644 index b3969b2f83..0000000000 --- a/demos/src/Nodes/Table/React/index.spec.js +++ /dev/null @@ -1,90 +0,0 @@ -context('/src/Nodes/Table/React/', () => { - beforeEach(() => { - cy.visit('/src/Nodes/Table/React/') - }) - - beforeEach(() => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.clearContent() - }) - }) - - it('creates a table (1x1)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertTable({ cols: 1, rows: 1, withHeaderRow: false }) - - cy.get('.tiptap').find('td').its('length').should('eq', 1) - cy.get('.tiptap').find('tr').its('length').should('eq', 1) - }) - }) - - it('creates a table (3x1)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertTable({ cols: 3, rows: 1, withHeaderRow: false }) - - cy.get('.tiptap').find('td').its('length').should('eq', 3) - cy.get('.tiptap').find('tr').its('length').should('eq', 1) - }) - }) - - it('creates a table (1x3)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertTable({ cols: 1, rows: 3, withHeaderRow: false }) - - cy.get('.tiptap').find('td').its('length').should('eq', 3) - cy.get('.tiptap').find('tr').its('length').should('eq', 3) - }) - }) - - it('creates a table with header row (1x3)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertTable({ cols: 1, rows: 3, withHeaderRow: true }) - - cy.get('.tiptap').find('th').its('length').should('eq', 1) - cy.get('.tiptap').find('td').its('length').should('eq', 2) - cy.get('.tiptap').find('tr').its('length').should('eq', 3) - }) - }) - - it('creates a table with correct defaults (3x3, th)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertTable() - - cy.get('.tiptap').find('th').its('length').should('eq', 3) - cy.get('.tiptap').find('td').its('length').should('eq', 6) - cy.get('.tiptap').find('tr').its('length').should('eq', 3) - }) - }) - - it('sets the minimum width on the colgroups by default (3x1)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertTable({ cols: 3, rows: 1, withHeaderRow: false }) - - cy.get('.tiptap').find('col').invoke('attr', 'style').should('eq', 'min-width: 25px;') - }) - }) - - it('generates correct markup for a table (1x1)', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.insertTable({ cols: 1, rows: 1, withHeaderRow: false }) - - const html = editor.getHTML() - - expect(html).to.equal( - 'Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse unordered lists correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent( - 'Example Text
Example Text
Example Text
Example Text
') - cy.get('.tiptap').type('{selectall}') - }) - }) - - it('should parse unordered lists correctly', () => { - cy.get('.tiptap').then(([{ editor }]) => { - editor.commands.setContent( - 'Example Text
Example Text
Example Text
Example Text
') + const editor = await getEditor(page) + await editor.evaluate((el: any) => el.editor.commands.selectAll()) + }) + + test('parses task lists correctly', async ({ page }) => { + const editor = await getEditor(page) + const html = await editor.evaluate((el: any) => { + el.editor.commands.setContent( + 'Example Text
Example Text
Hello world
', + }) + } + + it('cuts content to start of document', () => { + const editor = createEditor() + + editor.commands.cut({ from: 7, to: 12 }, 1) + + expect(editor.getHTML()).toBe('worldHello
') + + editor.destroy() + }) + + it('cuts content to end of document', () => { + const editor = createEditor() + + editor.commands.cut({ from: 1, to: 6 }, editor.state.doc.nodeSize - 2) + + expect(editor.getHTML()).toBe('world
Hello
') + + editor.destroy() + }) +}) diff --git a/packages/core/__tests__/getContent.spec.ts b/packages/core/__tests__/getContent.spec.ts new file mode 100644 index 0000000000..4116f8a31f --- /dev/null +++ b/packages/core/__tests__/getContent.spec.ts @@ -0,0 +1,37 @@ +import { Editor } from '@tiptap/core' +import StarterKit from '@tiptap/starter-kit' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('editor.getHTML / editor.getJSON', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [StarterKit], + content: 'Example Text
', + }) + }) + + afterEach(() => { + editor.destroy() + }) + + it('returns html', () => { + expect(editor.getHTML()).toBe('Example Text
') + }) + + it('returns json', () => { + expect(editor.getJSON()).toEqual({ + type: 'doc', + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: 'Example Text' }], + }, + ], + }) + }) +}) diff --git a/packages/core/__tests__/insertContent.spec.ts b/packages/core/__tests__/insertContent.spec.ts index 2511bd6d08..1fb19e56dd 100644 --- a/packages/core/__tests__/insertContent.spec.ts +++ b/packages/core/__tests__/insertContent.spec.ts @@ -1,8 +1,12 @@ import { Editor } from '@tiptap/core' import Document from '@tiptap/extension-document' +import Image from '@tiptap/extension-image' +import Link from '@tiptap/extension-link' import Paragraph from '@tiptap/extension-paragraph' import Text from '@tiptap/extension-text' -import { afterEach, describe, expect, it } from 'vitest' +import { Color, TextStyle } from '@tiptap/extension-text-style' +import StarterKit from '@tiptap/starter-kit' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' describe('insertContent', () => { let editor: Editor @@ -26,3 +30,165 @@ describe('insertContent', () => { expect(editor.getHTML()).toBe('helloworld
') }) }) + +describe('insertContent command', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Image, Color, TextStyle, Link, StarterKit], + content: '', + }) + }) + + afterEach(() => { + editor.destroy() + }) + + it('inserts html content correctly', () => { + editor.commands.insertContent( + 'Hello World
This is a paragraph
with a break.
And this is some additional string content.
', + ) + + expect(editor.getHTML()).toContain( + 'Hello World
This is a paragraph
with a break.
And this is some additional string content.
', + ) + }) + + it('keeps spaces inbetween tags in html content', () => { + editor.commands.insertContent('Hello World
') + expect(editor.getHTML()).toContain('Hello World
') + }) + + it('keeps empty spaces', () => { + editor.commands.insertContent(' ') + expect(editor.getHTML()).toContain('') + }) + + it('inserts text content correctly', () => { + editor.commands.insertContent(`Hello World +This is content with a new line. Is this working? + +Lets see if multiple new lines are inserted correctly + +And more lines`) + + expect(editor.getText()).toContain( + 'Hello World\nThis is content with a new line. Is this working?\n\nLets see if multiple new lines are inserted correctly', + ) + }) + + it('keeps newlines in pre tag', () => { + editor.commands.insertContent('
foo\nbar')
+ expect(editor.getHTML()).toContain('foo\nbar')
+ })
+
+ it('keeps newlines and tabs', () => {
+ editor.commands.insertContent('Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK
') + expect(editor.getHTML()).toContain('Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK
') + }) + + it('keeps newlines and tabs between html fragments', () => { + editor.commands.insertContent('Hello World
') + expect(editor.getHTML()).toContain('Hello World
') + }) + + it('allows inserting nothing', () => { + editor.commands.insertContent('') + expect(editor.getHTML()).toBeDefined() + }) + + it('allows inserting a partial HTML tag', () => { + editor.commands.insertContent('foo') + expect(editor.getHTML()).toContain('
foo
') + }) + + it('allows inserting an incomplete HTML tag', () => { + editor.commands.insertContent('foofoo<p
') + }) + + it('allows inserting a list', () => { + editor.commands.insertContent('ABC
123
Hello\n World\n
\n', { + parseOptions: { preserveWhitespace: false }, + }) + expect(editor.getHTML()).toContain('Hello World
') + }) + + it('splits content when image is inserted inbetween text', () => { + editor.commands.insertContent('HelloWorld
') + editor.commands.setTextSelection(6) + editor.commands.insertContent('Hello
World
', + ) + }) + + it('does not split content when image is inserted at beginning of text', () => { + editor.commands.insertContent('HelloWorld
') + editor.commands.setTextSelection(1) + editor.commands.insertContent('HelloWorld
') + }) + + it('respects editor.options.parseOptions if defined to be `false`', () => { + editor.options.parseOptions = { preserveWhitespace: false } + editor.commands.insertContent('\nHello\n World\n
\n') + expect(editor.getHTML()).toContain('Hello World
') + }) + + it('respects editor.options.parseOptions if defined to be `full`', () => { + editor.options.parseOptions = { preserveWhitespace: 'full' } + editor.commands.insertContent('\nHello\n World\n
\n') + expect(editor.getHTML()).toContain('Hello\n World
') + }) + + it('respects editor.options.parseOptions if defined to be `true`', () => { + editor.options.parseOptions = { preserveWhitespace: true } + editor.commands.insertContent('Hello\n World\n
') + expect(editor.getHTML()).toContain('Hello World
') + }) +}) + +describe('insertContent command — applying rules', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [StarterKit], + content: '', + }) + }) + + afterEach(() => { + editor.destroy() + }) + + it('applies list InputRule', async () => { + editor.commands.insertContent('-', { applyInputRules: true }) + editor.commands.insertContent(' ', { applyInputRules: true }) + // applyInputRules schedules rule evaluation via setTimeout(0) + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + expect(editor.getHTML()).toContain('This is an italic text
') + }) +}) diff --git a/packages/core/__tests__/pluginOrder.spec.ts b/packages/core/__tests__/pluginOrder.spec.ts new file mode 100644 index 0000000000..150ad984c5 --- /dev/null +++ b/packages/core/__tests__/pluginOrder.spec.ts @@ -0,0 +1,60 @@ +import { Editor, Extension } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { afterEach, describe, expect, it } from 'vitest' + +describe('pluginOrder', () => { + let editor: Editor + + afterEach(() => { + editor?.destroy() + }) + + it('runs keyboard shortcuts in correct priority order', () => { + const order: number[] = [] + + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + priority: 1000, + addKeyboardShortcuts() { + return { + a: () => { + order.push(1) + return false + }, + } + }, + }), + Extension.create({ + addKeyboardShortcuts() { + return { + a: () => { + order.push(3) + return false + }, + } + }, + }), + Extension.create({ + addKeyboardShortcuts() { + return { + a: () => { + order.push(2) + return false + }, + } + }, + }), + ], + }) + + editor.view.dom.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' })) + + expect(order).toEqual([1, 2, 3]) + }) +}) diff --git a/packages/core/__tests__/setContent.spec.ts b/packages/core/__tests__/setContent.spec.ts new file mode 100644 index 0000000000..08074ba68d --- /dev/null +++ b/packages/core/__tests__/setContent.spec.ts @@ -0,0 +1,167 @@ +import { Editor } from '@tiptap/core' +import Mention from '@tiptap/extension-mention' +import StarterKit from '@tiptap/starter-kit' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('setContent command', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [StarterKit, Mention], + content: '', + }) + }) + + afterEach(() => { + editor.destroy() + }) + + it('inserts raw text content', () => { + editor.commands.setContent('Hello World.') + expect(editor.getHTML()).toContain('Hello World.
') + }) + + it('inserts raw JSON content', () => { + editor.commands.setContent({ + type: 'paragraph', + content: [{ type: 'text', text: 'Hello World.' }], + }) + expect(editor.getHTML()).toContain('Hello World.
') + }) + + it('inserts a Prosemirror Node as content', () => { + editor.commands.setContent(editor.schema.node('paragraph', null, editor.schema.text('Hello World.'))) + expect(editor.getHTML()).toContain('Hello World.
') + }) + + it('inserts a Prosemirror Fragment as content', () => { + editor.commands.setContent( + editor.schema.node('doc', null, editor.schema.node('paragraph', null, editor.schema.text('Hello World.'))) + .content, + ) + expect(editor.getHTML()).toContain('Hello World.
') + }) + + it('respects the emitUpdate option', () => { + let updateCount = 0 + const callback = () => { + updateCount += 1 + } + editor.on('update', callback) + + editor.commands.setContent('Hello World.', { emitUpdate: true }) + const first = updateCount + + updateCount = 0 + editor.commands.setContent('Hello World again.', { emitUpdate: false }) + const second = updateCount + editor.off('update', callback) + + expect(first).toBe(1) + expect(second).toBe(0) + }) + + it('inserts more complex html content', () => { + editor.commands.setContent( + 'This is a paragraph.
List Item A
List Item B
Subchild
This is a paragraph.
List Item A
List Item B
Subchild
Hello\n\tworld\n\t\thow\n\t\t\tnice.
') + expect(editor.getHTML()).toContain('Hello world how nice.
') + }) + + it('keeps newlines and tabs when preserveWhitespace = full', () => { + editor.commands.setContent('Hello\n\tworld\n\t\thow\n\t\t\tnice.
', { + parseOptions: { preserveWhitespace: 'full' }, + }) + expect(editor.getHTML()).toContain('Hello\n\tworld\n\t\thow\n\t\t\tnice.
') + }) + + it('overwrites existing content', () => { + editor.commands.setContent('Initial Content
') + expect(editor.getHTML()).toContain('Initial Content
') + + editor.commands.setContent('Overwritten Content
') + expect(editor.getHTML()).toContain('Overwritten Content
') + + editor.commands.setContent('Content without tags') + expect(editor.getHTML()).toContain('Content without tags
') + }) + + it('inserts mentions', () => { + editor.commands.setContent('@John Doe
') + expect(editor.getHTML()).toContain( + '@John Doe', + ) + }) + + it('removes newlines and tabs between html fragments', () => { + editor.commands.setContent('Hello World
') + expect(editor.getHTML()).toContain('Hello World
') + }) + + it('keeps newlines and tabs between html fragments when preserveWhitespace = full', () => { + editor.commands.setContent('Hello World
', { + parseOptions: { preserveWhitespace: 'full' }, + }) + expect(editor.getHTML()).toContain('\n\t
Hello World
') + }) + + it('allows inserting nothing', () => { + editor.commands.setContent('') + expect(editor.getHTML()).toBeDefined() + }) + + it('allows inserting nothing when preserveWhitespace = full', () => { + editor.commands.setContent('', { parseOptions: { preserveWhitespace: 'full' } }) + expect(editor.getHTML()).toBeDefined() + }) + + it('allows inserting a partial HTML tag', () => { + editor.commands.setContent('foo') + expect(editor.getHTML()).toContain('
foo
') + }) + + it('allows inserting a partial HTML tag when preserveWhitespace = full', () => { + editor.commands.setContent('foo', { parseOptions: { preserveWhitespace: 'full' } }) + expect(editor.getHTML()).toContain('
foo
') + }) + + it('removes an incomplete HTML tag', () => { + editor.commands.setContent('foofoo
') + }) + + it('allows inserting an incomplete HTML tag when preserveWhitespace = full', () => { + editor.commands.setContent('foofoo<p
') + }) + + it('allows inserting a list', () => { + editor.commands.setContent('ABC
123
ABC
123
Hello\n World\n
\n', { + parseOptions: { preserveWhitespace: false }, + }) + expect(editor.getHTML()).toContain('Hello World
') + }) +}) diff --git a/packages/core/__tests__/transformPastedHTML.spec.ts b/packages/core/__tests__/transformPastedHTML.spec.ts new file mode 100644 index 0000000000..e8ad36c6a9 --- /dev/null +++ b/packages/core/__tests__/transformPastedHTML.spec.ts @@ -0,0 +1,249 @@ +import { Editor, Extension } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { afterEach, describe, expect, it } from 'vitest' + +describe('transformPastedHTML', () => { + let editor: Editor + + afterEach(() => { + editor?.destroy() + }) + + it('runs transforms in correct priority order (higher priority first)', () => { + const order: number[] = [] + + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + name: 'extension1', + priority: 100, + transformPastedHTML(html) { + order.push(2) + return html + }, + }), + Extension.create({ + name: 'extension2', + priority: 200, + transformPastedHTML(html) { + order.push(1) + return html + }, + }), + Extension.create({ + name: 'extension3', + priority: 50, + transformPastedHTML(html) { + order.push(3) + return html + }, + }), + ], + }) + + editor.view.props.transformPastedHTML?.('test
', editor.view) + + expect(order).toEqual([1, 2, 3]) + }) + + it('chains transforms correctly', () => { + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + name: 'replaceFoo', + priority: 100, + transformPastedHTML(html) { + return html.replace(/foo/g, 'bar') + }, + }), + Extension.create({ + name: 'replaceBar', + priority: 90, + transformPastedHTML(html) { + return html.replace(/bar/g, 'baz') + }, + }), + ], + }) + + const result = editor.view.props.transformPastedHTML?.('foo
', editor.view) + + expect(result).toBe('baz
') + }) + + it('integrates with baseTransform from editorProps', () => { + editor = new Editor({ + editorProps: { + transformPastedHTML(html) { + return html.replace(/base/g, 'replaced') + }, + }, + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + name: 'extensionTransform', + transformPastedHTML(html) { + return html.replace(/replaced/g, 'final') + }, + }), + ], + }) + + const result = editor.view.props.transformPastedHTML?.('base
', editor.view) + + expect(result).toBe('final
') + }) + + it('handles extensions without transforms', () => { + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + name: 'noTransform', + }), + Extension.create({ + name: 'withTransform', + transformPastedHTML(html) { + return html.replace(/test/g, 'success') + }, + }), + ], + }) + + const result = editor.view.props.transformPastedHTML?.('test
', editor.view) + + expect(result).toBe('success
') + }) + + it('returns original HTML if no transforms are defined', () => { + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ name: 'noTransform1' }), + Extension.create({ name: 'noTransform2' }), + ], + }) + + const result = editor.view.props.transformPastedHTML?.('unchanged
', editor.view) + + expect(result).toBe('unchanged
') + }) + + it('has access to extension context', () => { + let capturedContext: any = null + + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + name: 'contextChecker', + addOptions() { + return { + testOption: 'testValue', + } + }, + transformPastedHTML(html) { + capturedContext = { + name: this.name, + hasOptions: !!this.options, + hasEditor: !!this.editor, + hasStorage: this.storage !== undefined, + } + return html + }, + }), + ], + }) + + editor.view.props.transformPastedHTML?.('test
', editor.view) + + expect(capturedContext).toEqual({ + name: 'contextChecker', + hasOptions: true, + hasEditor: true, + hasStorage: true, + }) + }) + + it('works with multiple transforms modifying HTML structure', () => { + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + name: 'removeStyles', + priority: 100, + transformPastedHTML(html) { + return html.replace(/\s+style="[^"]*"/gi, '') + }, + }), + Extension.create({ + name: 'addClass', + priority: 90, + transformPastedHTML(html) { + return html.replace(//g, '
') + }, + }), + ], + }) + + const result = editor.view.props.transformPastedHTML?.('
test
', editor.view) + + expect(result).toBe('test
') + }) + + it('handles empty HTML', () => { + editor = new Editor({ + extensions: [ + Document, + Paragraph, + Text, + Extension.create({ + name: 'transform', + transformPastedHTML(html) { + return html || 'default
' + }, + }), + ], + }) + + const result = editor.view.props.transformPastedHTML?.('', editor.view) + + expect(result).toBe('default
') + }) + + it('passes view parameter through', () => { + let viewPassed = false + + editor = new Editor({ + editorProps: { + transformPastedHTML(html, view) { + viewPassed = !!view + return html + }, + }, + extensions: [Document, Paragraph, Text], + }) + + editor.view.props.transformPastedHTML?.('test
', editor.view) + + expect(viewPassed).toBe(true) + }) +}) diff --git a/packages/extension-details/__tests__/details-commands.spec.ts b/packages/extension-details/__tests__/details-commands.spec.ts new file mode 100644 index 0000000000..9660effcb1 --- /dev/null +++ b/packages/extension-details/__tests__/details-commands.spec.ts @@ -0,0 +1,48 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { Details, DetailsContent, DetailsSummary } from '../src/index.js' + +describe('Details commands', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, Details, DetailsSummary, DetailsContent], + content: 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + it('parsesContent
Content
This
is
a
paragraph.
Another paragraph.
', + }) + }) + + afterEach(() => { + editor.destroy() + }) + + it('renders invisible character decorations when shown', () => { + editor.commands.showInvisibleCharacters() + const decorations = editor.view.dom.querySelectorAll('[class*="tiptap-invisible-character"]') + expect(decorations.length).toBeGreaterThan(0) + }) +}) diff --git a/packages/extension-subscript/__tests__/subscript.spec.ts b/packages/extension-subscript/__tests__/subscript.spec.ts new file mode 100644 index 0000000000..85fe615333 --- /dev/null +++ b/packages/extension-subscript/__tests__/subscript.spec.ts @@ -0,0 +1,47 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Subscript from '@tiptap/extension-subscript' +import Text from '@tiptap/extension-text' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('Subscript', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, Subscript], + content: 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + it('transforms inline style vertical-align: sub to sub tags', () => { + editor.commands.setContent('Example Text
') + expect(editor.getHTML()).toBe('Example Text
') + }) + + it('omits inline style with a different vertical-align', () => { + editor.commands.setContent('Example Text
') + expect(editor.getHTML()).toBe('Example Text
') + }) + + it('toggleSubscript wraps the selection in a sub tag', () => { + editor.commands.toggleSubscript() + expect(editor.getHTML()).toBe('Example Text
') + }) + + it('toggleSubscript twice removes the sub tag', () => { + editor.commands.toggleSubscript() + editor.commands.selectAll() + editor.commands.toggleSubscript() + expect(editor.getHTML()).toBe('Example Text
') + }) +}) diff --git a/packages/extension-superscript/__tests__/superscript.spec.ts b/packages/extension-superscript/__tests__/superscript.spec.ts new file mode 100644 index 0000000000..16bd511d53 --- /dev/null +++ b/packages/extension-superscript/__tests__/superscript.spec.ts @@ -0,0 +1,47 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Superscript from '@tiptap/extension-superscript' +import Text from '@tiptap/extension-text' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('Superscript', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, Superscript], + content: 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + it('transforms inline style vertical-align: super to sup tags', () => { + editor.commands.setContent('Example Text
') + expect(editor.getHTML()).toBe('Example Text
') + }) + + it('omits inline style with a different vertical-align', () => { + editor.commands.setContent('Example Text
') + expect(editor.getHTML()).toBe('Example Text
') + }) + + it('toggleSuperscript wraps the selection in a sup tag', () => { + editor.commands.toggleSuperscript() + expect(editor.getHTML()).toBe('Example Text
') + }) + + it('toggleSuperscript twice removes the sup tag', () => { + editor.commands.toggleSuperscript() + editor.commands.selectAll() + editor.commands.toggleSuperscript() + expect(editor.getHTML()).toBe('Example Text
') + }) +}) diff --git a/packages/extension-table/__tests__/tableCommands.spec.ts b/packages/extension-table/__tests__/tableCommands.spec.ts new file mode 100644 index 0000000000..f15e89dcb5 --- /dev/null +++ b/packages/extension-table/__tests__/tableCommands.spec.ts @@ -0,0 +1,191 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import { TableKit } from '@tiptap/extension-table' +import Text from '@tiptap/extension-text' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +const countCells = (editor: Editor, selector: string) => editor.view.dom.querySelectorAll(selector).length + +const selectFirstTwoHeaderCells = (editor: Editor) => { + const positions: number[] = [] + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'tableHeader') { + positions.push(pos) + } + }) + editor.chain().focus().setCellSelection({ anchorCell: positions[0], headCell: positions[1] }).run() +} + +describe('Table commands', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, TableKit], + content: '', + }) + editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() + }) + + afterEach(() => { + editor.destroy() + }) + + it('adds a table with three columns and three rows', () => { + expect(countCells(editor, 'table')).toBe(1) + expect(countCells(editor, 'table tr')).toBe(3) + expect(countCells(editor, 'table th')).toBe(3) + expect(countCells(editor, 'table td')).toBe(6) + }) + + it('adds & deletes columns', () => { + editor.commands.addColumnBefore() + expect(countCells(editor, 'table th')).toBe(4) + + editor.commands.addColumnAfter() + expect(countCells(editor, 'table th')).toBe(5) + + editor.commands.deleteColumn() + editor.commands.deleteColumn() + expect(countCells(editor, 'table th')).toBe(3) + }) + + it('adds & deletes rows', () => { + editor.commands.addRowBefore() + expect(countCells(editor, 'table tr')).toBe(4) + + editor.commands.addRowAfter() + expect(countCells(editor, 'table tr')).toBe(5) + + editor.commands.deleteRow() + editor.commands.deleteRow() + expect(countCells(editor, 'table tr')).toBe(3) + }) + + it('deletes the table', () => { + editor.commands.deleteTable() + expect(countCells(editor, 'table')).toBe(0) + }) + + it('merges cells', () => { + selectFirstTwoHeaderCells(editor) + editor.commands.mergeCells() + expect(countCells(editor, 'table th')).toBe(2) + }) + + it('splits cells', () => { + selectFirstTwoHeaderCells(editor) + editor.commands.mergeCells() + expect(countCells(editor, 'table th')).toBe(2) + + editor.commands.splitCell() + expect(countCells(editor, 'table th')).toBe(3) + }) + + it('toggles header columns', () => { + editor.commands.toggleHeaderColumn() + expect(countCells(editor, 'table th')).toBe(5) + }) + + it('toggles header row', () => { + editor.commands.toggleHeaderRow() + expect(countCells(editor, 'table th')).toBe(0) + }) + + it('merges and splits via mergeOrSplit', () => { + selectFirstTwoHeaderCells(editor) + editor.commands.mergeCells() + expect(editor.view.dom.querySelector('th[colspan="2"]')).not.toBeNull() + + editor.commands.mergeOrSplit() + expect(editor.view.dom.querySelector('th[colspan="2"]')).toBeNull() + }) + + it('creates a 1x1 table', () => { + editor.commands.clearContent() + editor.commands.insertTable({ cols: 1, rows: 1, withHeaderRow: false }) + expect(countCells(editor, 'td')).toBe(1) + expect(countCells(editor, 'tr')).toBe(1) + }) + + it('creates a 3x1 table', () => { + editor.commands.clearContent() + editor.commands.insertTable({ cols: 3, rows: 1, withHeaderRow: false }) + expect(countCells(editor, 'td')).toBe(3) + expect(countCells(editor, 'tr')).toBe(1) + }) + + it('creates a 1x3 table', () => { + editor.commands.clearContent() + editor.commands.insertTable({ cols: 1, rows: 3, withHeaderRow: false }) + expect(countCells(editor, 'td')).toBe(3) + expect(countCells(editor, 'tr')).toBe(3) + }) + + it('creates a 1x3 table with header row', () => { + editor.commands.clearContent() + editor.commands.insertTable({ cols: 1, rows: 3, withHeaderRow: true }) + expect(countCells(editor, 'th')).toBe(1) + expect(countCells(editor, 'td')).toBe(2) + expect(countCells(editor, 'tr')).toBe(3) + }) + + it('creates a 3x3 table with defaults', () => { + editor.commands.clearContent() + editor.commands.insertTable() + expect(countCells(editor, 'th')).toBe(3) + expect(countCells(editor, 'td')).toBe(6) + expect(countCells(editor, 'tr')).toBe(3) + }) + + it('sets a minimum width on colgroups', () => { + editor.commands.clearContent() + editor.commands.insertTable({ cols: 3, rows: 1, withHeaderRow: false }) + const firstCol = editor.view.dom.querySelector('col') + expect(firstCol?.getAttribute('style')).toBe('min-width: 25px;') + }) + + it('generates correct markup for a 1x1 table', () => { + editor.commands.clearContent() + editor.commands.insertTable({ cols: 1, rows: 1, withHeaderRow: false }) + expect(editor.getHTML()).toBe( + 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + it('sets the background color of the selected text', () => { + expect(editor.isActive('textStyle', { backgroundColor: '#958DF1' })).toBe(false) + editor.commands.setBackgroundColor('#958DF1') + expect(editor.isActive('textStyle', { backgroundColor: '#958DF1' })).toBe(true) + expect(editor.getHTML()).toContain('Example Text') + }) + + it('removes the background color of the selected text', () => { + editor.commands.setBackgroundColor('#958DF1') + expect(editor.getHTML()).toContain(' { + editor.commands.setBackgroundColor('#958DF1') + expect(editor.getAttributes('textStyle').backgroundColor).toBe('#958DF1') + }) +}) diff --git a/packages/extension-text-style/__tests__/color-commands.spec.ts b/packages/extension-text-style/__tests__/color-commands.spec.ts new file mode 100644 index 0000000000..04677a5ef3 --- /dev/null +++ b/packages/extension-text-style/__tests__/color-commands.spec.ts @@ -0,0 +1,45 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { Color, TextStyle } from '@tiptap/extension-text-style' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('Color commands', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, TextStyle, Color], + content: 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + it('sets the color of the selected text', () => { + expect(editor.isActive('textStyle', { color: '#958DF1' })).toBe(false) + editor.commands.setColor('#958DF1') + expect(editor.isActive('textStyle', { color: '#958DF1' })).toBe(true) + expect(editor.getHTML()).toContain('Example Text') + }) + + it('removes the color of the selected text', () => { + editor.commands.setColor('#958DF1') + expect(editor.getHTML()).toContain(' { + editor.commands.setColor('#958DF1') + expect(editor.getAttributes('textStyle').color).toBe('#958DF1') + }) +}) diff --git a/packages/extension-text-style/__tests__/font-family-commands.spec.ts b/packages/extension-text-style/__tests__/font-family-commands.spec.ts new file mode 100644 index 0000000000..aac55180a7 --- /dev/null +++ b/packages/extension-text-style/__tests__/font-family-commands.spec.ts @@ -0,0 +1,57 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { FontFamily, TextStyle } from '@tiptap/extension-text-style' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('FontFamily commands', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, TextStyle, FontFamily], + content: 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + it('sets the font-family of the selected text', () => { + expect(editor.isActive('textStyle', { fontFamily: 'monospace' })).toBe(false) + editor.commands.setFontFamily('monospace') + expect(editor.isActive('textStyle', { fontFamily: 'monospace' })).toBe(true) + expect(editor.getHTML()).toContain('Example Text') + }) + + it('removes the font-family of the selected text', () => { + editor.commands.setFontFamily('monospace') + expect(editor.getHTML()).toContain(' { + editor.commands.setFontFamily('var(--title-font-family)') + expect(editor.getHTML()).toContain('Example Text') + }) + + it('allows fonts containing multiple font families', () => { + editor.commands.setFontFamily('"Comic Sans MS", "Comic Sans"') + expect(editor.getHTML()).toContain( + 'Example Text', + ) + }) + + it('allows fonts containing a space and number as a font-family', () => { + editor.commands.setFontFamily('"Exo 2"') + expect(editor.getHTML()).toContain('Example Text') + }) +}) diff --git a/packages/extension-text-style/__tests__/font-size-commands.spec.ts b/packages/extension-text-style/__tests__/font-size-commands.spec.ts new file mode 100644 index 0000000000..d2925cbef6 --- /dev/null +++ b/packages/extension-text-style/__tests__/font-size-commands.spec.ts @@ -0,0 +1,40 @@ +import { Editor } from '@tiptap/core' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import { FontSize, TextStyle } from '@tiptap/extension-text-style' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('FontSize commands', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, TextStyle, FontSize], + content: 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + it('sets the font-size of the selected text', () => { + expect(editor.isActive('textStyle', { fontSize: '28px' })).toBe(false) + editor.commands.setFontSize('28px') + expect(editor.isActive('textStyle', { fontSize: '28px' })).toBe(true) + expect(editor.getHTML()).toContain('Example Text') + }) + + it('removes the font-size of the selected text', () => { + editor.commands.setFontSize('28px') + expect(editor.getHTML()).toContain('') + + editor.commands.unsetFontSize() + expect(editor.getHTML()).not.toContain(' { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, TextStyle, LineHeight], + content: 'Example Text
', + }) + editor.commands.selectAll() + }) + + afterEach(() => { + editor.destroy() + }) + + const cases = [ + { value: '1.5', style: 'line-height: 1.5' }, + { value: '2.0', style: 'line-height: 2.0' }, + { value: '4.0', style: 'line-height: 4.0' }, + ] + + cases.forEach(({ value, style }) => { + it(`sets line-height ${value} for the selected text`, () => { + expect(editor.isActive('textStyle', { lineHeight: value })).toBe(false) + editor.commands.toggleTextStyle({ lineHeight: value }) + expect(editor.isActive('textStyle', { lineHeight: value })).toBe(true) + expect(editor.getHTML()).toContain(`Example Text`) + }) + }) + + it('removes the line-height of the selected text', () => { + editor.commands.toggleTextStyle({ lineHeight: '1.5' }) + expect(editor.getHTML()).toContain('') + + editor.commands.unsetLineHeight() + expect(editor.getHTML()).not.toContain('This has a <span> tag without a style attribute, so it's thrown away. +But this one is wrapped in a <span> tag with an inline style attribute, so it's kept - even if it's empty for now.
+--- merge nested span styles option enabled ---
++ + + red serif + + +
++ + + + blue serif + + + +
++ + green serif + red serif + +
++ + plain + blue + plain + + green + green serif + + plain + +
++ + blue + + green + + green serif + blue serif + + + +
+` + +describe('TextStyleKit nested span merge', () => { + let editor: Editor + + beforeEach(() => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [Document, Paragraph, Text, TextStyleKit, Bold], + content: demoContent, + }) + }) + + afterEach(() => { + editor.destroy() + }) + + it('merges styles of a span with one nested child span into a single span', () => { + const spans = editor.view.dom.querySelectorAll('p:nth-child(4) > span') + expect(spans.length).toBe(1) + expect(spans[0].textContent?.trim()).toBe('red serif') + expect(spans[0].getAttribute('style')).toBe('color: #FF0000; font-family: serif') + }) + + it('merges styles of multiple nested spans into the innermost descendant', () => { + const spans = editor.view.dom.querySelectorAll('p:nth-child(5) > span') + expect(spans.length).toBe(1) + expect(spans[0].textContent?.trim()).toBe('blue serif') + expect(spans[0].getAttribute('style')).toBe('color: #0000FF; font-family: serif') + }) + + it('merges parent styles into each sibling descendant span', () => { + const spans = editor.view.dom.querySelectorAll('p:nth-child(6) > span') + expect(spans.length).toBe(2) + expect(spans[0].textContent?.trim()).toBe('green serif') + expect(spans[0].getAttribute('style')).toBe('color: #00FF00; font-family: serif') + expect(spans[1].textContent?.trim()).toBe('red serif') + expect(spans[1].getAttribute('style')).toBe('color: #FF0000; font-family: serif') + }) + + it('keeps descendant spans intact when the parent span has no style', () => { + const spans = editor.view.dom.querySelectorAll('p:nth-child(7) > span') + expect(spans.length).toBe(4) + expect(spans[0].textContent?.trim()).toBe('blue') + expect(spans[0].getAttribute('style')).toBe('color: #0000FF') + expect(spans[1].textContent?.trim()).toBe('green') + expect(spans[1].getAttribute('style')).toBe('color: #00FF00') + expect(spans[2].textContent?.trim()).toBe('green serif') + expect(spans[2].getAttribute('style')).toBe('color: #00FF00; font-family: serif') + }) + + it('merges parent styles into descendants while preserving root text spans', () => { + const spans = editor.view.dom.querySelectorAll('p:nth-child(8) > span') + expect(spans.length).toBe(4) + expect(spans[0].textContent?.trim()).toBe('blue') + expect(spans[0].getAttribute('style')).toBe('color: #0000FF') + expect(spans[1].textContent?.trim()).toBe('green') + expect(spans[1].getAttribute('style')).toBe('color: #00FF00') + expect(spans[2].textContent?.trim()).toBe('green serif') + expect(spans[2].getAttribute('style')).toBe('color: #00FF00; font-family: serif') + expect(spans[3].textContent?.trim()).toBe('blue serif') + expect(spans[3].getAttribute('style')).toBe('color: #0000FF; font-family: serif') + }) +}) diff --git a/packages/extension-unique-id/__tests__/unique-id.spec.ts b/packages/extension-unique-id/__tests__/unique-id.spec.ts new file mode 100644 index 0000000000..4c7d5cb852 --- /dev/null +++ b/packages/extension-unique-id/__tests__/unique-id.spec.ts @@ -0,0 +1,44 @@ +import { Editor } from '@tiptap/core' +import UniqueID from '@tiptap/extension-unique-id' +import StarterKit from '@tiptap/starter-kit' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +describe('UniqueID', () => { + let editor: Editor + + beforeEach(async () => { + const element = document.createElement('div') + document.body.appendChild(element) + editor = new Editor({ + element, + extensions: [StarterKit, UniqueID.configure({ types: ['heading', 'paragraph'] })], + content: 'Paragraph one.
Paragraph two.
', + }) + // UniqueID generates initial ids in onCreate via setTimeout(0) + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + }) + + afterEach(() => { + editor.destroy() + }) + + it('assigns a unique id to headings', () => { + const heading = editor.view.dom.querySelector('h1') + expect(heading?.getAttribute('data-id')).toMatch(/.+/) + }) + + it('assigns a unique id to paragraphs', () => { + const paragraphs = editor.view.dom.querySelectorAll('p') + paragraphs.forEach(p => { + expect(p.getAttribute('data-id')).toMatch(/.+/) + }) + }) + + it('gives different ids to different nodes', () => { + const paragraphs = Array.from(editor.view.dom.querySelectorAll('p')) + const ids = paragraphs.map(p => p.getAttribute('data-id')) + expect(new Set(ids).size).toBe(ids.length) + }) +}) diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..871db21812 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test' + +process.env.NODE_OPTIONS = [process.env.NODE_OPTIONS, '--require tsconfig-paths/register'].filter(Boolean).join(' ') + +export default defineConfig({ + testDir: './demos', + testMatch: '**/*.spec.ts', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['github'], ['blob']] : [['line'], ['html', { open: 'never' }]], + use: { + baseURL: 'http://127.0.0.1:4080', + trace: 'on-first-retry', + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + ], + webServer: { + command: 'pnpm -C demos run start:e2e', + url: 'http://127.0.0.1:4080', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ccfa13eca..ad1e3b6793 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,6 @@ settings: overrides: '@rollup/pluginutils>picomatch': 2.3.2 '@octokit/action>undici': 6.24.1 - '@cypress/request>uuid': 14.0.0 anymatch>picomatch: 2.3.2 glob: ^10.5.0 micromatch>picomatch: 2.3.2 @@ -63,9 +62,9 @@ importers: '@commitlint/config-conventional': specifier: ^19.6.0 version: 19.6.0 - '@cypress/webpack-preprocessor': - specifier: ^6.0.2 - version: 6.0.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(babel-loader@9.2.1(@babel/core@7.26.0)(webpack@5.97.1(esbuild@0.27.2)))(webpack@5.97.1(esbuild@0.27.2)) + '@playwright/test': + specifier: ^1.54.2 + version: 1.60.0 '@testing-library/react': specifier: 16.2.0 version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -87,9 +86,6 @@ importers: babel-loader: specifier: ^9.2.1 version: 9.2.1(@babel/core@7.26.0)(webpack@5.97.1(esbuild@0.27.2)) - cypress: - specifier: ^15.13.0 - version: 15.13.0 cz-conventional-changelog: specifier: ^3.3.0 version: 3.3.0(@types/node@25.5.0)(typescript@5.7.3) @@ -102,9 +98,6 @@ importers: eslint-config-prettier: specifier: 9.1.0 version: 9.1.0(eslint@8.57.1) - eslint-plugin-cypress: - specifier: ^2.15.2 - version: 2.15.2(eslint@8.57.1) eslint-plugin-html: specifier: ^6.2.0 version: 6.2.0 @@ -156,6 +149,9 @@ importers: ts-loader: specifier: 9.3.1 version: 9.3.1(typescript@5.7.3)(webpack@5.97.1(esbuild@0.27.2)) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 tsup: specifier: ^8.5.1 version: 8.5.1(jiti@2.4.2)(postcss@8.5.12)(typescript@5.7.3)(yaml@2.7.0) @@ -1977,21 +1973,6 @@ packages: resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==} engines: {node: '>=v18'} - '@cypress/request@3.0.10': - resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==} - engines: {node: '>= 6'} - - '@cypress/webpack-preprocessor@6.0.2': - resolution: {integrity: sha512-0+1+4iy4W9PE6R5ywBNKAZoFp8Sf//w3UJ+CKTqkcAjA29b+dtsD0iFT70DsYE0BMqUM1PO7HXFGbXllQ+bRAA==} - peerDependencies: - '@babel/core': ^7.0.1 - '@babel/preset-env': ^7.0.0 - babel-loader: ^8.3 || ^9 - webpack: ^4 || ^5 - - '@cypress/xvfb@1.2.4': - resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -2805,6 +2786,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -3214,15 +3200,6 @@ packages: '@types/react@19.1.6': resolution: {integrity: sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==} - '@types/sinonjs__fake-timers@8.1.1': - resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} - - '@types/sizzle@2.3.9': - resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} - - '@types/tmp@0.2.6': - resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -3235,9 +3212,6 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.58.0': resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3299,6 +3273,7 @@ packages: '@ungap/structured-clone@1.2.1': resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@vitejs/plugin-react@1.3.2': resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==} @@ -3445,10 +3420,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -3516,9 +3487,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - arch@2.2.0: - resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} - arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -3566,27 +3534,13 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} - - assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - async@2.6.4: resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - at-least-node@1.0.0: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} @@ -3602,12 +3556,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - aws-sign2@0.7.0: - resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} - - aws4@1.13.2: - resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} - axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -3648,9 +3596,6 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} - bcrypt-pbkdf@1.0.2: - resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} @@ -3665,15 +3610,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - blob-util@2.0.2: - resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} - - bluebird@3.7.1: - resolution: {integrity: sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==} - - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -3696,9 +3632,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3722,10 +3655,6 @@ packages: resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} engines: {node: '>=6'} - cachedir@2.4.0: - resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} - engines: {node: '>=6'} - call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -3752,9 +3681,6 @@ packages: caniuse-lite@1.0.30001695: resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} - caseless@0.12.0: - resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3799,14 +3725,6 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - ci-info@4.1.0: - resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} - engines: {node: '>=8'} - - clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -3819,14 +3737,6 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - cli-table3@0.6.1: - resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} - engines: {node: 10.* || >= 12.*} - - cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - cli-truncate@4.0.0: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} @@ -3865,14 +3775,6 @@ packages: colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} - colors@1.4.0: - resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} - engines: {node: '>=0.1.90'} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -3887,10 +3789,6 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -3907,10 +3805,6 @@ packages: common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} - common-tags@1.8.2: - resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} - engines: {node: '>=4.0.0'} - compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -3949,9 +3843,6 @@ packages: core-js-compat@3.40.0: resolution: {integrity: sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==} - core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - corser@2.0.1: resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} engines: {node: '>= 0.4.0'} @@ -3992,11 +3883,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - cypress@15.13.0: - resolution: {integrity: sha512-hJ9sY++TUC/HlUzHVJpIrDyqKMjlhx5PTXl/A7eA91JNEtUWkJAqefQR5mo9AtLra/9+m+JJaMg2U5Qd0a74Fw==} - engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0} - hasBin: true - cz-conventional-changelog@3.3.0: resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} engines: {node: '>= 10'} @@ -4132,10 +4018,6 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} - dashdash@1.14.1: - resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} - engines: {node: '>=0.10'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -4151,9 +4033,6 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -4208,10 +4087,6 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} @@ -4284,9 +4159,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ecc-jsbn@0.1.2: - resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} - electron-to-chromium@1.5.84: resolution: {integrity: sha512-I+DQ8xgafao9Ha6y0qjHHvpZ9OfyA1qKlkHkjywxzniORU2awxyz7f/iVJcULmrF2yrM3nHQf+iDjJtbbexd/g==} @@ -4314,9 +4186,6 @@ packages: resolution: {integrity: sha512-Nw2m7JLIO4Ou2X/yZPRNscHQXVbbr6SErjkJ7EooG7MbR3yDZszCv9KTizsXFc7yZl0n3WF+qUKIC/Lw6H9xaQ==} engines: {node: '>=18.12.0'} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - enhanced-resolve@5.18.0: resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==} engines: {node: '>=10.13.0'} @@ -4579,11 +4448,6 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-cypress@2.15.2: - resolution: {integrity: sha512-CtcFEQTDKyftpI22FVGpx8bkpKyYXBlNge6zSo0pl5/qJvBAnzaD76Vu2AsP16d6mTj478Ldn2mhgrWV+Xr0vQ==} - peerDependencies: - eslint: '>= 3.2.1' - eslint-plugin-html@6.2.0: resolution: {integrity: sha512-vi3NW0E8AJombTvt8beMwkL1R/fdRWl4QSNRNMhVQKWm36/X0KF0unGNAY4mqUF06mnwVWZcIcerrCnfn9025g==} @@ -4671,9 +4535,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter2@6.4.7: - resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} - eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -4684,18 +4545,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} - execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - executable@4.1.1: - resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} - engines: {node: '>=4'} - expand-tilde@2.0.2: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} @@ -4704,9 +4557,6 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -4714,15 +4564,6 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - - extsprintf@1.3.0: - resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} - engines: {'0': node >=0.6.0} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4746,9 +4587,6 @@ packages: fastq@1.18.0: resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -4841,13 +4679,6 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} - forever-agent@0.6.1: - resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} - engines: {node: '>= 6'} - fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -4867,6 +4698,11 @@ packages: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4905,10 +4741,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -4917,9 +4749,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - getpass@0.1.7: - resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} - git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} @@ -4946,10 +4775,6 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - global-dirs@3.0.1: - resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} - engines: {node: '>=10'} - global-modules@1.0.0: resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} engines: {node: '>=0.10.0'} @@ -5015,10 +4840,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasha@5.2.2: - resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} - engines: {node: '>=8'} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -5060,17 +4881,9 @@ packages: engines: {node: '>=12'} hasBin: true - http-signature@1.4.0: - resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} - engines: {node: '>=0.10'} - human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} - human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} - human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -5117,20 +4930,12 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@2.0.0: - resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} - engines: {node: '>=10'} - ini@4.1.1: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -5217,10 +5022,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-installed-globally@0.4.0: - resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} - engines: {node: '>=10'} - is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -5260,10 +5061,6 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5288,9 +5085,6 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} - is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -5327,9 +5121,6 @@ packages: isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} - isstream@0.1.2: - resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -5360,9 +5151,6 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - jsbn@0.1.1: - resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} - jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -5385,15 +5173,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -5413,10 +5195,6 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - jsprim@2.0.2: - resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} - engines: {'0': node >=0.6.0} - katex@0.16.22: resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} hasBin: true @@ -5476,15 +5254,6 @@ packages: engines: {node: '>=18.12.0'} hasBin: true - listr2@3.14.0: - resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} - engines: {node: '>=10.0.0'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - listr2@8.2.5: resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} engines: {node: '>=18.0.0'} @@ -5533,9 +5302,6 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -5558,10 +5324,6 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-update@4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} - log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -5839,9 +5601,6 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - ospath@1.2.2: - resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} - outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -5881,10 +5640,6 @@ packages: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -5940,12 +5695,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - - performance-now@2.1.0: - resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -5988,6 +5737,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + portfinder@1.0.32: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -6069,10 +5828,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-bytes@5.6.0: - resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} - engines: {node: '>=6'} - pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -6081,10 +5836,6 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} @@ -6149,12 +5900,6 @@ packages: prosemirror-view@1.38.1: resolution: {integrity: sha512-4FH/uM1A4PNyrxXbD+RAbAsf0d/mM0D/wAKSVVWK7o0A9Q/oOXJBrw786mBf2Vnrs/Edly6dH6Z2gsb7zWwaUw==} - proxy-from-env@1.0.0: - resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} - - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} - punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -6278,9 +6023,6 @@ packages: remixicon@2.5.0: resolution: {integrity: sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww==} - request-progress@3.0.0: - resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6583,14 +6325,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -6631,11 +6365,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sshpk@1.18.0: - resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} - engines: {node: '>=0.10.0'} - hasBin: true - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -6692,10 +6421,6 @@ packages: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -6743,12 +6468,6 @@ packages: resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} engines: {node: '>=16.0.0'} - systeminformation@5.31.5: - resolution: {integrity: sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==} - engines: {node: '>=8.0.0'} - os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] - hasBin: true - tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -6800,9 +6519,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - throttleit@1.0.1: - resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -6835,21 +6551,10 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tldts-core@6.1.73: - resolution: {integrity: sha512-k1g5eX87vxu3g//6XMn62y4qjayu4cYby/PF7Ksnh4F4uUK1Z1ze/mJ4a+y5OjdJ+cXRp+YTInZhH+FGdUWy1w==} - - tldts@6.1.73: - resolution: {integrity: sha512-/h4bVmuEMm57c2uCiAf1Q9mlQk7cA22m+1Bu0K92vUUtTVT9D4mOFWD9r4WQuTULcG9eeZtNKhLl0Il1LdKGog==} - hasBin: true - tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} - tmp@0.2.5: - resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} - engines: {node: '>=14.14'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -6858,10 +6563,6 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@5.1.0: - resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} - engines: {node: '>=16'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -6891,8 +6592,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6921,9 +6623,6 @@ packages: typescript: optional: true - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - tunnel@0.0.6: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} @@ -6962,9 +6661,6 @@ packages: resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==} hasBin: true - tweetnacl@0.14.5: - resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -6981,10 +6677,6 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -7076,10 +6768,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -7115,10 +6803,6 @@ packages: varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} - verror@1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} - engines: {'0': node >=0.6.0} - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -7395,10 +7079,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -7490,9 +7170,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yjs@13.6.23: resolution: {integrity: sha512-ExtnT5WIOVpkL56bhLeisG/N5c4fmzKn4k0ROVfJa5TY2QHbH7F0Wu2T5ZhR7ErsFWQEFafyrnSI8TPKVF9Few==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -7622,7 +7299,7 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-plugin-utils': 7.26.5 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -8607,46 +8284,6 @@ snapshots: '@types/conventional-commits-parser': 5.0.1 chalk: 5.4.1 - '@cypress/request@3.0.10': - dependencies: - aws-sign2: 0.7.0 - aws4: 1.13.2 - caseless: 0.12.0 - combined-stream: 1.0.8 - extend: 3.0.2 - forever-agent: 0.6.1 - form-data: 4.0.4 - http-signature: 1.4.0 - is-typedarray: 1.0.0 - isstream: 0.1.2 - json-stringify-safe: 5.0.1 - mime-types: 2.1.35 - performance-now: 2.1.0 - qs: 6.14.1 - safe-buffer: 5.2.1 - tough-cookie: 5.1.0 - tunnel-agent: 0.6.0 - uuid: 14.0.0 - - '@cypress/webpack-preprocessor@6.0.2(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(babel-loader@9.2.1(@babel/core@7.26.0)(webpack@5.97.1(esbuild@0.27.2)))(webpack@5.97.1(esbuild@0.27.2))': - dependencies: - '@babel/core': 7.26.0 - '@babel/preset-env': 7.26.0(@babel/core@7.26.0) - babel-loader: 9.2.1(@babel/core@7.26.0)(webpack@5.97.1(esbuild@0.27.2)) - bluebird: 3.7.1 - debug: 4.4.0 - lodash: 4.17.21 - webpack: 5.97.1(esbuild@0.27.2) - transitivePeerDependencies: - - supports-color - - '@cypress/xvfb@1.2.4(supports-color@8.1.1)': - dependencies: - debug: 3.2.7(supports-color@8.1.1) - lodash.once: 4.1.1 - transitivePeerDependencies: - - supports-color - '@esbuild/aix-ppc64@0.21.5': optional: true @@ -9344,6 +8981,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + '@polka/url@1.0.0-next.29': {} '@preact/signals-core@1.12.1': {} @@ -9744,12 +9385,6 @@ snapshots: dependencies: csstype: 3.1.3 - '@types/sinonjs__fake-timers@8.1.1': {} - - '@types/sizzle@2.3.9': {} - - '@types/tmp@0.2.6': {} - '@types/unist@3.0.3': {} '@types/use-sync-external-store@0.0.6': {} @@ -9760,11 +9395,6 @@ snapshots: dependencies: '@types/node': 22.10.3 - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 22.10.3 - optional: true - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9787,7 +9417,7 @@ snapshots: '@typescript-eslint/types': 8.58.0 '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.58.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 eslint: 8.57.1 typescript: 5.7.3 transitivePeerDependencies: @@ -9797,7 +9427,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.7.3) '@typescript-eslint/types': 8.58.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -9816,7 +9446,7 @@ snapshots: '@typescript-eslint/types': 8.58.0 '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.7.3) '@typescript-eslint/utils': 8.58.0(eslint@8.57.1)(typescript@5.7.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 eslint: 8.57.1 ts-api-utils: 2.5.0(typescript@5.7.3) typescript: 5.7.3 @@ -9831,7 +9461,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.7.3) '@typescript-eslint/types': 8.58.0 '@typescript-eslint/visitor-keys': 8.58.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.15 @@ -10083,11 +9713,6 @@ snapshots: acorn@8.14.0: {} - aggregate-error@3.1.0: - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - ajv-formats@2.1.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -10148,8 +9773,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 - arch@2.2.0: {} - arg@5.0.2: {} argparse@1.0.10: @@ -10215,22 +9838,12 @@ snapshots: get-intrinsic: 1.2.7 is-array-buffer: 3.0.5 - asn1@0.2.6: - dependencies: - safer-buffer: 2.1.2 - - assert-plus@1.0.0: {} - assertion-error@2.0.1: {} - astral-regex@2.0.0: {} - async@2.6.4: dependencies: lodash: 4.17.23 - asynckit@0.4.0: {} - at-least-node@1.0.0: {} autoprefixer@10.4.20(postcss@8.5.12): @@ -10247,10 +9860,6 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 - aws-sign2@0.7.0: {} - - aws4@1.13.2: {} - axobject-query@4.1.0: {} babel-loader@9.2.1(@babel/core@7.26.0)(webpack@5.97.1(esbuild@0.27.2)): @@ -10294,10 +9903,6 @@ snapshots: dependencies: safe-buffer: 5.1.2 - bcrypt-pbkdf@1.0.2: - dependencies: - tweetnacl: 0.14.5 - before-after-hook@2.2.3: {} better-path-resolve@1.0.0: @@ -10312,12 +9917,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - blob-util@2.0.2: {} - - bluebird@3.7.1: {} - - bluebird@3.7.2: {} - boolbase@1.0.0: {} brace-expansion@1.1.11: @@ -10344,8 +9943,6 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) - buffer-crc32@0.2.13: {} - buffer-from@1.1.2: {} buffer@5.7.1: @@ -10367,8 +9964,6 @@ snapshots: cachedir@2.3.0: {} - cachedir@2.4.0: {} - call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0 @@ -10394,8 +9989,6 @@ snapshots: caniuse-lite@1.0.30001695: {} - caseless@0.12.0: {} - ccount@2.0.1: {} chai@6.2.2: {} @@ -10439,10 +10032,6 @@ snapshots: ci-info@3.9.0: {} - ci-info@4.1.0: {} - - clean-stack@2.2.0: {} - cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -10453,17 +10042,6 @@ snapshots: cli-spinners@2.9.2: {} - cli-table3@0.6.1: - dependencies: - string-width: 4.2.3 - optionalDependencies: - colors: 1.4.0 - - cli-truncate@2.1.0: - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 @@ -10503,13 +10081,6 @@ snapshots: colorjs.io@0.5.2: {} - colors@1.4.0: - optional: true - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - comma-separated-tokens@2.0.3: {} commander@12.1.0: {} @@ -10518,8 +10089,6 @@ snapshots: commander@4.1.1: {} - commander@6.2.1: {} - commander@7.2.0: {} commander@8.3.0: {} @@ -10546,8 +10115,6 @@ snapshots: common-path-prefix@3.0.0: {} - common-tags@1.8.2: {} - compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -10584,8 +10151,6 @@ snapshots: dependencies: browserslist: 4.24.4 - core-util-is@1.0.2: {} - corser@2.0.1: {} cosmiconfig-typescript-loader@6.1.0(@types/node@25.5.0)(cosmiconfig@9.0.0(typescript@5.7.3))(typescript@5.7.3): @@ -10621,52 +10186,6 @@ snapshots: csstype@3.1.3: {} - cypress@15.13.0: - dependencies: - '@cypress/request': 3.0.10 - '@cypress/xvfb': 1.2.4(supports-color@8.1.1) - '@types/sinonjs__fake-timers': 8.1.1 - '@types/sizzle': 2.3.9 - '@types/tmp': 0.2.6 - arch: 2.2.0 - blob-util: 2.0.2 - bluebird: 3.7.2 - buffer: 5.7.1 - cachedir: 2.4.0 - chalk: 4.1.2 - ci-info: 4.1.0 - cli-cursor: 3.1.0 - cli-table3: 0.6.1 - commander: 6.2.1 - common-tags: 1.8.2 - dayjs: 1.11.13 - debug: 4.4.3(supports-color@8.1.1) - enquirer: 2.4.1 - eventemitter2: 6.4.7 - execa: 4.1.0 - executable: 4.1.1 - extract-zip: 2.0.1(supports-color@8.1.1) - figures: 3.2.0 - fs-extra: 9.1.0 - hasha: 5.2.2 - is-installed-globally: 0.4.0 - listr2: 3.14.0(enquirer@2.4.1) - lodash: 4.17.23 - log-symbols: 4.1.0 - minimist: 1.2.8 - ospath: 1.2.2 - pretty-bytes: 5.6.0 - process: 0.11.10 - proxy-from-env: 1.0.0 - request-progress: 3.0.0 - supports-color: 8.1.1 - systeminformation: 5.31.5 - tmp: 0.2.5 - tree-kill: 1.2.2 - tslib: 1.14.1 - untildify: 4.0.0 - yauzl: 2.10.0 - cz-conventional-changelog@3.3.0(@types/node@25.5.0)(typescript@5.7.3): dependencies: chalk: 2.4.2 @@ -10835,10 +10354,6 @@ snapshots: dargs@8.1.0: {} - dashdash@1.14.1: - dependencies: - assert-plus: 1.0.0 - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.3 @@ -10859,23 +10374,17 @@ snapshots: dataloader@1.4.0: {} - dayjs@1.11.13: {} - - debug@3.2.7(supports-color@8.1.1): + debug@3.2.7: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 debug@4.4.0: dependencies: ms: 2.1.3 - debug@4.4.3(supports-color@8.1.1): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 decode-uri-component@0.4.1: {} @@ -10905,8 +10414,6 @@ snapshots: dependencies: robust-predicates: 3.0.2 - delayed-stream@1.0.0: {} - deprecation@2.3.1: {} dequal@2.0.3: {} @@ -10972,11 +10479,6 @@ snapshots: eastasianwidth@0.2.0: {} - ecc-jsbn@0.1.2: - dependencies: - jsbn: 0.1.1 - safer-buffer: 2.1.2 - electron-to-chromium@1.5.84: {} emoji-datasource@16.0.0: {} @@ -10995,10 +10497,6 @@ snapshots: emojibase@16.0.0: {} - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - enhanced-resolve@5.18.0: dependencies: graceful-fs: 4.2.11 @@ -11301,7 +10799,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7 is-core-module: 2.16.1 resolve: 1.22.10 transitivePeerDependencies: @@ -11309,7 +10807,7 @@ snapshots: eslint-module-utils@2.12.0(@typescript-eslint/parser@8.58.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 @@ -11317,11 +10815,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-cypress@2.15.2(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - globals: 13.24.0 - eslint-plugin-html@6.2.0: dependencies: htmlparser2: 7.2.0 @@ -11333,7 +10826,7 @@ snapshots: array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7 doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 @@ -11462,26 +10955,12 @@ snapshots: esutils@2.0.3: {} - eventemitter2@6.4.7: {} - eventemitter3@4.0.7: {} eventemitter3@5.0.1: {} events@3.3.0: {} - execa@4.1.0: - dependencies: - cross-spawn: 7.0.6 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -11494,18 +10973,12 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - executable@4.1.1: - dependencies: - pify: 2.3.0 - expand-tilde@2.0.2: dependencies: homedir-polyfill: 1.0.3 expect-type@1.3.0: {} - extend@3.0.2: {} - extendable-error@0.1.7: {} external-editor@3.1.0: @@ -11514,18 +10987,6 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 - extract-zip@2.0.1(supports-color@8.1.1): - dependencies: - debug: 4.4.3(supports-color@8.1.1) - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - - extsprintf@1.3.0: {} - fast-deep-equal@3.1.3: {} fast-equals@5.3.3: {} @@ -11548,10 +11009,6 @@ snapshots: dependencies: reusify: 1.0.4 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdir@6.4.3(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -11641,16 +11098,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - forever-agent@0.6.1: {} - - form-data@4.0.4: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - fraction.js@4.3.7: {} fs-extra@11.3.0: @@ -11678,6 +11125,9 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -11720,10 +11170,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.2 - get-stream@8.0.1: {} get-symbol-description@1.1.0: @@ -11732,10 +11178,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.7 - getpass@0.1.7: - dependencies: - assert-plus: 1.0.0 - git-raw-commits@4.0.0: dependencies: dargs: 8.1.0 @@ -11765,10 +11207,6 @@ snapshots: dependencies: ini: 4.1.1 - global-dirs@3.0.1: - dependencies: - ini: 2.0.0 - global-modules@1.0.0: dependencies: global-prefix: 1.0.2 @@ -11841,11 +11279,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasha@5.2.2: - dependencies: - is-stream: 2.0.1 - type-fest: 0.8.1 - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -11916,16 +11349,8 @@ snapshots: - debug - supports-color - http-signature@1.4.0: - dependencies: - assert-plus: 1.0.0 - jsprim: 2.0.2 - sshpk: 1.18.0 - human-id@1.0.2: {} - human-signals@1.1.1: {} - human-signals@5.0.0: {} husky@8.0.3: {} @@ -11957,14 +11382,10 @@ snapshots: imurmurhash@0.1.4: {} - indent-string@4.0.0: {} - inherits@2.0.4: {} ini@1.3.8: {} - ini@2.0.0: {} - ini@4.1.1: {} inquirer@8.2.5: @@ -12065,11 +11486,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-installed-globally@0.4.0: - dependencies: - global-dirs: 3.0.1 - is-path-inside: 3.0.3 - is-interactive@1.0.0: {} is-map@2.0.3: {} @@ -12102,8 +11518,6 @@ snapshots: dependencies: call-bound: 1.0.3 - is-stream@2.0.1: {} - is-stream@3.0.0: {} is-string@1.1.1: @@ -12129,8 +11543,6 @@ snapshots: dependencies: which-typed-array: 1.1.18 - is-typedarray@1.0.0: {} - is-unicode-supported@0.1.0: {} is-utf8@0.2.1: {} @@ -12156,8 +11568,6 @@ snapshots: isomorphic.js@0.2.5: {} - isstream@0.1.2: {} - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -12187,8 +11597,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsbn@0.1.1: {} - jsesc@3.0.2: {} jsesc@3.1.0: {} @@ -12201,12 +11609,8 @@ snapshots: json-schema-traverse@1.0.0: {} - json-schema@0.4.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - json-stringify-safe@5.0.1: {} - json5@1.0.2: dependencies: minimist: 1.2.8 @@ -12225,13 +11629,6 @@ snapshots: jsonparse@1.3.1: {} - jsprim@2.0.2: - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.4.0 - verror: 1.10.0 - katex@0.16.22: dependencies: commander: 8.3.0 @@ -12292,19 +11689,6 @@ snapshots: transitivePeerDependencies: - supports-color - listr2@3.14.0(enquirer@2.4.1): - dependencies: - cli-truncate: 2.1.0 - colorette: 2.0.20 - log-update: 4.0.0 - p-map: 4.0.0 - rfdc: 1.4.1 - rxjs: 7.8.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - optionalDependencies: - enquirer: 2.4.1 - listr2@8.2.5: dependencies: cli-truncate: 4.0.0 @@ -12346,8 +11730,6 @@ snapshots: lodash.mergewith@4.6.2: {} - lodash.once@4.1.1: {} - lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -12365,13 +11747,6 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-update@4.0.0: - dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 - log-update@6.1.0: dependencies: ansi-escapes: 7.0.0 @@ -12649,8 +12024,6 @@ snapshots: os-tmpdir@1.0.2: {} - ospath@1.2.2: {} - outdent@0.5.0: {} own-keys@1.0.1: @@ -12689,10 +12062,6 @@ snapshots: p-map@2.1.0: {} - p-map@4.0.0: - dependencies: - aggregate-error: 3.1.0 - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} @@ -12733,10 +12102,6 @@ snapshots: pathe@2.0.3: {} - pend@1.2.0: {} - - performance-now@2.1.0: {} - periscopic@3.1.0: dependencies: '@types/estree': 1.0.6 @@ -12778,10 +12143,18 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 + playwright-core@1.60.0: {} + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + portfinder@1.0.32: dependencies: async: 2.6.4 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7 mkdirp: 0.5.6 transitivePeerDependencies: - supports-color @@ -12839,8 +12212,6 @@ snapshots: prettier@3.3.3: {} - pretty-bytes@5.6.0: {} - pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -12849,8 +12220,6 @@ snapshots: prismjs@1.30.0: {} - process@0.11.10: {} - property-information@6.5.0: {} prosemirror-changeset@2.3.0: @@ -12964,13 +12333,6 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.2 - proxy-from-env@1.0.0: {} - - pump@3.0.2: - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -13111,10 +12473,6 @@ snapshots: remixicon@2.5.0: {} - request-progress@3.0.0: - dependencies: - throttleit: 1.0.1 - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -13427,7 +12785,7 @@ snapshots: simple-peer@9.11.1: dependencies: buffer: 6.0.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 err-code: 3.0.1 get-browser-rtc: 1.1.0 queue-microtask: 1.2.3 @@ -13444,18 +12802,6 @@ snapshots: slash@3.0.0: {} - slice-ansi@3.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - - slice-ansi@4.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 @@ -13490,18 +12836,6 @@ snapshots: sprintf-js@1.0.3: {} - sshpk@1.18.0: - dependencies: - asn1: 0.2.6 - assert-plus: 1.0.0 - bcrypt-pbkdf: 1.0.2 - dashdash: 1.14.1 - ecc-jsbn: 0.1.2 - getpass: 0.1.7 - jsbn: 0.1.1 - safer-buffer: 2.1.2 - tweetnacl: 0.14.5 - stackback@0.0.2: {} std-env@4.0.0: {} @@ -13570,8 +12904,6 @@ snapshots: strip-bom@4.0.0: {} - strip-final-newline@2.0.0: {} - strip-final-newline@3.0.0: {} strip-json-comments@3.1.1: {} @@ -13627,8 +12959,6 @@ snapshots: sync-message-port@1.2.0: {} - systeminformation@5.31.5: {} - tabbable@6.2.0: {} tailwindcss@3.4.17: @@ -13692,8 +13022,6 @@ snapshots: dependencies: any-promise: 1.3.0 - throttleit@1.0.1: {} - through@2.3.8: {} tiny-invariant@1.3.3: {} @@ -13721,28 +13049,16 @@ snapshots: tinyrainbow@3.1.0: {} - tldts-core@6.1.73: {} - - tldts@6.1.73: - dependencies: - tldts-core: 6.1.73 - tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 - tmp@0.2.5: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 totalist@3.0.1: {} - tough-cookie@5.1.0: - dependencies: - tldts: 6.1.73 - tr46@0.0.3: {} tree-kill@1.2.2: {} @@ -13771,7 +13087,11 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@1.14.1: {} + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 tslib@2.8.1: {} @@ -13785,7 +13105,7 @@ snapshots: cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 esbuild: 0.27.2 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 @@ -13807,10 +13127,6 @@ snapshots: - tsx - yaml - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - tunnel@0.0.6: {} turbo-darwin-64@2.3.3: @@ -13840,8 +13156,6 @@ snapshots: turbo-windows-64: 2.3.3 turbo-windows-arm64: 2.3.3 - tweetnacl@0.14.5: {} - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -13852,8 +13166,6 @@ snapshots: type-fest@0.21.3: {} - type-fest@0.8.1: {} - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.3 @@ -13952,8 +13264,6 @@ snapshots: universalify@2.0.1: {} - untildify@4.0.0: {} - update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -13980,12 +13290,6 @@ snapshots: varint@6.0.0: {} - verror@1.10.0: - dependencies: - assert-plus: 1.0.0 - core-util-is: 1.0.2 - extsprintf: 1.3.0 - vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -14106,7 +13410,7 @@ snapshots: vue-eslint-parser@9.4.3(eslint@8.57.1): dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 eslint: 8.57.1 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -14252,12 +13556,6 @@ snapshots: word-wrap@1.2.5: {} - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -14331,11 +13629,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - yjs@13.6.23: dependencies: lib0: 0.2.114 diff --git a/tests/cypress.config.js b/tests/cypress.config.js deleted file mode 100644 index 466d2fdd78..0000000000 --- a/tests/cypress.config.js +++ /dev/null @@ -1,13 +0,0 @@ -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - defaultCommandTimeout: 30000, - video: false, - e2e: { - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - baseUrl: 'http://localhost:3000', - specPattern: '../{demos,tests}/**/*.spec.{js,ts}', - }, -}) diff --git a/tests/cypress/fixtures/example.json b/tests/cypress/fixtures/example.json deleted file mode 100644 index 02e4254378..0000000000 --- a/tests/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/tests/cypress/integration/core/pluginOrder.spec.ts b/tests/cypress/integration/core/pluginOrder.spec.ts deleted file mode 100644 index f9e446d717..0000000000 --- a/tests/cypress/integration/core/pluginOrder.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -///test
') - - expect(order).to.deep.eq([1, 2, 3]) - - editor.destroy() - }) - }) - - it('should chain transforms correctly', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - const editor = new Editor({ - element, - extensions: [ - Document, - Paragraph, - Text, - Extension.create({ - name: 'replaceFoo', - priority: 100, - transformPastedHTML(html) { - return html.replace(/foo/g, 'bar') - }, - }), - Extension.create({ - name: 'replaceBar', - priority: 90, - transformPastedHTML(html) { - return html.replace(/bar/g, 'baz') - }, - }), - ], - }) - - const result = editor.view.props.transformPastedHTML?.('foo
') - - // First transform: foo -> bar (priority 100) - // Second transform: bar -> baz (priority 90) - expect(result).to.eq('baz
') - - editor.destroy() - }) - }) - - it('should integrate with baseTransform from editorProps', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - const editor = new Editor({ - element, - editorProps: { - transformPastedHTML(html) { - return html.replace(/base/g, 'replaced') - }, - }, - extensions: [ - Document, - Paragraph, - Text, - Extension.create({ - name: 'extensionTransform', - transformPastedHTML(html) { - return html.replace(/replaced/g, 'final') - }, - }), - ], - }) - - const result = editor.view.props.transformPastedHTML?.('base
') - - // Base transform runs first: base -> replaced - // Extension transform runs second: replaced -> final - expect(result).to.eq('final
') - - editor.destroy() - }) - }) - - it('should handle extensions without transforms', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - const editor = new Editor({ - element, - extensions: [ - Document, - Paragraph, - Text, - Extension.create({ - name: 'noTransform', - // No transformPastedHTML defined - }), - Extension.create({ - name: 'withTransform', - transformPastedHTML(html) { - return html.replace(/test/g, 'success') - }, - }), - ], - }) - - const result = editor.view.props.transformPastedHTML?.('test
') - - // Should still work even with extensions that don't define transformPastedHTML - expect(result).to.eq('success
') - - editor.destroy() - }) - }) - - it('should return original HTML if no transforms are defined', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - const editor = new Editor({ - element, - extensions: [ - Document, - Paragraph, - Text, - Extension.create({ - name: 'noTransform1', - }), - Extension.create({ - name: 'noTransform2', - }), - ], - }) - - const result = editor.view.props.transformPastedHTML?.('unchanged
') - - expect(result).to.eq('unchanged
') - - editor.destroy() - }) - }) - - it('should have access to extension context', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - let capturedContext: any = null - - const editor = new Editor({ - element, - extensions: [ - Document, - Paragraph, - Text, - Extension.create({ - name: 'contextChecker', - addOptions() { - return { - testOption: 'testValue', - } - }, - transformPastedHTML(html) { - capturedContext = { - name: this.name, - hasOptions: !!this.options, - hasEditor: !!this.editor, - hasStorage: this.storage !== undefined, - } - return html - }, - }), - ], - }) - - editor.view.props.transformPastedHTML?.('test
') - - expect(capturedContext).to.deep.eq({ - name: 'contextChecker', - hasOptions: true, - hasEditor: true, - hasStorage: true, - }) - - editor.destroy() - }) - }) - - it('should work with multiple transforms modifying HTML structure', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - const editor = new Editor({ - element, - extensions: [ - Document, - Paragraph, - Text, - Extension.create({ - name: 'removeStyles', - priority: 100, - transformPastedHTML(html) { - return html.replace(/\s+style="[^"]*"/gi, '') - }, - }), - Extension.create({ - name: 'addClass', - priority: 90, - transformPastedHTML(html) { - return html.replace(//g, '
') - }, - }), - ], - }) - - const result = editor.view.props.transformPastedHTML?.('
test
') - - expect(result).to.eq('test
') - - editor.destroy() - }) - }) - - it('should handle empty HTML', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - const editor = new Editor({ - element, - extensions: [ - Document, - Paragraph, - Text, - Extension.create({ - name: 'transform', - transformPastedHTML(html) { - return html || 'default
' - }, - }), - ], - }) - - const result = editor.view.props.transformPastedHTML?.('') - - expect(result).to.eq('default
') - - editor.destroy() - }) - }) - - it('should handle view parameter being passed through', () => { - cy.window().then(({ document }) => { - const element = document.createElement('div') - - document.body.append(element) - - let viewPassed = false - - const editor = new Editor({ - element, - editorProps: { - transformPastedHTML(html, view) { - viewPassed = !!view - return html - }, - }, - extensions: [Document, Paragraph, Text], - }) - - editor.view.props.transformPastedHTML?.('test
', editor.view) - - expect(viewPassed).to.equal(true) - - editor.destroy() - }) - }) -}) diff --git a/tests/cypress/plugins/index.js b/tests/cypress/plugins/index.js deleted file mode 100644 index b00f0c2bb8..0000000000 --- a/tests/cypress/plugins/index.js +++ /dev/null @@ -1,76 +0,0 @@ -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -const path = require('path') -const { globSync } = require('tinyglobby') -const webpackPreprocessor = require('@cypress/webpack-preprocessor') - -module.exports = on => { - const alias = {} - - globSync('../packages/*', { onlyDirectories: true }) - .map(name => name.replace('../packages/', '')) - .forEach(name => { - alias[`@tiptap/${name.split('/').slice(0, -1).join('/')}$`] = path.resolve(`../packages/${name}/src/index.ts`) - }) - - // Specifically resolve the pm package - globSync('../packages/pm/*', { onlyDirectories: true }) - .map(name => name.replace('../packages/pm', '')) - .forEach(name => { - alias[`@tiptap/pm${name.split('/').slice(0, -1).join('/')}$`] = path.resolve(`../packages/pm/${name}/index.ts`) - }) - // Specifically resolve the static-renderer package - alias['@tiptap/static-renderer/json/html-string$'] = path.resolve( - '../packages/static-renderer/src/json/html-string/index.ts', - ) - alias['@tiptap/static-renderer/pm/html-string$'] = path.resolve( - '../packages/static-renderer/src/pm/html-string/index.ts', - ) - alias['@tiptap/static-renderer/pm/react$'] = path.resolve('../packages/static-renderer/src/pm/react/index.ts') - alias['@tiptap/static-renderer/pm/markdown$'] = path.resolve('../packages/static-renderer/src/pm/markdown/index.ts') - alias['@tiptap/static-renderer$'] = path.resolve('../packages/static-renderer/src/index.ts') - - const options = { - webpackOptions: { - module: { - rules: [ - { - test: /\.tsx?$/, - loader: 'ts-loader', - exclude: /node_modules/, - options: { - configFile: path.resolve(__dirname, '..', 'tsconfig.json'), - transpileOnly: true, - }, - }, - { - test: /\.jsx?$/, - use: 'babel-loader', - exclude: /node_modules/, - }, - ], - }, - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - alias, - extensionAlias: { - '.js': ['.js', '.ts'], - '.jsx': ['.jsx', '.tsx'], - }, - }, - }, - } - - on('file:preprocessor', webpackPreprocessor(options)) -} diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js deleted file mode 100644 index f2d56fa648..0000000000 --- a/tests/cypress/support/commands.js +++ /dev/null @@ -1,101 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) - -function defaults(object, name, value) { - if (!object) { - return { - [name]: value, - } - } - - if (object[name] === undefined) { - return { - ...object, - [name]: value, - } - } - - return object -} - -Cypress.Commands.overwrite('trigger', (originalFn, element, text, options) => { - if (text === 'keydown') { - const isMac = Cypress.platform === 'darwin' - const { modKey, ...rest } = options - - if (modKey) { - const newOptions = { - ...defaults(rest, 'force', true), - ...(isMac ? { metaKey: modKey } : { ctrlKey: modKey }), - } - - return originalFn(element, text, newOptions) - } - } - - return originalFn(element, text, options) -}) - -Cypress.Commands.overwrite('type', (originalFn, element, text, options) => { - const newOptions = defaults(options, 'force', true) - - return originalFn(element, text, newOptions) -}) - -Cypress.Commands.overwrite('click', (originalFn, element, text, options) => { - const newOptions = defaults(options, 'force', true) - - return originalFn(element, text, newOptions) -}) - -Cypress.Commands.add('paste', { prevSubject: true }, (subject, pasteOptions) => { - // Support both { pastePayload, pasteType } and shorthand { paste } - const pastePayload = pasteOptions?.paste ?? pasteOptions?.pastePayload - let pasteType = pasteOptions?.pasteType - - if (!pasteType) { - // default to application/json for objects, text/plain for strings - pasteType = typeof pastePayload === 'string' ? 'text/plain' : 'application/json' - } - - const data = pasteType === 'application/json' ? JSON.stringify(pastePayload) : pastePayload - // https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer - const clipboardData = new DataTransfer() - - clipboardData.setData(pasteType, data) - // https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event - const pasteEvent = new ClipboardEvent('paste', { - bubbles: true, - cancelable: true, - dataType: pasteType, - data, - clipboardData, - }) - - subject[0].dispatchEvent(pasteEvent) - - return subject -}) diff --git a/tests/cypress/support/e2e.js b/tests/cypress/support/e2e.js deleted file mode 100644 index b6ca346622..0000000000 --- a/tests/cypress/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands.js' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/tests/cypress/tsconfig.json b/tests/cypress/tsconfig.json deleted file mode 100644 index 5bccd40363..0000000000 --- a/tests/cypress/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "moduleResolution": "nodenext", - "module": "nodenext", - "strict": false, - "noEmit": false, - "sourceMap": false, - "importHelpers": false, - "types": ["cypress", "react", "react-dom"], - "paths": { - "@tiptap/pm/*": ["packages/pm/*"], - "@tiptap/static-renderer/pm/*": ["packages/static-renderer/src/pm/*"], - "@tiptap/static-renderer/json/*": ["packages/static-renderer/src/json/*"], - "@tiptap/*": ["packages/*/src/index.ts", "packages-deprecated/*/src/index.ts"] - }, - "typeRoots": ["../../node_modules/@types", "../../node_modules/"] - }, - "include": ["./*/*.ts", "../../**/*.ts"], - "exclude": [ - "../../packages/react", - "../../packages/vue-2", - "../../packages/vue-3", - "../../packages/extension-code-block-lowlight" - ] -} diff --git a/tests/package.json b/tests/package.json deleted file mode 100644 index 3dbc1ca591..0000000000 --- a/tests/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -}