Same Start. Same End. Wildly Different Effort. One auth Flow with register and forgotten-password branches, side by side with a fair Playwright sketch.
Want to see the same test in DoesQA and Playwright?
Cool. Pick a journey real humans do: create an account with email verification, or reset a forgotten password with email. Same door in. Same "you're in" screen out. Two branches that start together and finish together.
In DoesQA this can be one Flow. Shared Open. Shared first clicks. Split. Join. Email inbox included.
In Playwright that is... well. Selectors. Page objects. A mail helper you wired up at 11pm. A remote runner you do not fully control. And a second copy of half of that when the forgotten password path "is basically the same" until it isn't.
If you want hundreds of lines of code, a huge amount of duplication, and flaky runs on third-party remote runners, the choice is clear.
If you want solid, easy-to-maintain quality assurance, check out DoesQA.
The journey
One product surface. Two paths:
Register with email verification
Forgotten password with email reset
Both open the app, hit Register, confirm the Login / Register screen, then split. Both end on Welcome. In DoesQA that shape is a later split: shared start, two branches, one join.
That canvas is the whole point. Shared start. Branch labels. Mail steps on both sides. One Welcome check at the bottom. Two Test Cases from one Flow.
Open https://example.com on Chrome. Touch Register. Check Login / Register is displayed.
Register branch
Set Email to testEmail. Set Password to NEW_PASSWORD. Touch Submit. Wait for new Test Case email. Open latest. Touch Verify email link.
Forgotten Password branch
Touch Already have an account. Set Email to testEmail. Touch Submit. Wait for new Test Case email. Open latest. Touch Reset password link. Set Password to NEW_PASSWORD. Touch Save new password.
Shared end
Check Welcome heading text contains Welcome.
No custom mail server in the test. No second framework for inbox. The Test Case inbox is a first-class step. Selectors live in shared Elements. Change Email once; both branches pick it up.
That is what "same start, same end" looks like when the product was built for journeys, not for assembling a lab around a browser API.
The same test in Playwright
Here is a fair, slightly-better-than-average sketch of that same Flow. Page objects. A small mail helper. Two specs that share the start and still duplicate the glue.
// auth.pages.tsimport{Page,expect}from'@playwright/test';exportclass AuthPages {constructor(private readonly page: Page){}asyncopen(){awaitthis.page.goto('https://example.com');}asynctouchRegister(){awaitthis.page.getByRole('link',{name:'Register'}).click();}asyncexpectLoginOrRegister(){awaitexpect(this.page.getByText('Login / Register')).toBeVisible();}asynctouchAlreadyHaveAnAccount(){awaitthis.page.getByRole('link',{name:'Already have an account'}).click();}asyncfillEmail(email: string){awaitthis.page.getByLabel('Email').fill(email);}asyncfillPassword(password: string){awaitthis.page.getByLabel('Password').fill(password);}asyncsubmit(){awaitthis.page.getByRole('button',{name:'Submit'}).click();}asyncsaveNewPassword(){awaitthis.page.getByRole('button',{name:'Save new password'}).click();}asyncexpectWelcome(){awaitexpect(this.page.getByRole('heading',{name:/Welcome/i})).toBeVisible();}}
// auth.pages.tsimport{Page,expect}from'@playwright/test';exportclass AuthPages {constructor(private readonly page: Page){}asyncopen(){awaitthis.page.goto('https://example.com');}asynctouchRegister(){awaitthis.page.getByRole('link',{name:'Register'}).click();}asyncexpectLoginOrRegister(){awaitexpect(this.page.getByText('Login / Register')).toBeVisible();}asynctouchAlreadyHaveAnAccount(){awaitthis.page.getByRole('link',{name:'Already have an account'}).click();}asyncfillEmail(email: string){awaitthis.page.getByLabel('Email').fill(email);}asyncfillPassword(password: string){awaitthis.page.getByLabel('Password').fill(password);}asyncsubmit(){awaitthis.page.getByRole('button',{name:'Submit'}).click();}asyncsaveNewPassword(){awaitthis.page.getByRole('button',{name:'Save new password'}).click();}asyncexpectWelcome(){awaitexpect(this.page.getByRole('heading',{name:/Welcome/i})).toBeVisible();}}
// auth.pages.tsimport{Page,expect}from'@playwright/test';exportclass AuthPages {constructor(private readonly page: Page){}asyncopen(){awaitthis.page.goto('https://example.com');}asynctouchRegister(){awaitthis.page.getByRole('link',{name:'Register'}).click();}asyncexpectLoginOrRegister(){awaitexpect(this.page.getByText('Login / Register')).toBeVisible();}asynctouchAlreadyHaveAnAccount(){awaitthis.page.getByRole('link',{name:'Already have an account'}).click();}asyncfillEmail(email: string){awaitthis.page.getByLabel('Email').fill(email);}asyncfillPassword(password: string){awaitthis.page.getByLabel('Password').fill(password);}asyncsubmit(){awaitthis.page.getByRole('button',{name:'Submit'}).click();}asyncsaveNewPassword(){awaitthis.page.getByRole('button',{name:'Save new password'}).click();}asyncexpectWelcome(){awaitexpect(this.page.getByRole('heading',{name:/Welcome/i})).toBeVisible();}}
// mail.ts// "Slightly better than average" still means you own this.exportasyncfunctionwaitForNewMessage(inboxId: string){// poll your mail API / Mailosaur / custom inbox service}exportasyncfunctionopenLatestAndClick(linkText: RegExp){// fetch HTML, find anchor, return href for page.goto}
// mail.ts// "Slightly better than average" still means you own this.exportasyncfunctionwaitForNewMessage(inboxId: string){// poll your mail API / Mailosaur / custom inbox service}exportasyncfunctionopenLatestAndClick(linkText: RegExp){// fetch HTML, find anchor, return href for page.goto}
// mail.ts// "Slightly better than average" still means you own this.exportasyncfunctionwaitForNewMessage(inboxId: string){// poll your mail API / Mailosaur / custom inbox service}exportasyncfunctionopenLatestAndClick(linkText: RegExp){// fetch HTML, find anchor, return href for page.goto}
That is the polite version of the canvas you just looked at. You still have not counted CI config, parallel workers, the browser cloud bill, reporting, flake retries, and the meeting where someone asks why register is green while reset is "flaky again".
How long does this take?
Rough estimate, from scratch, for a very good Playwright developer who already knows the tooling: a day to a day and a half.
That is project setup, page objects, standing up a mail provider, wiring the helper, both paths green once, and the usual "why is the inbox empty" diversion. Not CI. Not a runner farm. Not the second week of flakes.
The same Flow in DoesQA, from a newly created blank account: about 10 minutes.
Open. Touch. Check. Branch. Mail steps. Welcome. Done. Hosted runners and Results are already there.
Playwright is good at driving a single browser. It is not a mail product, a runner product, a results product, or a maintenance product. Those arrive as extra code and extra vendors. See The Real Cost of Playwright and DoesQA compared.
Where DoesQA is better (and we're not shy about it)
Edit both specs and hope the page object still matches
Add a third auth path
Another branch on the same Flow
Another spec file, more shared-but-not-shared glue
Debug a fail
Step timeline, screenshot, video on the Run
Logs, your reporter, the runner vendor's UI
DoesQA ships authoring, shared Steps, hosted runners, and Results as one platform. When something is wrong, it is DoesQA's problem to fix the platform. That is the difference between a Flow and a second development repo that needs to be maintained.
For more on the benefits of Codeless versus code: Codeless vs coded.
So... which one is the same test?
Both prove register and forgotten password.
Only one keeps the shared start and shared end as a single maintained object without creating a second engineering team.
If you want hundreds of lines of code, a huge amount of duplication, and flaky runs on third-party remote runners, Playwright is free. Help yourself.