Skip to content

test: add editor editing BDD coverage#344

Merged
appflowy merged 4 commits into
mainfrom
editor_test
May 15, 2026
Merged

test: add editor editing BDD coverage#344
appflowy merged 4 commits into
mainfrom
editor_test

Conversation

@appflowy
Copy link
Copy Markdown
Contributor

@appflowy appflowy commented May 15, 2026

Summary

  • add Playwright BDD config and scripts for editor editing coverage
  • migrate editor edit, markdown, slash command, toolbar, toggle, paste, and undo/redo cases into a BDD feature
  • ignore generated playwright/.features-gen output

Tests

  • pnpm exec bddgen test -c playwright.bdd.config.ts
  • pnpm run type-check
  • pnpm exec prettier --check playwright.bdd.config.ts playwright/bdd/steps/editor-editing.steps.ts package.json
  • git diff --check -- .gitignore package.json pnpm-lock.yaml playwright.bdd.config.ts playwright/bdd/features/editor/editor-editing.feature playwright/bdd/steps/editor-editing.steps.ts
  • pnpm exec playwright test -c playwright.bdd.config.ts --project=chromium --workers=1

Result: 56 passed (15.5m)

Summary by Sourcery

Add Playwright BDD configuration and comprehensive editor editing feature coverage using Gherkin-based workflows.

New Features:

  • Introduce Playwright BDD configuration and scripts to run behavior-driven end-to-end tests.
  • Add a Gherkin feature describing editor editing behaviors including typing, formatting, slash commands, lists, toggles, paste, and undo/redo flows.
  • Implement shared Playwright BDD step definitions for editor interactions and assertions.

Enhancements:

  • Extend package.json with BDD-based Playwright test scripts for headless and headed runs.
  • Integrate the playwright-bdd dependency into the project.

Build:

  • Configure BDD test generation output directory for Playwright and exclude generated artifacts from version control.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 15, 2026

Reviewer's Guide

Adds Playwright BDD configuration and a comprehensive editor-editing feature suite, migrating existing editor e2e cases into Gherkin-based BDD tests and wiring them into the test runner, while ignoring generated artifacts.

Sequence diagram for Playwright BDD editor editing scenario

sequenceDiagram
  participant PlaywrightTest as Playwright_test_runner
  participant PlaywrightBdd as playwright_bdd
  participant Steps as editor_editing_steps
  participant Page as Page

  PlaywrightTest->>PlaywrightBdd: defineBddConfig(playwright.bdd.config.ts)
  PlaywrightBdd->>PlaywrightTest: generate testDir (playwright/.features-gen)

  PlaywrightTest->>PlaywrightBdd: run feature Editor_editing
  PlaywrightBdd->>Steps: Given a blank document page is open
  Steps->>Page: signInAndWaitForApp
  Steps->>Page: createDocumentPageAndNavigate
  Steps->>Page: focusEditor

  PlaywrightBdd->>Steps: When I type "text" in the editor
  Steps->>Page: insertTextIntoExpandedSelection
  Steps->>Page: keyboard.type

  PlaywrightBdd->>Steps: Then the editor contains "text"
  Steps->>Page: EditorSelectors.slateEditor
  Steps->>Page: expect.toContainText
Loading

File-Level Changes

Change Details Files
Introduce Playwright BDD configuration and wiring into existing test tooling
  • Add playwright.bdd.config.ts using defineBddConfig and Playwright defineConfig for BDD tests
  • Configure BDD feature and step discovery and generation output directory
  • Set Playwright defaults (projects, timeouts, reporters, CI behavior, permissions) for BDD runs
playwright.bdd.config.ts
Add editor editing BDD feature and step definitions covering rich editing behaviors
  • Create Gherkin feature describing editor editing scenarios (typing, markdown, slash commands, toolbar, toggles, paste, undo/redo)
  • Implement step definitions using playwright-bdd Given/When/Then plus extensive helper functions to drive the Slate-based editor via test hooks
  • Add utilities for editor focus, selection, formatting shortcuts, block inspection, slash menu interactions, paste handling, and link assertions
playwright/bdd/features/editor/editor-editing.feature
playwright/bdd/steps/editor-editing.steps.ts
Integrate BDD tests into npm scripts and dependencies and ignore generated artifacts
  • Add test:e2e:bdd and test:e2e:bdd:headed scripts that run bddgen followed by Playwright using the BDD config
  • Add playwright-bdd dependency
  • Ignore generated playwright/.features-gen output and update pnpm-lock.yaml accordingly
