Add slow versions of every auction scenario
Each scenario gets a spec in cypress/e2e/slow/ that replays the same steps for watching: every phone step pauses and outlines the acting phone, typing is visible, time windows stretch 4x, and the final screen holds for 5 s. npm run e2e:slow runs them in a visible Chrome window; npm run e2e still runs only the normal-speed specs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3a9944489f
commit
8d8b6727c7
21 changed files with 138 additions and 19 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import scenario from '../scenarios/auction-deadline';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import scenario from '../scenarios/bid-capped-at-quote';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import scenario from '../scenarios/every-driver-passes';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import scenario from '../scenarios/lowest-bid-wins';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import scenario from '../scenarios/one-ride-per-driver';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import scenario from '../scenarios/rider-cancels';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
|
|
|
|||
4
cypress/e2e/slow/auction-deadline.cy.ts
Normal file
4
cypress/e2e/slow/auction-deadline.cy.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import scenario from '../../scenarios/auction-deadline';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
4
cypress/e2e/slow/bid-capped-at-quote.cy.ts
Normal file
4
cypress/e2e/slow/bid-capped-at-quote.cy.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import scenario from '../../scenarios/bid-capped-at-quote';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
4
cypress/e2e/slow/every-driver-passes.cy.ts
Normal file
4
cypress/e2e/slow/every-driver-passes.cy.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import scenario from '../../scenarios/every-driver-passes';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
4
cypress/e2e/slow/lowest-bid-wins.cy.ts
Normal file
4
cypress/e2e/slow/lowest-bid-wins.cy.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import scenario from '../../scenarios/lowest-bid-wins';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
4
cypress/e2e/slow/one-ride-per-driver.cy.ts
Normal file
4
cypress/e2e/slow/one-ride-per-driver.cy.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import scenario from '../../scenarios/one-ride-per-driver';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
4
cypress/e2e/slow/rider-cancels.cy.ts
Normal file
4
cypress/e2e/slow/rider-cancels.cy.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import scenario from '../../scenarios/rider-cancels';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
4
cypress/e2e/slow/tie-goes-to-earlier-bid.cy.ts
Normal file
4
cypress/e2e/slow/tie-goes-to-earlier-bid.cy.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import scenario from '../../scenarios/tie-goes-to-earlier-bid';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import scenario from '../scenarios/tie-goes-to-earlier-bid';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { defineScenario, tid } from '../support/e2e';
|
||||
import { scaled } from '../support/pace';
|
||||
import { alice, maria, james } from '../support/phones';
|
||||
|
||||
export default defineScenario('closes on the deadline and awards whoever did bid', () => {
|
||||
cy.resetDispatch({ auctionDurationMs: 4000 });
|
||||
cy.resetDispatch({ auctionDurationMs: scaled(4000) });
|
||||
cy.openPhones([alice, maria, james]);
|
||||
cy.requestRide('alice', 'ferry', 'Ferry Building');
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ export default defineScenario('closes on the deadline and awards whoever did bid
|
|||
// 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-status'), { timeout: scaled(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');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { defineScenario, tid } from '../support/e2e';
|
||||
import { typeDelayMs } from '../support/pace';
|
||||
import { alice, maria } from '../support/phones';
|
||||
|
||||
export default defineScenario('does not let a driver bid above the quoted fare', () => {
|
||||
|
|
@ -6,9 +7,9 @@ export default defineScenario('does not let a driver bid above the quoted fare',
|
|||
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('bid-input')).clear().type('11.01', { delay: typeDelayMs() });
|
||||
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('bid-input')).clear().type('11', { delay: typeDelayMs() });
|
||||
cy.phone('maria').find(tid('submit-bid')).should('not.have.attr', 'aria-disabled');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,13 +63,19 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
import { isSlow, scaled, stepDelayMs, typeDelayMs } from './pace';
|
||||
|
||||
/** Selector for a React Native `testID`, which react-native-web renders as data-testid. */
|
||||
export const tid = (id: string) => `[data-testid="${id}"]`;
|
||||
|
||||
const dispatchUrl = () => Cypress.expose('dispatchUrl') as string;
|
||||
|
||||
// The dispatch server's default auction window (AUCTION_MS); slow runs stretch it.
|
||||
const DEFAULT_AUCTION_MS = 15_000;
|
||||
|
||||
Cypress.Commands.add('resetDispatch', (options = {}) => {
|
||||
return cy.request('POST', `${dispatchUrl()}/test/reset`, options).its('body');
|
||||
const auctionDurationMs = options.auctionDurationMs ?? (isSlow() ? scaled(DEFAULT_AUCTION_MS) : undefined);
|
||||
return cy.request('POST', `${dispatchUrl()}/test/reset`, { auctionDurationMs }).its('body');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('dispatchState', () => {
|
||||
|
|
@ -113,6 +119,13 @@ Cypress.Commands.add('openPhones', (phones) => {
|
|||
|
||||
Cypress.Commands.add('phone', (id) => {
|
||||
Cypress.log({ name: 'phone', message: id });
|
||||
if (stepDelayMs() > 0) {
|
||||
// Show which phone is about to act, then give the viewer a moment to see it.
|
||||
cy.get('iframe[data-phone]', { log: false }).then((frames) => {
|
||||
frames.removeClass('acting').filter(`[data-phone="${id}"]`).addClass('acting');
|
||||
});
|
||||
cy.wait(stepDelayMs(), { log: false });
|
||||
}
|
||||
return cy
|
||||
.get(`iframe[data-phone="${id}"]`, { log: false })
|
||||
.its('0.contentDocument.body', { log: false })
|
||||
|
|
@ -121,7 +134,7 @@ Cypress.Commands.add('phone', (id) => {
|
|||
|
||||
Cypress.Commands.add('requestRide', (riderId, query, placeTitle) => {
|
||||
cy.phone(riderId).find(tid('where-to')).click();
|
||||
cy.phone(riderId).find(tid('destination-input')).type(query);
|
||||
cy.phone(riderId).find(tid('destination-input')).type(query, { delay: typeDelayMs() });
|
||||
cy.phone(riderId).contains(placeTitle).click();
|
||||
cy.phone(riderId).find(tid('confirm-ride')).click();
|
||||
cy.phone(riderId).find(tid('ride-status')).should('have.text', 'Finding your driver');
|
||||
|
|
@ -133,7 +146,7 @@ function offerOn(driverId: string, rider?: string) {
|
|||
}
|
||||
|
||||
Cypress.Commands.add('bid', (driverId, amount, options = {}) => {
|
||||
offerOn(driverId, options.rider).find(tid('bid-input')).clear().type(amount.toFixed(2));
|
||||
offerOn(driverId, options.rider).find(tid('bid-input')).clear().type(amount.toFixed(2), { delay: typeDelayMs() });
|
||||
offerOn(driverId, options.rider).find(tid('submit-bid')).click();
|
||||
});
|
||||
|
||||
|
|
|
|||
35
cypress/support/pace.ts
Normal file
35
cypress/support/pace.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// How fast a scenario plays. Slow runs are for watching: every step on a phone pauses
|
||||
// first and highlights that phone, typing is visible, and time windows stretch so the
|
||||
// scenario still holds at that speed.
|
||||
|
||||
import type { Scenario } from './e2e';
|
||||
|
||||
export type Pace = 'normal' | 'slow';
|
||||
|
||||
const PACES = {
|
||||
normal: { stepDelayMs: 0, typeDelayMs: 10, timeScale: 1, endPauseMs: 0 },
|
||||
// endPauseMs holds the final screen before the next spec loads or the window closes.
|
||||
slow: { stepDelayMs: 800, typeDelayMs: 150, timeScale: 4, endPauseMs: 5000 },
|
||||
} satisfies Record<Pace, { stepDelayMs: number; typeDelayMs: number; timeScale: number; endPauseMs: number }>;
|
||||
|
||||
let current: Pace = 'normal';
|
||||
|
||||
export const isSlow = () => current === 'slow';
|
||||
export const stepDelayMs = () => PACES[current].stepDelayMs;
|
||||
export const typeDelayMs = () => PACES[current].typeDelayMs;
|
||||
|
||||
/** Stretch a duration (auction window, assertion timeout) to fit the current pace. */
|
||||
export const scaled = (ms: number) => ms * PACES[current].timeScale;
|
||||
|
||||
/**
|
||||
* Register a scenario as a test at the given pace. The pace is set when the test runs,
|
||||
* not when it's registered, because "Run all specs" loads every spec into one page.
|
||||
*/
|
||||
export function runScenario(scenario: Scenario, pace: Pace = 'normal') {
|
||||
it(pace === 'slow' ? `${scenario.title} (slow)` : scenario.title, () => {
|
||||
current = pace;
|
||||
scenario.run();
|
||||
const { endPauseMs } = PACES[pace];
|
||||
if (endPauseMs > 0) cy.wait(endPauseMs, { log: false });
|
||||
});
|
||||
}
|
||||
|
|
@ -20,9 +20,10 @@ This setup runs several "phones" at once in one browser, without iOS simulators,
|
|||
```bash
|
||||
npm run e2e # start dispatch + web server, run every scenario headlessly, shut down
|
||||
npm run e2e:open # same, but in the Cypress UI so you can watch the phones
|
||||
npm run e2e:slow # every scenario at watching speed, in a visible Chrome window
|
||||
```
|
||||
|
||||
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`.
|
||||
In the Cypress UI, each scenario is its own spec, and `slow/` holds a slow version of each. Pick one to watch it, or use **Run all specs** to play them back to back. `npm run e2e` runs only the normal-speed specs. 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:
|
||||
|
||||
|
|
@ -76,7 +77,9 @@ What each driver sees afterwards:
|
|||
| `cypress/support/e2e.ts` | Custom commands, network fixtures, and `defineScenario`. |
|
||||
| `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. |
|
||||
| `cypress/e2e/<name>.cy.ts` | A thin spec that registers one scenario at normal speed. |
|
||||
| `cypress/e2e/slow/<name>.cy.ts` | The same scenario at watching speed. |
|
||||
| `cypress/support/pace.ts` | `runScenario`, pace settings, and `scaled()` for time windows. |
|
||||
|
||||
### Scenarios
|
||||
|
||||
|
|
@ -151,8 +154,17 @@ export default defineScenario('lowest bid wins', () => {
|
|||
```ts
|
||||
// cypress/e2e/lowest-bid-wins.cy.ts
|
||||
import scenario from '../scenarios/lowest-bid-wins';
|
||||
import { runScenario } from '../support/pace';
|
||||
|
||||
it(scenario.title, scenario.run);
|
||||
runScenario(scenario);
|
||||
```
|
||||
|
||||
```ts
|
||||
// cypress/e2e/slow/lowest-bid-wins.cy.ts
|
||||
import scenario from '../../scenarios/lowest-bid-wins';
|
||||
import { runScenario } from '../../support/pace';
|
||||
|
||||
runScenario(scenario, 'slow');
|
||||
```
|
||||
|
||||
Each scenario starts with `cy.resetDispatch()` and opens its own phones, so any spec can run alone or in any order.
|
||||
|
|
@ -191,6 +203,24 @@ Place search (Photon) and routing (OSRM) are external services, so every test st
|
|||
|
||||
So the quoted fares are: UberX **$11.00**, Comfort $14.00, UberXL $17.50, Black $26.00. To test other fares, add a fixture and override the intercept inside the test.
|
||||
|
||||
### Slow runs
|
||||
|
||||
A slow spec plays the same steps for a human to follow:
|
||||
|
||||
- Every `cy.phone(...)` step pauses first (800 ms) and outlines that phone in blue in the simulator.
|
||||
- Typing is visible (150 ms per key).
|
||||
- Each scenario holds its final screen for 5 s before the next spec loads or the window closes.
|
||||
- Time windows are stretched 4×. `cy.resetDispatch()` without an explicit window gets 60 s instead of 15 s.
|
||||
|
||||
Tune these in `PACES` in `cypress/support/pace.ts`. If a scenario depends on a time window, wrap the numbers in `scaled()` so it still holds when slow:
|
||||
|
||||
```ts
|
||||
cy.resetDispatch({ auctionDurationMs: scaled(4000) });
|
||||
cy.phone('alice').find(tid('ride-status'), { timeout: scaled(8000) }).should('have.text', 'Driver found');
|
||||
```
|
||||
|
||||
Use `typeDelayMs()` for any `.type()` a scenario does directly. The pace is set when each test starts, so normal and slow specs can share one "Run all specs" session.
|
||||
|
||||
### Timing tips
|
||||
|
||||
- Prefer letting every invited driver answer; the auction then closes at once, with no waiting.
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@
|
|||
"lint": "expo lint",
|
||||
"dispatch": "node server/dispatch.mts",
|
||||
"sim:web": "expo start --port 8090",
|
||||
"e2e": "start-server-and-test dispatch http-get://localhost:8787/state sim:web http://localhost:8090 'cypress run'",
|
||||
"e2e:open": "start-server-and-test dispatch http-get://localhost:8787/state sim:web http://localhost:8090 'cypress open --e2e'"
|
||||
"e2e": "start-server-and-test dispatch http-get://localhost:8787/state sim:web http://localhost:8090 'cypress run --spec \"cypress/e2e/*.cy.ts\"'",
|
||||
"e2e:open": "start-server-and-test dispatch http-get://localhost:8787/state sim:web http://localhost:8090 'cypress open --e2e'",
|
||||
"e2e:slow": "start-server-and-test dispatch http-get://localhost:8787/state sim:web http://localhost:8090 'cypress run --headed --browser chrome --spec \"cypress/e2e/slow/*.cy.ts\"'"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@
|
|||
border: 10px solid #111;
|
||||
border-radius: 36px;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
/* Slow test runs mark the phone that's about to act. */
|
||||
.phone iframe.acting { border-color: #2563eb; }
|
||||
aside {
|
||||
flex: none;
|
||||
width: 320px;
|
||||
|
|
|
|||
Loading…
Reference in a new issue