BlogApp E2E Flows — Critical User Journeys

Critical user journeys are the highest-value E2E tests — they verify that the most important workflows function correctly from end to end. For the BlogApp, these are: browsing and reading posts, creating and publishing a post (admin), and submitting comments. Each journey exercises the full stack — Angular routing, HTTP calls, ASP.NET Core API, EF Core, and SQL Server — producing the highest confidence that the application works as users expect.

BlogApp E2E User Journeys

// ── Journey 1: Public post browsing ──────────────────────────────────────
describe('Public post browsing', () => {
  beforeEach(() => cy.visit('/'));

  it('loads the post list and navigates to a post', () => {
    cy.intercept('GET', '/api/posts*').as('getPosts');
    cy.visit('/');
    cy.wait('@getPosts');

    cy.getByCy('post-card').should('have.length.gt', 0);
    cy.getByCy('post-card').first().within(() => {
      cy.getByCy('post-title').invoke('text').as('firstTitle');
    });

    // Click the first post
    cy.getByCy('post-card').first().getByCy('read-more-btn').click();
    cy.url().should('match', /\/posts\/.+/);

    // Verify the post title matches
    cy.get('@firstTitle').then(title => {
      cy.get('h1').should('contain', title as string);
    });
  });

  it('filters posts by category', () => {
    cy.intercept('GET', '/api/posts?*category=dotnet*').as('filteredPosts');
    cy.getByCy('category-link-dotnet').click();
    cy.wait('@filteredPosts');
    cy.url().should('include', 'category=dotnet');
    cy.getByCy('active-category').should('contain', '.NET');
  });

  it('paginates through posts', () => {
    cy.getByCy('next-page-btn').click();
    cy.url().should('include', 'page=2');
    cy.getByCy('post-card').should('have.length.gt', 0);
  });
});

// ── Journey 2: Admin post creation and publishing ─────────────────────────
describe('Admin post creation', () => {
  beforeEach(() => cy.loginAsAdmin());

  it('creates and publishes a post end-to-end', () => {
    const uniqueTitle = `E2E Test Post ${Date.now()}`;
    const uniqueSlug  = `e2e-test-post-${Date.now()}`;

    cy.visit('/admin/posts/new');
    cy.getByCy('title-input').type(uniqueTitle);
    cy.getByCy('slug-input').should('have.value', uniqueSlug.toLowerCase().replace(/ /g, '-'));

    cy.getByCy('body-editor').type(
      'This is the body of the test post. It has enough content to pass validation.'
    );

    // Upload a cover image
    cy.getByCy('cover-image-upload').selectFile('cypress/fixtures/test-image.jpg', {
      action: 'drag-drop',
    });
    cy.getByCy('image-preview').should('be.visible');
    cy.getByCy('upload-progress').should('not.exist');  // upload complete

    // Publish
    cy.intercept('POST', '/api/posts').as('createPost');
    cy.getByCy('publish-button').click();
    cy.wait('@createPost').then(interception => {
      expect(interception.response!.statusCode).toBe(201);
    });

    // Verify redirected to post list
    cy.url().should('include', '/admin/posts');
    cy.getByCy('success-toast').should('contain', 'Post created');

    // Verify post appears in the public list
    cy.visit('/');
    cy.contains(uniqueTitle).should('be.visible');
  });
});

// ── Journey 3: Comment submission ─────────────────────────────────────────
describe('Comment submission', () => {
  beforeEach(() => cy.loginAsAdmin());

  it('submits a comment and sees it appear without page reload', () => {
    // Navigate to a published post
    cy.visit('/posts/getting-started-dotnet');

    const commentText = `Test comment ${Date.now()}`;

    // Submit the comment
    cy.getByCy('comment-input').type(commentText);
    cy.intercept('POST', '/api/comments').as('submitComment');
    cy.getByCy('submit-comment-btn').click();

    // Verify optimistic update appears immediately
    cy.getByCy('comment-list').should('contain', commentText);
    cy.getByCy('comment-pending-indicator').should('be.visible');

    // Wait for server confirmation
    cy.wait('@submitComment').then(interception => {
      expect(interception.response!.statusCode).toBe(201);
    });

    // Pending indicator gone, comment persisted
    cy.getByCy('comment-pending-indicator').should('not.exist');
    cy.getByCy('comment-input').should('have.value', '');  // cleared after submit
  });
});
Note: Using Date.now() in test data (post titles, slugs) ensures uniqueness across test runs — a post created in one run doesn’t conflict with the next run’s test. This is the Cypress equivalent of using Guid.NewGuid() in integration tests. The slug generated from the timestamp-based title is unique but not human-readable — this is acceptable for test data since it’s only used to verify the created post appears in the list, not to test slug generation logic.
Tip: Store frequently navigated URLs and repeated test data in Cypress fixtures (cypress/fixtures/) and custom commands. A fixture file published-post.json can contain a known post’s slug, making tests resilient to data changes: cy.fixture('published-post').then(post => cy.visit(`/posts/${post.slug}`)). Custom commands like cy.createPost() abstract the creation API call, making journeys that need a pre-existing post cleaner and less fragile.
Warning: E2E tests that create real data in a shared test database can interfere with each other across parallel test runs. If two E2E test agents both create a post with slug “test-post”, one will fail with a 409 Conflict. Always use unique identifiers (Date.now(), crypto.randomUUID()) in test data. Also ensure the test database is reset between CI runs — a weekly Respawn of the E2E test database or a separate test database per CI agent prevents accumulated test data interference.

Common Mistakes

Mistake 1 — Hardcoded test data that conflicts across parallel runs

❌ Wrong — cy.getByCy('title-input').type('Test Post'); second parallel agent gets 409 on duplicate slug.

✅ Correct — unique title per run: cy.getByCy('title-input').type(\`Test Post ${Date.now()}\`).

Mistake 2 — Testing too many things in one journey test (hard to diagnose failures)

❌ Wrong — one test covers create, publish, comment, view count, notification — any step failure masks all others.

✅ Correct — one critical flow per test; keep journeys focused on the primary user action and its immediate outcomes.

🧠 Test Yourself

A Cypress test clicks “Publish” and checks the success toast. The API is slow (3 seconds). The defaultCommandTimeout is 8 seconds. Does the test pass?