Split auction scenarios into one Cypress spec each
Scenario steps move to cypress/scenarios/ and each gets a thin spec in cypress/e2e/, so the same steps can back more than one spec. Shared phones move to cypress/support/phones.ts, and "Run all specs" is enabled in the Cypress UI for playing every scenario back to back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
64ab853eeb
commit
3a9944489f
19 changed files with 239 additions and 176 deletions
|
|
@ -17,5 +17,7 @@ export default defineConfig({
|
||||||
pageLoadTimeout: 120_000,
|
pageLoadTimeout: 120_000,
|
||||||
defaultCommandTimeout: 10_000,
|
defaultCommandTimeout: 10_000,
|
||||||
video: false,
|
video: false,
|
||||||
|
// Adds "Run all specs" to `npm run e2e:open`, to play every scenario back to back.
|
||||||
|
experimentalRunAllSpecs: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
3
cypress/e2e/auction-deadline.cy.ts
Normal file
3
cypress/e2e/auction-deadline.cy.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import scenario from '../scenarios/auction-deadline';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
import { tid, type PhoneSpec } from '../support/e2e';
|
|
||||||
|
|
||||||
// Multi-phone scenarios for the ride price auction. When a rider requests a ride, every
|
|
||||||
// online, idle driver gets a sealed-bid invitation capped at the rider's quoted fare.
|
|
||||||
// The lowest bid wins; the auction closes once every driver has answered or time runs out.
|
|
||||||
//
|
|
||||||
// The fixtures make every trip 5 km / 10 min, so UberX is quoted at $11.00.
|
|
||||||
|
|
||||||
const alice: PhoneSpec = { role: 'rider', id: 'alice', name: 'Alice' };
|
|
||||||
const bob: PhoneSpec = { role: 'rider', id: 'bob', name: 'Bob' };
|
|
||||||
const maria: PhoneSpec = { role: 'driver', id: 'maria', name: 'Maria', car: 'Toyota Camry · Silver', plate: '7KDL294', rating: 4.95 };
|
|
||||||
const james: PhoneSpec = { role: 'driver', id: 'james', name: 'James', car: 'Honda Accord · Black', plate: '8TRW115', rating: 4.88 };
|
|
||||||
const aisha: PhoneSpec = { role: 'driver', id: 'aisha', name: 'Aisha', car: 'Tesla Model 3 · White', plate: '9EVA662', rating: 4.97 };
|
|
||||||
|
|
||||||
describe('ride price auction', () => {
|
|
||||||
it('awards the ride to the lowest bid and runs the trip end to end', () => {
|
|
||||||
cy.resetDispatch();
|
|
||||||
cy.openPhones([alice, maria, james, aisha]);
|
|
||||||
|
|
||||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
|
||||||
|
|
||||||
// Every driver is invited, capped at the quoted fare.
|
|
||||||
for (const driver of ['maria', 'james', 'aisha']) {
|
|
||||||
cy.phone(driver).find(tid('offer-destination')).should('have.text', 'Alice → Ferry Building');
|
|
||||||
cy.phone(driver).find(tid('offer-max-fare')).should('have.text', 'Rider pays up to $11.00');
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.bid('maria', 10);
|
|
||||||
cy.phone('maria').find(tid('offer-my-bid')).should('contain.text', 'You bid $10.00');
|
|
||||||
cy.phone('alice').find(tid('offer-count')).should('have.text', '1 offer so far');
|
|
||||||
|
|
||||||
cy.bid('james', 8.5);
|
|
||||||
cy.phone('alice').find(tid('offer-count')).should('have.text', '2 offers so far');
|
|
||||||
|
|
||||||
// The last driver answering closes the auction without waiting out the clock.
|
|
||||||
cy.pass('aisha');
|
|
||||||
|
|
||||||
cy.phone('alice').find(tid('ride-status')).should('have.text', 'Driver found');
|
|
||||||
cy.phone('alice').find(tid('ride-fare')).should('have.text', '$8.50');
|
|
||||||
cy.phone('alice').find(tid('driver-name')).should('contain.text', 'James');
|
|
||||||
cy.phone('alice').find(tid('matched-driver')).should('contain.text', '8TRW115');
|
|
||||||
|
|
||||||
cy.phone('james').find(tid('job-fare')).should('have.text', 'Fare $8.50');
|
|
||||||
cy.phone('maria').find(tid('driver-result')).should('have.text', 'Another driver won that ride');
|
|
||||||
cy.phone('aisha').find(tid('driver-idle')).should('exist');
|
|
||||||
cy.phone('aisha').find(tid('driver-result')).should('not.exist');
|
|
||||||
|
|
||||||
cy.dispatchState().then(({ rides }) => {
|
|
||||||
expect(rides).to.have.length(1);
|
|
||||||
expect(rides[0]).to.include({ status: 'matched', winnerId: 'james', fare: 8.5, maxFare: 11 });
|
|
||||||
expect(rides[0].declined).to.deep.equal(['aisha']);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The winning driver finishes the trip; the rider's phone follows.
|
|
||||||
cy.phone('james').find(tid('complete-trip')).click();
|
|
||||||
cy.phone('james').find(tid('driver-result')).should('have.text', 'Trip complete · you earned $8.50');
|
|
||||||
cy.phone('alice').find(tid('ride-status')).should('have.text', 'You’ve arrived');
|
|
||||||
cy.phone('alice').should('contain.text', 'Thanks for riding with James.');
|
|
||||||
|
|
||||||
cy.phone('alice').find(tid('trip-done')).click();
|
|
||||||
cy.phone('alice').find(tid('where-to')).should('exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('breaks a tie in favour of the earlier bid', () => {
|
|
||||||
cy.resetDispatch();
|
|
||||||
cy.openPhones([alice, maria, james]);
|
|
||||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
|
||||||
|
|
||||||
cy.bid('james', 9);
|
|
||||||
cy.phone('james').find(tid('offer-my-bid')).should('exist');
|
|
||||||
cy.bid('maria', 9);
|
|
||||||
|
|
||||||
cy.phone('alice').find(tid('driver-name')).should('contain.text', 'James');
|
|
||||||
cy.phone('maria').find(tid('driver-result')).should('have.text', 'Another driver won that ride');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not let a driver bid above the quoted fare', () => {
|
|
||||||
cy.resetDispatch();
|
|
||||||
cy.openPhones([alice, maria]);
|
|
||||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
|
||||||
|
|
||||||
cy.phone('maria').find(tid('bid-input')).clear().type('11.01');
|
|
||||||
cy.phone('maria').find(tid('submit-bid')).should('have.attr', 'aria-disabled', 'true');
|
|
||||||
|
|
||||||
cy.phone('maria').find(tid('bid-input')).clear().type('11');
|
|
||||||
cy.phone('maria').find(tid('submit-bid')).should('not.have.attr', 'aria-disabled');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('closes on the deadline and awards whoever did bid', () => {
|
|
||||||
cy.resetDispatch({ auctionDurationMs: 4000 });
|
|
||||||
cy.openPhones([alice, maria, james]);
|
|
||||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
|
||||||
|
|
||||||
cy.bid('maria', 10.75);
|
|
||||||
// James never answers; the auction stays open until the window runs out.
|
|
||||||
cy.phone('alice').find(tid('ride-status')).should('have.text', 'Finding your driver');
|
|
||||||
|
|
||||||
cy.phone('alice').find(tid('ride-status'), { timeout: 8000 }).should('have.text', 'Driver found');
|
|
||||||
cy.phone('alice').find(tid('ride-fare')).should('have.text', '$10.75');
|
|
||||||
cy.phone('james').find(tid('driver-result')).should('have.text', 'That request closed before you answered');
|
|
||||||
cy.phone('james').find(tid('offer')).should('not.exist');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('tells the rider when every driver passes', () => {
|
|
||||||
cy.resetDispatch();
|
|
||||||
cy.openPhones([alice, maria, james]);
|
|
||||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
|
||||||
|
|
||||||
cy.pass('maria');
|
|
||||||
cy.pass('james');
|
|
||||||
|
|
||||||
cy.phone('alice').find(tid('ride-status')).should('have.text', 'No drivers available');
|
|
||||||
cy.waitForDispatch((s) => s.rides[0]?.status === 'unmatched', 'ride unmatched');
|
|
||||||
|
|
||||||
// Going back lets the rider try again with a fresh auction.
|
|
||||||
cy.phone('alice').find(tid('unmatched-back')).click();
|
|
||||||
cy.phone('alice').find(tid('confirm-ride')).click();
|
|
||||||
cy.phone('maria').find(tid('offer')).should('have.length', 1);
|
|
||||||
cy.waitForDispatch((s) => s.rides.length === 2 && s.rides[1].status === 'auction', 'second auction open');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('withdraws the offer from every driver when the rider cancels', () => {
|
|
||||||
cy.resetDispatch();
|
|
||||||
cy.openPhones([alice, maria, james]);
|
|
||||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
|
||||||
|
|
||||||
cy.bid('maria', 9);
|
|
||||||
cy.phone('alice').find(tid('cancel-request')).click();
|
|
||||||
cy.phone('alice').find(tid('confirm-ride')).should('exist');
|
|
||||||
|
|
||||||
for (const driver of ['maria', 'james']) {
|
|
||||||
cy.phone(driver).find(tid('offer')).should('not.exist');
|
|
||||||
cy.phone(driver).find(tid('driver-result')).should('have.text', 'The rider cancelled their request');
|
|
||||||
}
|
|
||||||
cy.waitForDispatch((s) => s.rides[0]?.status === 'cancelled', 'ride cancelled');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('never gives one driver two rides at once', () => {
|
|
||||||
cy.resetDispatch();
|
|
||||||
cy.openPhones([alice, bob, maria, james]);
|
|
||||||
|
|
||||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
|
||||||
cy.requestRide('bob', 'ferry', 'Ferry Building');
|
|
||||||
cy.phone('maria').find(tid('offer')).should('have.length', 2);
|
|
||||||
|
|
||||||
// Maria is the cheapest on both rides.
|
|
||||||
cy.bid('maria', 7, { rider: 'Alice' });
|
|
||||||
cy.bid('maria', 6, { rider: 'Bob' });
|
|
||||||
cy.bid('james', 9, { rider: 'Bob' });
|
|
||||||
|
|
||||||
// Bob's auction has every answer, so it closes first and Maria wins it.
|
|
||||||
cy.phone('bob').find(tid('driver-name')).should('contain.text', 'Maria');
|
|
||||||
cy.phone('bob').find(tid('ride-fare')).should('have.text', '$6.00');
|
|
||||||
|
|
||||||
// Alice's auction is still waiting on James. Maria's lower bid no longer counts.
|
|
||||||
cy.phone('alice').find(tid('ride-status')).should('have.text', 'Finding your driver');
|
|
||||||
cy.bid('james', 10, { rider: 'Alice' });
|
|
||||||
|
|
||||||
cy.phone('alice').find(tid('driver-name')).should('contain.text', 'James');
|
|
||||||
cy.phone('alice').find(tid('ride-fare')).should('have.text', '$10.00');
|
|
||||||
cy.waitForDispatch((s) => s.rides.every((r) => r.status === 'matched'), 'both rides matched').then(({ rides }) => {
|
|
||||||
expect(rides.map((r) => [r.riderId, r.winnerId])).to.deep.equal([
|
|
||||||
['alice', 'james'],
|
|
||||||
['bob', 'maria'],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
3
cypress/e2e/bid-capped-at-quote.cy.ts
Normal file
3
cypress/e2e/bid-capped-at-quote.cy.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import scenario from '../scenarios/bid-capped-at-quote';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
3
cypress/e2e/every-driver-passes.cy.ts
Normal file
3
cypress/e2e/every-driver-passes.cy.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import scenario from '../scenarios/every-driver-passes';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
3
cypress/e2e/lowest-bid-wins.cy.ts
Normal file
3
cypress/e2e/lowest-bid-wins.cy.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import scenario from '../scenarios/lowest-bid-wins';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
3
cypress/e2e/one-ride-per-driver.cy.ts
Normal file
3
cypress/e2e/one-ride-per-driver.cy.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import scenario from '../scenarios/one-ride-per-driver';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
3
cypress/e2e/rider-cancels.cy.ts
Normal file
3
cypress/e2e/rider-cancels.cy.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import scenario from '../scenarios/rider-cancels';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
3
cypress/e2e/tie-goes-to-earlier-bid.cy.ts
Normal file
3
cypress/e2e/tie-goes-to-earlier-bid.cy.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import scenario from '../scenarios/tie-goes-to-earlier-bid';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
17
cypress/scenarios/auction-deadline.ts
Normal file
17
cypress/scenarios/auction-deadline.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, maria, james } from '../support/phones';
|
||||||
|
|
||||||
|
export default defineScenario('closes on the deadline and awards whoever did bid', () => {
|
||||||
|
cy.resetDispatch({ auctionDurationMs: 4000 });
|
||||||
|
cy.openPhones([alice, maria, james]);
|
||||||
|
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||||
|
|
||||||
|
cy.bid('maria', 10.75);
|
||||||
|
// James never answers; the auction stays open until the window runs out.
|
||||||
|
cy.phone('alice').find(tid('ride-status')).should('have.text', 'Finding your driver');
|
||||||
|
|
||||||
|
cy.phone('alice').find(tid('ride-status'), { timeout: 8000 }).should('have.text', 'Driver found');
|
||||||
|
cy.phone('alice').find(tid('ride-fare')).should('have.text', '$10.75');
|
||||||
|
cy.phone('james').find(tid('driver-result')).should('have.text', 'That request closed before you answered');
|
||||||
|
cy.phone('james').find(tid('offer')).should('not.exist');
|
||||||
|
});
|
||||||
14
cypress/scenarios/bid-capped-at-quote.ts
Normal file
14
cypress/scenarios/bid-capped-at-quote.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, maria } from '../support/phones';
|
||||||
|
|
||||||
|
export default defineScenario('does not let a driver bid above the quoted fare', () => {
|
||||||
|
cy.resetDispatch();
|
||||||
|
cy.openPhones([alice, maria]);
|
||||||
|
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||||
|
|
||||||
|
cy.phone('maria').find(tid('bid-input')).clear().type('11.01');
|
||||||
|
cy.phone('maria').find(tid('submit-bid')).should('have.attr', 'aria-disabled', 'true');
|
||||||
|
|
||||||
|
cy.phone('maria').find(tid('bid-input')).clear().type('11');
|
||||||
|
cy.phone('maria').find(tid('submit-bid')).should('not.have.attr', 'aria-disabled');
|
||||||
|
});
|
||||||
20
cypress/scenarios/every-driver-passes.ts
Normal file
20
cypress/scenarios/every-driver-passes.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, maria, james } from '../support/phones';
|
||||||
|
|
||||||
|
export default defineScenario('tells the rider when every driver passes', () => {
|
||||||
|
cy.resetDispatch();
|
||||||
|
cy.openPhones([alice, maria, james]);
|
||||||
|
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||||
|
|
||||||
|
cy.pass('maria');
|
||||||
|
cy.pass('james');
|
||||||
|
|
||||||
|
cy.phone('alice').find(tid('ride-status')).should('have.text', 'No drivers available');
|
||||||
|
cy.waitForDispatch((s) => s.rides[0]?.status === 'unmatched', 'ride unmatched');
|
||||||
|
|
||||||
|
// Going back lets the rider try again with a fresh auction.
|
||||||
|
cy.phone('alice').find(tid('unmatched-back')).click();
|
||||||
|
cy.phone('alice').find(tid('confirm-ride')).click();
|
||||||
|
cy.phone('maria').find(tid('offer')).should('have.length', 1);
|
||||||
|
cy.waitForDispatch((s) => s.rides.length === 2 && s.rides[1].status === 'auction', 'second auction open');
|
||||||
|
});
|
||||||
50
cypress/scenarios/lowest-bid-wins.ts
Normal file
50
cypress/scenarios/lowest-bid-wins.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, maria, james, aisha } from '../support/phones';
|
||||||
|
|
||||||
|
export default defineScenario('awards the ride to the lowest bid and runs the trip end to end', () => {
|
||||||
|
cy.resetDispatch();
|
||||||
|
cy.openPhones([alice, maria, james, aisha]);
|
||||||
|
|
||||||
|
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||||
|
|
||||||
|
// Every driver is invited, capped at the quoted fare.
|
||||||
|
for (const driver of ['maria', 'james', 'aisha']) {
|
||||||
|
cy.phone(driver).find(tid('offer-destination')).should('have.text', 'Alice → Ferry Building');
|
||||||
|
cy.phone(driver).find(tid('offer-max-fare')).should('have.text', 'Rider pays up to $11.00');
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.bid('maria', 10);
|
||||||
|
cy.phone('maria').find(tid('offer-my-bid')).should('contain.text', 'You bid $10.00');
|
||||||
|
cy.phone('alice').find(tid('offer-count')).should('have.text', '1 offer so far');
|
||||||
|
|
||||||
|
cy.bid('james', 8.5);
|
||||||
|
cy.phone('alice').find(tid('offer-count')).should('have.text', '2 offers so far');
|
||||||
|
|
||||||
|
// The last driver answering closes the auction without waiting out the clock.
|
||||||
|
cy.pass('aisha');
|
||||||
|
|
||||||
|
cy.phone('alice').find(tid('ride-status')).should('have.text', 'Driver found');
|
||||||
|
cy.phone('alice').find(tid('ride-fare')).should('have.text', '$8.50');
|
||||||
|
cy.phone('alice').find(tid('driver-name')).should('contain.text', 'James');
|
||||||
|
cy.phone('alice').find(tid('matched-driver')).should('contain.text', '8TRW115');
|
||||||
|
|
||||||
|
cy.phone('james').find(tid('job-fare')).should('have.text', 'Fare $8.50');
|
||||||
|
cy.phone('maria').find(tid('driver-result')).should('have.text', 'Another driver won that ride');
|
||||||
|
cy.phone('aisha').find(tid('driver-idle')).should('exist');
|
||||||
|
cy.phone('aisha').find(tid('driver-result')).should('not.exist');
|
||||||
|
|
||||||
|
cy.dispatchState().then(({ rides }) => {
|
||||||
|
expect(rides).to.have.length(1);
|
||||||
|
expect(rides[0]).to.include({ status: 'matched', winnerId: 'james', fare: 8.5, maxFare: 11 });
|
||||||
|
expect(rides[0].declined).to.deep.equal(['aisha']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The winning driver finishes the trip; the rider's phone follows.
|
||||||
|
cy.phone('james').find(tid('complete-trip')).click();
|
||||||
|
cy.phone('james').find(tid('driver-result')).should('have.text', 'Trip complete · you earned $8.50');
|
||||||
|
cy.phone('alice').find(tid('ride-status')).should('have.text', 'You’ve arrived');
|
||||||
|
cy.phone('alice').should('contain.text', 'Thanks for riding with James.');
|
||||||
|
|
||||||
|
cy.phone('alice').find(tid('trip-done')).click();
|
||||||
|
cy.phone('alice').find(tid('where-to')).should('exist');
|
||||||
|
});
|
||||||
33
cypress/scenarios/one-ride-per-driver.ts
Normal file
33
cypress/scenarios/one-ride-per-driver.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, bob, maria, james } from '../support/phones';
|
||||||
|
|
||||||
|
export default defineScenario('never gives one driver two rides at once', () => {
|
||||||
|
cy.resetDispatch();
|
||||||
|
cy.openPhones([alice, bob, maria, james]);
|
||||||
|
|
||||||
|
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||||
|
cy.requestRide('bob', 'ferry', 'Ferry Building');
|
||||||
|
cy.phone('maria').find(tid('offer')).should('have.length', 2);
|
||||||
|
|
||||||
|
// Maria is the cheapest on both rides.
|
||||||
|
cy.bid('maria', 7, { rider: 'Alice' });
|
||||||
|
cy.bid('maria', 6, { rider: 'Bob' });
|
||||||
|
cy.bid('james', 9, { rider: 'Bob' });
|
||||||
|
|
||||||
|
// Bob's auction has every answer, so it closes first and Maria wins it.
|
||||||
|
cy.phone('bob').find(tid('driver-name')).should('contain.text', 'Maria');
|
||||||
|
cy.phone('bob').find(tid('ride-fare')).should('have.text', '$6.00');
|
||||||
|
|
||||||
|
// Alice's auction is still waiting on James. Maria's lower bid no longer counts.
|
||||||
|
cy.phone('alice').find(tid('ride-status')).should('have.text', 'Finding your driver');
|
||||||
|
cy.bid('james', 10, { rider: 'Alice' });
|
||||||
|
|
||||||
|
cy.phone('alice').find(tid('driver-name')).should('contain.text', 'James');
|
||||||
|
cy.phone('alice').find(tid('ride-fare')).should('have.text', '$10.00');
|
||||||
|
cy.waitForDispatch((s) => s.rides.every((r) => r.status === 'matched'), 'both rides matched').then(({ rides }) => {
|
||||||
|
expect(rides.map((r) => [r.riderId, r.winnerId])).to.deep.equal([
|
||||||
|
['alice', 'james'],
|
||||||
|
['bob', 'maria'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
18
cypress/scenarios/rider-cancels.ts
Normal file
18
cypress/scenarios/rider-cancels.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, maria, james } from '../support/phones';
|
||||||
|
|
||||||
|
export default defineScenario('withdraws the offer from every driver when the rider cancels', () => {
|
||||||
|
cy.resetDispatch();
|
||||||
|
cy.openPhones([alice, maria, james]);
|
||||||
|
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||||
|
|
||||||
|
cy.bid('maria', 9);
|
||||||
|
cy.phone('alice').find(tid('cancel-request')).click();
|
||||||
|
cy.phone('alice').find(tid('confirm-ride')).should('exist');
|
||||||
|
|
||||||
|
for (const driver of ['maria', 'james']) {
|
||||||
|
cy.phone(driver).find(tid('offer')).should('not.exist');
|
||||||
|
cy.phone(driver).find(tid('driver-result')).should('have.text', 'The rider cancelled their request');
|
||||||
|
}
|
||||||
|
cy.waitForDispatch((s) => s.rides[0]?.status === 'cancelled', 'ride cancelled');
|
||||||
|
});
|
||||||
15
cypress/scenarios/tie-goes-to-earlier-bid.ts
Normal file
15
cypress/scenarios/tie-goes-to-earlier-bid.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, maria, james } from '../support/phones';
|
||||||
|
|
||||||
|
export default defineScenario('breaks a tie in favour of the earlier bid', () => {
|
||||||
|
cy.resetDispatch();
|
||||||
|
cy.openPhones([alice, maria, james]);
|
||||||
|
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||||
|
|
||||||
|
cy.bid('james', 9);
|
||||||
|
cy.phone('james').find(tid('offer-my-bid')).should('exist');
|
||||||
|
cy.bid('maria', 9);
|
||||||
|
|
||||||
|
cy.phone('alice').find(tid('driver-name')).should('contain.text', 'James');
|
||||||
|
cy.phone('maria').find(tid('driver-result')).should('have.text', 'Another driver won that ride');
|
||||||
|
});
|
||||||
|
|
@ -14,6 +14,14 @@ export type PhoneSpec = {
|
||||||
rating?: number;
|
rating?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One multi-phone scenario. Scenarios live in cypress/scenarios/ and are registered by
|
||||||
|
* thin spec files in cypress/e2e/, so the same steps can back more than one spec.
|
||||||
|
*/
|
||||||
|
export type Scenario = { title: string; run: () => void };
|
||||||
|
|
||||||
|
export const defineScenario = (title: string, run: () => void): Scenario => ({ title, run });
|
||||||
|
|
||||||
export type DispatchState = {
|
export type DispatchState = {
|
||||||
config: { auctionDurationMs: number };
|
config: { auctionDurationMs: number };
|
||||||
devices: (PhoneSpec & { online: boolean; busy: boolean })[];
|
devices: (PhoneSpec & { online: boolean; busy: boolean })[];
|
||||||
|
|
|
||||||
8
cypress/support/phones.ts
Normal file
8
cypress/support/phones.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { PhoneSpec } from './e2e';
|
||||||
|
|
||||||
|
// The cast shared by the auction scenarios.
|
||||||
|
export const alice: PhoneSpec = { role: 'rider', id: 'alice', name: 'Alice' };
|
||||||
|
export const bob: PhoneSpec = { role: 'rider', id: 'bob', name: 'Bob' };
|
||||||
|
export const maria: PhoneSpec = { role: 'driver', id: 'maria', name: 'Maria', car: 'Toyota Camry · Silver', plate: '7KDL294', rating: 4.95 };
|
||||||
|
export const james: PhoneSpec = { role: 'driver', id: 'james', name: 'James', car: 'Honda Accord · Black', plate: '8TRW115', rating: 4.88 };
|
||||||
|
export const aisha: PhoneSpec = { role: 'driver', id: 'aisha', name: 'Aisha', car: 'Tesla Model 3 · White', plate: '9EVA662', rating: 4.97 };
|
||||||
|
|
@ -22,6 +22,8 @@ npm run e2e # start dispatch + web server, run every scenario headlessly,
|
||||||
npm run e2e:open # same, but in the Cypress UI so you can watch the phones
|
npm run e2e:open # same, but in the Cypress UI so you can watch the phones
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In the Cypress UI, each scenario is its own spec; pick one to watch it, or use **Run all specs** to play them back to back. To run one scenario headlessly, start the servers (below) and run `npx cypress run --spec cypress/e2e/lowest-bid-wins.cy.ts`.
|
||||||
|
|
||||||
To explore by hand, start the two servers yourself:
|
To explore by hand, start the two servers yourself:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -71,8 +73,22 @@ What each driver sees afterwards:
|
||||||
| `src/app/driver.tsx` | Driver screen: offers, bid input, current job, result banner. |
|
| `src/app/driver.tsx` | Driver screen: offers, bid input, current job, result banner. |
|
||||||
| `src/components/RideMap.tsx` / `.web.tsx` | Native map, and a plain backdrop on web (`react-native-maps` has no web build). |
|
| `src/components/RideMap.tsx` / `.web.tsx` | Native map, and a plain backdrop on web (`react-native-maps` has no web build). |
|
||||||
| `public/sim.html` | The simulator page: phones as iframes plus the live dispatch panel. |
|
| `public/sim.html` | The simulator page: phones as iframes plus the live dispatch panel. |
|
||||||
| `cypress/support/e2e.ts` | Custom commands and network fixtures. |
|
| `cypress/support/e2e.ts` | Custom commands, network fixtures, and `defineScenario`. |
|
||||||
| `cypress/e2e/auction.cy.ts` | Auction scenarios. |
|
| `cypress/support/phones.ts` | The shared cast: riders Alice and Bob; drivers Maria, James and Aisha. |
|
||||||
|
| `cypress/scenarios/<name>.ts` | One scenario's steps and assertions. |
|
||||||
|
| `cypress/e2e/<name>.cy.ts` | A thin spec that registers one scenario. |
|
||||||
|
|
||||||
|
### Scenarios
|
||||||
|
|
||||||
|
| Spec | Checks |
|
||||||
|
| --------------------------- | ------------------------------------------------------------------------------ |
|
||||||
|
| `lowest-bid-wins` | Lowest bid wins, the losers are told, and the trip runs to "You've arrived". |
|
||||||
|
| `tie-goes-to-earlier-bid` | Equal bids go to whoever bid first. |
|
||||||
|
| `bid-capped-at-quote` | A bid above the rider's quoted fare can't be submitted. |
|
||||||
|
| `auction-deadline` | The auction closes on its deadline and a driver who never answered is told. |
|
||||||
|
| `every-driver-passes` | The rider sees "No drivers available" and can request again. |
|
||||||
|
| `rider-cancels` | Cancelling withdraws the offer from every driver. |
|
||||||
|
| `one-ride-per-driver` | A driver who wins one ride isn't awarded another at the same time. |
|
||||||
|
|
||||||
### Protocol
|
### Protocol
|
||||||
|
|
||||||
|
|
@ -111,14 +127,14 @@ Environment variables: `PORT` (default 8787) and `AUCTION_MS` (default auction w
|
||||||
|
|
||||||
## Writing scenarios
|
## Writing scenarios
|
||||||
|
|
||||||
|
A scenario's steps live in `cypress/scenarios/`, apart from the spec file that runs them, so the same steps can back more than one spec. Add both files:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { tid, type PhoneSpec } from '../support/e2e';
|
// cypress/scenarios/lowest-bid-wins.ts
|
||||||
|
import { defineScenario, tid } from '../support/e2e';
|
||||||
|
import { alice, james, maria } from '../support/phones';
|
||||||
|
|
||||||
const alice: PhoneSpec = { role: 'rider', id: 'alice', name: 'Alice' };
|
export default defineScenario('lowest bid wins', () => {
|
||||||
const maria: PhoneSpec = { role: 'driver', id: 'maria', name: 'Maria', plate: '7KDL294' };
|
|
||||||
const james: PhoneSpec = { role: 'driver', id: 'james', name: 'James' };
|
|
||||||
|
|
||||||
it('lowest bid wins', () => {
|
|
||||||
cy.resetDispatch(); // always first: dispatch state outlives page loads
|
cy.resetDispatch(); // always first: dispatch state outlives page loads
|
||||||
cy.openPhones([alice, maria, james]); // waits until every phone is rendered and connected
|
cy.openPhones([alice, maria, james]); // waits until every phone is rendered and connected
|
||||||
|
|
||||||
|
|
@ -132,6 +148,15 @@ it('lowest bid wins', () => {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// cypress/e2e/lowest-bid-wins.cy.ts
|
||||||
|
import scenario from '../scenarios/lowest-bid-wins';
|
||||||
|
|
||||||
|
it(scenario.title, scenario.run);
|
||||||
|
```
|
||||||
|
|
||||||
|
Each scenario starts with `cy.resetDispatch()` and opens its own phones, so any spec can run alone or in any order.
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
| Command | Does |
|
| Command | Does |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue