Skip to content

Conversation

@appflowy
Copy link
Contributor

@appflowy appflowy commented Nov 16, 2025

Description


Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Summary by Sourcery

Organize and streamline the Cypress E2E test suite by centralizing test configuration, error handling, and authentication. Introduce shared support modules (loginTestUser command, exception-handlers, TestConfig) and refactor existing tests to use these utilities, eliminating duplication and improving maintainability.

New Features:

  • Add Cypress custom command 'loginTestUser' to centralize test user authentication
  • Introduce exception-handlers support module to ignore known non-critical errors
  • Introduce TestConfig support module for centralized environment configuration and logging

Enhancements:

  • Refactor all Cypress tests to use 'loginTestUser', 'setupCommonExceptionHandlers', and TestConfig values
  • Simplify and modularize share-page and other tests with TestTool utilities and reduced redundant waits
  • Standardize environment logging and remove scattered AuthTestUtils-based login flows

@sourcery-ai
Copy link

sourcery-ai bot commented Nov 16, 2025

Reviewer's Guide

This PR reorganizes the Cypress test suite by centralizing common utilities and configuration, removing repetitive boilerplate, and streamlining key test flows (notably the Share Page tests). Custom commands and support modules replace scattered login, exception handling, and environment-logging code, resulting in a more maintainable and consistent test base.

Sequence diagram for centralized login flow using custom command

sequenceDiagram
    actor Tester
    participant Cypress
    participant AuthTestUtils
    Tester->>Cypress: cy.loginTestUser(email?)
    Cypress->>AuthTestUtils: signInWithTestUrl(email)
    AuthTestUtils-->>Cypress: authentication result
    Cypress-->>Tester: returns test email
Loading

Sequence diagram for exception handling setup in tests

sequenceDiagram
    participant TestFile
    participant ExceptionHandlers
    TestFile->>ExceptionHandlers: setupCommonExceptionHandlers()
    ExceptionHandlers-->>TestFile: sets up uncaught:exception handler
Loading

Class diagram for new Cypress support modules and custom commands

classDiagram
    class TestConfig {
        +baseUrl: string
        +gotrueUrl: string
        +apiUrl: string
        +logTestEnvironment()
    }
    class ExceptionHandlers {
        +setupCommonExceptionHandlers(additionalPatterns: string[])
        +ignoreAllExceptions()
    }
    class Cypress {
        +loginTestUser(email?: string): Chainable<string>
    }
    TestConfig <.. Cypress : uses
    ExceptionHandlers <.. Cypress : uses
Loading

File-Level Changes

Change Details Files
Introduce custom cy.loginTestUser command to unify login flows
  • Define cy.loginTestUser in support/commands.ts
  • Remove AuthTestUtils imports and manual cy.visit/login/wait code in tests
  • Update all tests to call loginTestUser().then(email => …) instead of inline login logic
cypress/support/commands.ts
cypress/e2e/**/*.cy.ts
Extract common exception handling into a centralized module
  • Create support/exception-handlers.ts with IGNORE_ERROR_PATTERNS
  • Replace inline cy.on('uncaught:exception') blocks in tests with setupCommonExceptionHandlers calls
cypress/support/exception-handlers.ts
cypress/e2e/**/*.cy.ts
Centralize environment configuration and logging via TestConfig
  • Add support/test-config.ts exporting baseUrl, gotrueUrl, apiUrl and logTestEnvironment()
  • Replace scattered cy.task('log') env dumps with logTestEnvironment() in before hooks
cypress/support/test-config.ts
cypress/e2e/breadcrumb-navigation.cy.ts
cypress/e2e/chat/chat-input.cy.ts
cypress/e2e/chat/create-ai-chat.cy.ts
cypress/e2e/chat/model-selection-persistence.cy.ts
cypress/e2e/database/row-insertion.cy.ts
cypress/e2e/chat/selection-mode.cy.ts
cypress/e2e/database/checkbox-column.cy.ts
cypress/e2e/database/datetime-column.cy.ts
Refactor share-page.cy.ts to streamline the share/unshare flow
  • Remove lengthy manual share invitation and removal steps
  • Use TestTool.waitForPageLoad/SidebarReady for loading checks
  • Implement a linear multi-user sign-out/sign-in and share verification flow
cypress/e2e/page/share-page.cy.ts

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

@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 there - I've reviewed your changes - here's some feedback:

  • There are still many fixed-duration cy.wait calls (e.g. in loginTestUser and share tests); consider swapping these for element- or network-based waits (like waiting for specific UI elements or API responses) to make the tests faster and less flaky.
  • Several new hard-coded selectors (e.g. [data-testid="share-button"], .filter callbacks) appear directly in tests—extract these into your centralized selector files to keep the tests DRY and maintainable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are still many fixed-duration cy.wait calls (e.g. in loginTestUser and share tests); consider swapping these for element- or network-based waits (like waiting for specific UI elements or API responses) to make the tests faster and less flaky.