package.json
.gitignore
pnpm-lock.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The editor-editing.steps.ts file is quite large and mixes step definitions with a lot of helper logic; consider extracting the generic Slate/TestEditor helpers (e.g., selection, block traversal, paste/undo/redo utilities) into separate reusable modules to keep the step file focused and easier to navigate.
  • There are many waitForTimeout calls sprinkled through the steps; where possible, replace fixed delays with expectation-based waits (e.g., expect(...).to...) to make the tests less flaky and more deterministic.
  • The repeated window/__TEST_EDITOR__ access and type definitions for TestSlate* structures show similar patterns across helpers; consider centralizing the getTestEditor/getTestWindow logic and shared types to avoid duplication and reduce the chance of subtle inconsistencies.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `editor-editing.steps.ts` file is quite large and mixes step definitions with a lot of helper logic; consider extracting the generic Slate/TestEditor helpers (e.g., selection, block traversal, paste/undo/redo utilities) into separate reusable modules to keep the step file focused and easier to navigate.
- There are many `waitForTimeout` calls sprinkled through the steps; where possible, replace fixed delays with expectation-based waits (e.g., `expect(...).to...`) to make the tests less flaky and more deterministic.
- The repeated `window`/`__TEST_EDITOR__` access and type definitions for `TestSlate*` structures show similar patterns across helpers; consider centralizing the `getTestEditor`/`getTestWindow` logic and shared types to avoid duplication and reduce the chance of subtle inconsistencies.

## Individual Comments

### Comment 1
<location path="playwright/bdd/steps/editor-editing.steps.ts" line_range="653-680" />
<code_context>
+  });
+}
+
+async function pasteContent(page: Page, html: string, plainText: string) {
+  await page.evaluate(
+    ({ html, plainText }) => {
+      const getTestEditor = () => {
+        const testWindow = window as Window & {
+          __TEST_EDITOR__?: TestSlateEditor;
+          __TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
+        };
+
+        return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
+      };
+      const editor = getTestEditor();
+
+      if (!editor?.insertData) {
+        throw new Error('No test editor with insertData() found');
+      }
+
+      const dataTransfer = new DataTransfer();
+
+      if (html) dataTransfer.setData('text/html', html);
+      dataTransfer.setData('text/plain', plainText);
+      editor.insertData(dataTransfer);
+    },
+    { html, plainText }
+  );
+
+  await page.waitForTimeout(1000);
+}
+
</code_context>
<issue_to_address>
**suggestion (performance):** `pasteContent` uses a hardcoded 1s sleep after inserting data, which may be longer than necessary and still flaky.

The helper always waits 1000ms after `editor.insertData`, which can unnecessarily slow tests and still be flaky on slower environments. If the editor provides a reliable signal for paste completion (e.g., change in content or a custom event), prefer polling for that condition instead of using a fixed timeout to make the tests faster and more reliable.

