A suite I reviewed had 180 end-to-end tests for form validation. Each one logged in, navigated to a form, typed an invalid value, and checked the error message. The suite took fifty minutes and most of it was this.
Every one of those tests was checking a component. None of them needed a login, a database, or a page.
Cypress component testing mounts a single component in a real browser, without your application around it:
import { ContactForm } from '../../src/components/ContactForm';
it('shows an error when the email is malformed', () => {
cy.mount(<ContactForm onSubmit={cy.stub()} />);
cy.get('[data-testid="email"]').type('not-an-email');
cy.get('[data-testid="submit"]').click();
cy.get('[data-testid="email-error"]').should('contain', 'valid email');
});
That runs in a few hundred milliseconds instead of eight seconds, and it fails pointing directly at the component rather than at a page.
The mechanics are the easy part. The senior contribution is the boundary rule, because without one a team ends up testing everything twice.
Put it in a component test when the behaviour lives entirely inside the component.
Validation messages. Conditional rendering. Loading and empty states. Formatting. Disabled-button logic. Accessibility attributes. Anything where the component's props and internal state fully determine the outcome.
These are the tests that make an E2E suite slow for no coverage gain, because the page around them contributes nothing to what is being checked.
Put it in an end-to-end test when the behaviour lives between things.
Routing and navigation. Real requests reaching a real server. Data persisting across a reload. Authentication and permissions. Anything crossing more than one screen.
The rule in one sentence: if the test would still be meaningful with the rest of the app deleted, it is a component test.
Where teams get this wrong, in both directions.
Testing the framework, not your component. Asserting that clicking a button calls your click handler is testing React, not you. Assert the visible outcome instead.
Mocking so heavily nothing is left. A component test that stubs every child component and every hook is asserting on a structure you invented for the test. Mount the real children where you can.
Moving E2E tests down wholesale. A checkout test is not a component test with the network stubbed. If the value was in the integration, moving it down deletes the value while keeping the maintenance.
The migration, when you inherit a slow suite. Do not rewrite. Go through the E2E suite asking one question per test: does this need the app around it? The 180 validation tests answer no. Move those, delete the originals, and the suite gets dramatically faster with no coverage lost. That is one of the highest-value pieces of work a senior engineer can do on an inherited suite, and it is measurable, which makes it easy to justify.
One honest limitation. Component tests exercise your component in isolation, which means they cannot tell you the component is wired into the page correctly. Keep enough end-to-end coverage to prove the assembly, then push detail downward.
Remember this: the boundary is whether the behaviour needs the app around it. Detail goes down, integration stays up.
Reference: Component testing.