- Several new hard-coded selectors (e.g. `[data-testid="share-button"]`, `.filter` callbacks) appear directly in tests—extract these into your centralized selector files to keep the tests DRY and maintainable.

## Individual Comments

### Comment 1
<location> `cypress/support/commands.ts:43` </location>
<code_context>
+ * ```
+ */
+Cypress.Commands.add('loginTestUser', (email?: string) => {
+  const { v4: uuidv4 } = require('uuid');
+  const { AuthTestUtils } = require('./auth-utils');
+
</code_context>

<issue_to_address>
**nitpick:** Consider moving require statements to the top of the file for consistency.

Moving require statements to the top enhances readability, maintainability, and compatibility with static analysis tools.
</issue_to_address>

### Comment 2
<location> `cypress/support/exception-handlers.ts:52` </location>
<code_context>
+ *
+ * @param additionalPatterns - Optional array of additional error patterns to ignore
+ */
+export const setupCommonExceptionHandlers = (additionalPatterns: string[] = []) => {
+  const allPatterns = [...IGNORED_ERROR_PATTERNS, ...additionalPatterns];
+
</code_context>

<issue_to_address>
**suggestion:** Consider making error pattern matching case-insensitive for broader coverage.

Matching errors in a case-insensitive way will help ensure all relevant messages are properly ignored, regardless of capitalization.

Suggested implementation:

```typescript
export const setupCommonExceptionHandlers = (additionalPatterns: string[] = []) => {
  // Convert all patterns to lowercase for case-insensitive matching
  const allPatterns = [...IGNORED_ERROR_PATTERNS, ...additionalPatterns].map(pattern => pattern.toLowerCase());

```

You will also need to update any code that matches error messages against these patterns (e.g., in event handlers or filter functions) to convert the error message to lowercase before checking for a match. For example:

<<<<<<< SEARCH
if (allPatterns.some(pattern => errorMessage.includes(pattern))) {
=======
if (allPatterns.some(pattern => errorMessage.toLowerCase().includes(pattern))) {
>>>>>>> REPLACE

Make sure to apply this change wherever error messages are checked against the patterns.
</issue_to_address>

### Comment 3
<location> `cypress/support/exception-handlers.ts:55` </location>
<code_context>
    */
   setupBeforeEach: () => {
-    // Suppress known transient errors
-    cy.on('uncaught:exception', (err) => {
-      if (
-        err.message.includes('Minified React error') ||
</code_context>

<issue_to_address>
**suggestion (testing):** Logging to console may not be visible in CI environments; consider using cy.task for logging.

Using cy.task('log', ...) will ensure that these logs appear in CI test output, unlike console.warn.
</issue_to_address>

### Comment 4
<location> `cypress/e2e/page/share-page.cy.ts:25` </location>
<code_context>
         userBEmail = generateRandomEmail();
     });

     it('should invite user B to page via email and then remove their access', () => {
-        // Handle uncaught exceptions during workspace creation
-        cy.on('uncaught:exception', (err: Error) => {
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the test by extracting repeated flows into custom Cypress commands and replacing manual waits with assertion-based waits for improved readability and maintainability.

Here are a few quick wins to make that single “invite → remove” spec far more readable and maintainable without losing any of the new functionality:

1. Extract your common flows into Cypress commands (login, logout, wait‐for‐app, share/remove, etc.).  
2. Replace hard `cy.wait(...)`/`waitForReactUpdate` calls with polite “.should(…)” awaits inside those commands.  
3. Collapse the big `it()` down to a linear, high-level story.

### support/commands.ts
```ts
Cypress.Commands.add('login', (email: string) => {
  cy.visit('/login', { failOnStatusCode: false });
  cy.wait(1000);
  new AuthTestUtils().signInWithTestUrl(email);
  // wait for shell to load
  TestTool.waitForPageLoad();
  TestTool.waitForSidebarReady();
});

Cypress.Commands.add('logout', () => {
  WorkspaceSelectors.dropdownTrigger().click();
  cy.get('[data-testid="logout-button"]').click();
  cy.wait(1000);
});

Cypress.Commands.add('createPage', (): Cypress.Chainable<string> => {
  PageSelectors.newPageButton().click();
  return PageSelectors.names().last()
    .should('be.visible')
    .invoke('text');
});

Cypress.Commands.add('sharePage', (pageTitle: string, email: string) => {
  cy.get(`[data-testid="share-button"]`).click();
  cy.get('[data-testid="share-input"]').type(email);
  cy.get('[data-testid="permission-dropdown"]').click();
  cy.get('[data-testid="permission-can-edit"]').click();
  cy.get('[data-testid="share-invite-button"]').click();
  return cy.get('[data-testid="share-member-list"]')
           .should('contain', email);
});

Cypress.Commands.add('removeShare', (email: string) => {
  cy.get(`[data-testid="remove-member-${email}"]`).click();
  cy.get('[data-testid="share-member-list"]')
    .should('not.contain', email);
});
```

### Updated spec
```ts
it('invites user B then removes their access', () => {
  cy.login(userAEmail);
  cy.createPage().then(pageTitle => {
    cy.sharePage(pageTitle, userBEmail);
    cy.logout();

    cy.login(userBEmail);
    // verify shared page is visible under “Shared with me”
    cy.get('[data-testid="shared-with-me-section"]').click();
    cy.contains(pageTitle).should('exist');
    cy.logout();

    cy.login(userAEmail);
    cy.contains(pageTitle).click();
    cy.removeShare(userBEmail);
  });
});
```

This keeps every step but:

- removes deep nesting  
- zeroes out magic waits  
- makes each line express *intent*, not low-level clicks.
</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.

* ```
*/
Cypress.Commands.add('loginTestUser', (email?: string) => {
const { v4: uuidv4 } = require('uuid');
Copy link

Choose a reason for hiding this comment

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

nitpick: Consider moving require statements to the top of the file for consistency.

Moving require statements to the top enhances readability, maintainability, and compatibility with static analysis tools.

*
* @param additionalPatterns - Optional array of additional error patterns to ignore
*/
export const setupCommonExceptionHandlers = (additionalPatterns: string[] = []) => {
Copy link

Choose a reason for hiding this comment

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

suggestion: Consider making error pattern matching case-insensitive for broader coverage.

Matching errors in a case-insensitive way will help ensure all relevant messages are properly ignored, regardless of capitalization.

Suggested implementation:

export const setupCommonExceptionHandlers = (additionalPatterns: string[] = []) => {
  // Convert all patterns to lowercase for case-insensitive matching
  const allPatterns = [...IGNORED_ERROR_PATTERNS, ...additionalPatterns].map(pattern => pattern.toLowerCase());

You will also need to update any code that matches error messages against these patterns (e.g., in event handlers or filter functions) to convert the error message to lowercase before checking for a match. For example:

<<<<<<< SEARCH
if (allPatterns.some(pattern => errorMessage.includes(pattern))) {

if (allPatterns.some(pattern => errorMessage.toLowerCase().includes(pattern))) {

REPLACE

Make sure to apply this change wherever error messages are checked against the patterns.

userBEmail = generateRandomEmail();
});

it('should invite user B to page via email and then remove their access', () => {
Copy link

Choose a reason for hiding this comment

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

issue (complexity): Consider refactoring the test by extracting repeated flows into custom Cypress commands and replacing manual waits with assertion-based waits for improved readability and maintainability.

Here are a few quick wins to make that single “invite → remove” spec far more readable and maintainable without losing any of the new functionality:

  1. Extract your common flows into Cypress commands (login, logout, wait‐for‐app, share/remove, etc.).
  2. Replace hard cy.wait(...)/waitForReactUpdate calls with polite “.should(…)” awaits inside those commands.
  3. Collapse the big it() down to a linear, high-level story.

support/commands.ts

Cypress.Commands.add('login', (email: string) => {
  cy.visit('/login', { failOnStatusCode: false });
  cy.wait(1000);
  new AuthTestUtils().signInWithTestUrl(email);
  // wait for shell to load
  TestTool.waitForPageLoad();
  TestTool.waitForSidebarReady();
});

Cypress.Commands.add('logout', () => {
  WorkspaceSelectors.dropdownTrigger().click();
  cy.get('[data-testid="logout-button"]').click();
  cy.wait(1000);
});

Cypress.Commands.add('createPage', (): Cypress.Chainable<string> => {
  PageSelectors.newPageButton().click();
  return PageSelectors.names().last()
    .should('be.visible')
    .invoke('text');
});

Cypress.Commands.add('sharePage', (pageTitle: string, email: string) => {
  cy.get(`[data-testid="share-button"]`).click();
  cy.get('[data-testid="share-input"]').type(email);
  cy.get('[data-testid="permission-dropdown"]').click();
  cy.get('[data-testid="permission-can-edit"]').click();
  cy.get('[data-testid="share-invite-button"]').click();
  return cy.get('[data-testid="share-member-list"]')
           .should('contain', email);
});

Cypress.Commands.add('removeShare', (email: string) => {
  cy.get(`[data-testid="remove-member-${email}"]`).click();
  cy.get('[data-testid="share-member-list"]')
    .should('not.contain', email);
});

Updated spec

it('invites user B then removes their access', () => {
  cy.login(userAEmail);
  cy.createPage().then(pageTitle => {
    cy.sharePage(pageTitle, userBEmail);
    cy.logout();

    cy.login(userBEmail);
    // verify shared page is visible under “Shared with me”
    cy.get('[data-testid="shared-with-me-section"]').click();
    cy.contains(pageTitle).should('exist');
    cy.logout();

    cy.login(userAEmail);
    cy.contains(pageTitle).click();
    cy.removeShare(userBEmail);
  });
});

This keeps every step but:

  • removes deep nesting
  • zeroes out magic waits
  • makes each line express intent, not low-level clicks.

@appflowy appflowy closed this Nov 17, 2025
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.

2 participants