```suggestion
async function pasteContent(page: Page, html: string, plainText: string) {
  // Capture the initial editor content so we can reliably detect when the paste has been applied.
  const initialContent = await page.evaluate(() => {
    const getTestEditor = () => {
      const testWindow = window as Window & {
        __TEST_EDITOR__?: TestSlateEditor;
        __TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
      };

      return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
    };

    const editor = getTestEditor();
    return editor ? editor.children : null;
  });

  await page.evaluate(
    ({ html, plainText }) => {
      const getTestEditor = () => {
        const testWindow = window as Window & {
          __TEST_EDITOR__?: TestSlateEditor;
          __TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
        };

        return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
      };
      const editor = getTestEditor();

      if (!editor?.insertData) {
        throw new Error('No test editor with insertData() found');
      }

      const dataTransfer = new DataTransfer();

      if (html) dataTransfer.setData('text/html', html);
      dataTransfer.setData('text/plain', plainText);
      editor.insertData(dataTransfer);
    },
    { html, plainText }
  );

  // Wait until the editor content has changed compared to the initial snapshot, or time out.
  // This avoids a fixed sleep and makes the tests both faster and more robust.
  await page.waitForFunction(
    (before) => {
      const getTestEditor = () => {
        const testWindow = window as Window & {
          __TEST_EDITOR__?: TestSlateEditor;
          __TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
        };

        return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
      };

      const editor = getTestEditor();
      if (!editor) return false;

      const current = editor.children;
      // If we didn't have an initial snapshot for some reason, treat any content as "changed".
      if (before == null) return !!current;

      try {
        return JSON.stringify(current) !== JSON.stringify(before);
      } catch {
        // If serialization fails for any reason, fall back to assuming it's changed.
        return true;
      }
    },
    initialContent,
    { timeout: 2000 }
  );
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +653 to +680
async function pasteContent(page: Page, html: string, plainText: string) {
await page.evaluate(
({ html, plainText }) => {
const getTestEditor = () => {
const testWindow = window as Window & {
__TEST_EDITOR__?: TestSlateEditor;
__TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
};

return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
};
const editor = getTestEditor();

if (!editor?.insertData) {
throw new Error('No test editor with insertData() found');
}

const dataTransfer = new DataTransfer();

if (html) dataTransfer.setData('text/html', html);
dataTransfer.setData('text/plain', plainText);
editor.insertData(dataTransfer);
},
{ html, plainText }
);

await page.waitForTimeout(1000);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): pasteContent uses a hardcoded 1s sleep after inserting data, which may be longer than necessary and still flaky.

The helper always waits 1000ms after editor.insertData, which can unnecessarily slow tests and still be flaky on slower environments. If the editor provides a reliable signal for paste completion (e.g., change in content or a custom event), prefer polling for that condition instead of using a fixed timeout to make the tests faster and more reliable.

Suggested change
async function pasteContent(page: Page, html: string, plainText: string) {
await page.evaluate(
({ html, plainText }) => {
const getTestEditor = () => {
const testWindow = window as Window & {
__TEST_EDITOR__?: TestSlateEditor;
__TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
};
return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
};
const editor = getTestEditor();
if (!editor?.insertData) {
throw new Error('No test editor with insertData() found');
}
const dataTransfer = new DataTransfer();
if (html) dataTransfer.setData('text/html', html);
dataTransfer.setData('text/plain', plainText);
editor.insertData(dataTransfer);
},
{ html, plainText }
);
await page.waitForTimeout(1000);
}
async function pasteContent(page: Page, html: string, plainText: string) {
// Capture the initial editor content so we can reliably detect when the paste has been applied.
const initialContent = await page.evaluate(() => {
const getTestEditor = () => {
const testWindow = window as Window & {
__TEST_EDITOR__?: TestSlateEditor;
__TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
};
return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
};
const editor = getTestEditor();
return editor ? editor.children : null;
});
await page.evaluate(
({ html, plainText }) => {
const getTestEditor = () => {
const testWindow = window as Window & {
__TEST_EDITOR__?: TestSlateEditor;
__TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
};
return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
};
const editor = getTestEditor();
if (!editor?.insertData) {
throw new Error('No test editor with insertData() found');
}
const dataTransfer = new DataTransfer();
if (html) dataTransfer.setData('text/html', html);
dataTransfer.setData('text/plain', plainText);
editor.insertData(dataTransfer);
},
{ html, plainText }
);
// Wait until the editor content has changed compared to the initial snapshot, or time out.
// This avoids a fixed sleep and makes the tests both faster and more robust.
await page.waitForFunction(
(before) => {
const getTestEditor = () => {
const testWindow = window as Window & {
__TEST_EDITOR__?: TestSlateEditor;
__TEST_EDITORS__?: Record<string, TestSlateEditor | undefined>;
};
return testWindow.__TEST_EDITOR__ ?? Object.values(testWindow.__TEST_EDITORS__ ?? {})[0];
};
const editor = getTestEditor();
if (!editor) return false;
const current = editor.children;
// If we didn't have an initial snapshot for some reason, treat any content as "changed".
if (before == null) return !!current;
try {
return JSON.stringify(current) !== JSON.stringify(before);
} catch {
// If serialization fails for any reason, fall back to assuming it's changed.
return true;
}
},
initialContent,
{ timeout: 2000 }
);
}

@appflowy appflowy merged commit dfba7be into main May 15, 2026
4 of 5 checks passed
@appflowy appflowy deleted the editor_test branch May 15, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant