Cases / #60 · 2026-10-18 · Easy
A multi-step checkout end to end: wait for each step, and check what you typed
Log in, fill a cart, refuse an empty form, type the buyer, check the totals and finish — as three demo users: one normal, one whose pages are slow, one whose inputs are broken. Waiting for each page instead of sleeping handles the slow one; reading the fields back catches the broken one.
Run on production on 2026-10-18: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Sign-ups, checkouts and quote forms span several pages. A script that clicks through them with fixed sleeps breaks the first time a page is slow, and one that only checks the last page never notices that a field didn't take what it typed. Here the same checkout runs on Sauce Demo, a public practice shop with demo users built for exactly these problems: standard_user, performance_glitch_user (slow pages) and problem_user (broken inputs). The shop runs entirely in the browser, so there is no server-side order to read back — the checks are on its own pages.
What we used, and why
| What | Why |
|---|---|
page.waitForURL('**/checkout-step-one.html') | Each step waits for the page it leads to, however long that takes — never a fixed sleep. |
locator('[data-test=shopping-cart-badge]').filter({ hasText: '2' }).waitFor() | Waits until the cart really holds both items before going to it. |
locator('[data-test=subtotal-label], [data-test=error]').first().waitFor() | After submitting a step, waits for whichever comes first: the next step or an error message. |
page.inputValue() | Reads every field back after typing, before submitting. |
[data-test=…] selectors | The shop marks its elements for tests, so the script doesn't depend on layout or wording. |
| A fresh context per user | Each user starts logged out with an empty cart. |
How it works
- Log in and wait for the product list.
- Add two products, wait for the cart badge to show 2, open the cart and start the checkout.
- Step one: submit the empty form and read the error; type first name, last name and postal code; read the three fields back; submit.
- Step two: compare the subtotal with the sum of the item prices and the total with subtotal + tax; finish and read the confirmation.
- The same for each of the three users, timing every step.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000PROXY_URL_US— a proxy exit in the United States
// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL_US
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
const SITE = 'https://www.saucedemo.com'; // a public demo shop for testing practice
const PASSWORD = 'secret_sauce'; // printed on the shop's login page
// The shop's demo users: a normal one, one whose pages are slow, and one with broken inputs.
const USERS = ['standard_user', 'performance_glitch_user', 'problem_user'];
const BUYER = { firstName: 'Ada', lastName: 'Lovelace', postalCode: '10115' };
const ITEMS = ['sauce-labs-backpack', 'sauce-labs-bike-light'];
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({ proxy: process.env.PROXY_URL_US, headless: true }),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
const t = (sel) => `[data-test="${sel}"]`;
const money = (s) => Number(String(s).replace(/[^0-9.]/g, ''));
// The checkout as one user, in a fresh context: every step waits for the page it leads to
// (waitForURL), never for a fixed time.
async function checkout(user) {
const context = await browser.newContext();
const page = await context.newPage();
const steps = {};
let mark = Date.now();
const step = (name) => { steps[name] = Date.now() - mark; mark = Date.now(); };
const out = { user, empty_form_error: null, typed_back: null, step_one_error: null, item_sum: null, subtotal: null, tax: null, total: null, totals_add_up: null, completed: null };
try {
await page.goto(SITE);
await page.fill(t('username'), user);
await page.fill(t('password'), PASSWORD);
await page.click(t('login-button'));
await page.waitForURL('**/inventory.html');
step('login');
for (const item of ITEMS) await page.click(t(`add-to-cart-${item}`));
await page.locator(t('shopping-cart-badge')).filter({ hasText: String(ITEMS.length) }).waitFor();
await page.click(t('shopping-cart-link'));
await page.waitForURL('**/cart.html');
await page.click(t('checkout'));
await page.waitForURL('**/checkout-step-one.html');
step('cart');
// Step one: an empty form is refused; then type the buyer and read the fields back.
await page.click(t('continue'));
out.empty_form_error = await page.locator(t('error')).innerText();
await page.fill(t('firstName'), BUYER.firstName);
await page.fill(t('lastName'), BUYER.lastName);
await page.fill(t('postalCode'), BUYER.postalCode);
out.typed_back = { firstName: await page.inputValue(t('firstName')), lastName: await page.inputValue(t('lastName')), postalCode: await page.inputValue(t('postalCode')) };
await page.click(t('continue'));
// Either step two loads or the form shows an error: wait for whichever comes first.
await page.locator(`${t('subtotal-label')}, ${t('error')}`).first().waitFor();
if (!page.url().includes('checkout-step-two')) {
out.step_one_error = await page.locator(t('error')).innerText();
step('step_one');
return { ...out, steps_ms: steps };
}
step('step_one');
// Step two: the overview's totals against the item prices.
const prices = await page.locator(t('inventory-item-price')).allInnerTexts();
out.item_sum = Math.round(prices.reduce((a, p) => a + money(p), 0) * 100) / 100;
out.subtotal = money(await page.locator(t('subtotal-label')).innerText());
out.tax = money(await page.locator(t('tax-label')).innerText());
out.total = money(await page.locator(t('total-label')).innerText());
out.totals_add_up = out.subtotal === out.item_sum && Math.abs(out.subtotal + out.tax - out.total) < 0.005;
await page.click(t('finish'));
await page.waitForURL('**/checkout-complete.html');
out.completed = await page.locator(t('complete-header')).innerText();
step('step_two_and_finish');
return { ...out, steps_ms: steps };
} finally {
await context.close();
}
}
try {
const runs = [];
for (const user of USERS) runs.push(await checkout(user));
console.log(JSON.stringify(runs, null, 2));
} finally {
await browser.close();
}
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL_US
import json
import os
import re
import time
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
SITE = "https://www.saucedemo.com" # a public demo shop for testing practice
PASSWORD = "secret_sauce" # printed on the shop's login page
# The shop's demo users: a normal one, one whose pages are slow, and one with broken inputs.
USERS = ["standard_user", "performance_glitch_user", "problem_user"]
BUYER = {"firstName": "Ada", "lastName": "Lovelace", "postalCode": "10115"}
ITEMS = ["sauce-labs-backpack", "sauce-labs-bike-light"]
def t(sel):
return f'[data-test="{sel}"]'
def money(s):
return float(re.sub(r"[^0-9.]", "", s))
res = requests.post("https://starter.cdpfleet.com/chromium/session", headers={"x-api-key": KEY},
json={"proxy": os.environ["PROXY_URL_US"], "headless": True}, timeout=60)
res.raise_for_status()
def checkout(browser, user):
"""The checkout as one user, in a fresh context: every step waits for the page it leads to
(wait_for_url), never for a fixed time."""
context = browser.new_context()
page = context.new_page()
steps = {}
mark = [time.time()]
def step(name):
steps[name] = round((time.time() - mark[0]) * 1000)
mark[0] = time.time()
out = {"user": user, "empty_form_error": None, "typed_back": None, "step_one_error": None, "item_sum": None,
"subtotal": None, "tax": None, "total": None, "totals_add_up": None, "completed": None}
try:
page.goto(SITE)
page.fill(t("username"), user)
page.fill(t("password"), PASSWORD)
page.click(t("login-button"))
page.wait_for_url("**/inventory.html")
step("login")
for item in ITEMS:
page.click(t(f"add-to-cart-{item}"))
page.locator(t("shopping-cart-badge")).filter(has_text=str(len(ITEMS))).wait_for()
page.click(t("shopping-cart-link"))
page.wait_for_url("**/cart.html")
page.click(t("checkout"))
page.wait_for_url("**/checkout-step-one.html")
step("cart")
# Step one: an empty form is refused; then type the buyer and read the fields back.
page.click(t("continue"))
out["empty_form_error"] = page.locator(t("error")).inner_text()
page.fill(t("firstName"), BUYER["firstName"])
page.fill(t("lastName"), BUYER["lastName"])
page.fill(t("postalCode"), BUYER["postalCode"])
out["typed_back"] = {k: page.input_value(t(k)) for k in ("firstName", "lastName", "postalCode")}
page.click(t("continue"))
# Either step two loads or the form shows an error: wait for whichever comes first.
page.locator(f'{t("subtotal-label")}, {t("error")}').first.wait_for()
if "checkout-step-two" not in page.url:
out["step_one_error"] = page.locator(t("error")).inner_text()
step("step_one")
return {**out, "steps_ms": steps}
step("step_one")
# Step two: the overview's totals against the item prices.
prices = page.locator(t("inventory-item-price")).all_inner_texts()
out["item_sum"] = round(sum(money(p) for p in prices), 2)
out["subtotal"] = money(page.locator(t("subtotal-label")).inner_text())
out["tax"] = money(page.locator(t("tax-label")).inner_text())
out["total"] = money(page.locator(t("total-label")).inner_text())
out["totals_add_up"] = out["subtotal"] == out["item_sum"] and abs(out["subtotal"] + out["tax"] - out["total"]) < 0.005
page.click(t("finish"))
page.wait_for_url("**/checkout-complete.html")
out["completed"] = page.locator(t("complete-header")).inner_text()
step("step_two_and_finish")
return {**out, "steps_ms": steps}
finally:
context.close()
with sync_playwright() as p:
browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
print(json.dumps([checkout(browser, u) for u in USERS], indent=2, ensure_ascii=False))
finally:
browser.close()
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL_US
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final String SITE = "https://www.saucedemo.com"; // a public demo shop for testing practice
static final String PASSWORD = "secret_sauce"; // printed on the shop's login page
// The shop's demo users: a normal one, one whose pages are slow, and one with broken inputs.
static final List<String> USERS = List.of("standard_user", "performance_glitch_user", "problem_user");
static final String[][] BUYER = {{"firstName", "Ada"}, {"lastName", "Lovelace"}, {"postalCode", "10115"}};
static final List<String> ITEMS = List.of("sauce-labs-backpack", "sauce-labs-bike-light");
static String t(String sel) { return "[data-test=\"" + sel + "\"]"; }
static double money(String s) { return Double.parseDouble(s.replaceAll("[^0-9.]", "")); }
// The checkout as one user, in a fresh context: every step waits for the page it leads to
// (waitForURL), never for a fixed time.
static JsonObject checkout(Browser browser, String user) {
BrowserContext context = browser.newContext();
Page page = context.newPage();
JsonObject steps = new JsonObject();
long[] mark = {System.currentTimeMillis()};
java.util.function.Consumer<String> step = name -> { steps.addProperty(name, System.currentTimeMillis() - mark[0]); mark[0] = System.currentTimeMillis(); };
JsonObject out = new JsonObject();
out.addProperty("user", user);
for (String k : List.of("empty_form_error", "typed_back", "step_one_error", "item_sum", "subtotal", "tax", "total", "totals_add_up", "completed")) out.add(k, JsonNull.INSTANCE);
try {
page.navigate(SITE);
page.fill(t("username"), user);
page.fill(t("password"), PASSWORD);
page.click(t("login-button"));
page.waitForURL("**/inventory.html");
step.accept("login");
for (String item : ITEMS) page.click(t("add-to-cart-" + item));
page.locator(t("shopping-cart-badge")).filter(new Locator.FilterOptions().setHasText(String.valueOf(ITEMS.size()))).waitFor();
page.click(t("shopping-cart-link"));
page.waitForURL("**/cart.html");
page.click(t("checkout"));
page.waitForURL("**/checkout-step-one.html");
step.accept("cart");
// Step one: an empty form is refused; then type the buyer and read the fields back.
page.click(t("continue"));
out.addProperty("empty_form_error", page.locator(t("error")).innerText());
for (String[] f : BUYER) page.fill(t(f[0]), f[1]);
JsonObject typed = new JsonObject();
for (String[] f : BUYER) typed.addProperty(f[0], page.inputValue(t(f[0])));
out.add("typed_back", typed);
page.click(t("continue"));
// Either step two loads or the form shows an error: wait for whichever comes first.
page.locator(t("subtotal-label") + ", " + t("error")).first().waitFor();
if (!page.url().contains("checkout-step-two")) {
out.addProperty("step_one_error", page.locator(t("error")).innerText());
step.accept("step_one");
out.add("steps_ms", steps);
return out;
}
step.accept("step_one");
// Step two: the overview's totals against the item prices.
double sum = 0;
for (String p : page.locator(t("inventory-item-price")).allInnerTexts()) sum += money(p);
double itemSum = Math.round(sum * 100) / 100.0;
double subtotal = money(page.locator(t("subtotal-label")).innerText());
double tax = money(page.locator(t("tax-label")).innerText());
double total = money(page.locator(t("total-label")).innerText());
out.addProperty("item_sum", itemSum);
out.addProperty("subtotal", subtotal);
out.addProperty("tax", tax);
out.addProperty("total", total);
out.addProperty("totals_add_up", subtotal == itemSum && Math.abs(subtotal + tax - total) < 0.005);
page.click(t("finish"));
page.waitForURL("**/checkout-complete.html");
out.addProperty("completed", page.locator(t("complete-header")).innerText());
step.accept("step_two_and_finish");
out.add("steps_ms", steps);
return out;
} finally {
context.close();
}
}
public static void main(String[] args) throws Exception {
String body = "{\"proxy\": " + new Gson().toJson(System.getenv("PROXY_URL_US")) + ", \"headless\": true}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
JsonArray runs = new JsonArray();
for (String user : USERS) runs.add(checkout(browser, user));
System.out.println(new GsonBuilder().setPrettyPrinting().serializeNulls().disableHtmlEscaping().create().toJson(runs));
} finally {
browser.close();
}
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL_US
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.Playwright;
const string Site = "https://www.saucedemo.com"; // a public demo shop for testing practice
const string Password = "secret_sauce"; // printed on the shop's login page
// The shop's demo users: a normal one, one whose pages are slow, and one with broken inputs.
string[] users = { "standard_user", "performance_glitch_user", "problem_user" };
var buyer = new[] { ("firstName", "Ada"), ("lastName", "Lovelace"), ("postalCode", "10115") };
string[] items = { "sauce-labs-backpack", "sauce-labs-bike-light" };
string T(string sel) => $"[data-test=\"{sel}\"]";
double Money(string s) => double.Parse(Regex.Replace(s, "[^0-9.]", ""), System.Globalization.CultureInfo.InvariantCulture);
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
new { proxy = Environment.GetEnvironmentVariable("PROXY_URL_US"), headless = true });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
// The checkout as one user, in a fresh context: every step waits for the page it leads to
// (WaitForURLAsync), never for a fixed time.
async Task<JsonObject> Checkout(string user)
{
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
var steps = new JsonObject();
var sw = Stopwatch.StartNew();
void Step(string name) { steps[name] = sw.ElapsedMilliseconds; sw.Restart(); }
var output = new JsonObject
{
["user"] = user, ["empty_form_error"] = null, ["typed_back"] = null, ["step_one_error"] = null, ["item_sum"] = null,
["subtotal"] = null, ["tax"] = null, ["total"] = null, ["totals_add_up"] = null, ["completed"] = null,
};
try
{
await page.GotoAsync(Site);
await page.FillAsync(T("username"), user);
await page.FillAsync(T("password"), Password);
await page.ClickAsync(T("login-button"));
await page.WaitForURLAsync("**/inventory.html");
Step("login");
foreach (var item in items) await page.ClickAsync(T($"add-to-cart-{item}"));
await page.Locator(T("shopping-cart-badge")).Filter(new() { HasText = items.Length.ToString() }).WaitForAsync();
await page.ClickAsync(T("shopping-cart-link"));
await page.WaitForURLAsync("**/cart.html");
await page.ClickAsync(T("checkout"));
await page.WaitForURLAsync("**/checkout-step-one.html");
Step("cart");
// Step one: an empty form is refused; then type the buyer and read the fields back.
await page.ClickAsync(T("continue"));
output["empty_form_error"] = await page.Locator(T("error")).InnerTextAsync();
foreach (var (field, value) in buyer) await page.FillAsync(T(field), value);
var typed = new JsonObject();
foreach (var (field, _) in buyer) typed[field] = await page.InputValueAsync(T(field));
output["typed_back"] = typed;
await page.ClickAsync(T("continue"));
// Either step two loads or the form shows an error: wait for whichever comes first.
await page.Locator($"{T("subtotal-label")}, {T("error")}").First.WaitForAsync();
if (!page.Url.Contains("checkout-step-two"))
{
output["step_one_error"] = await page.Locator(T("error")).InnerTextAsync();
Step("step_one");
output["steps_ms"] = steps;
return output;
}
Step("step_one");
// Step two: the overview's totals against the item prices.
var prices = await page.Locator(T("inventory-item-price")).AllInnerTextsAsync();
var itemSum = Math.Round(prices.Sum(Money), 2);
var subtotal = Money(await page.Locator(T("subtotal-label")).InnerTextAsync());
var tax = Money(await page.Locator(T("tax-label")).InnerTextAsync());
var total = Money(await page.Locator(T("total-label")).InnerTextAsync());
output["item_sum"] = itemSum;
output["subtotal"] = subtotal;
output["tax"] = tax;
output["total"] = total;
output["totals_add_up"] = subtotal == itemSum && Math.Abs(subtotal + tax - total) < 0.005;
await page.ClickAsync(T("finish"));
await page.WaitForURLAsync("**/checkout-complete.html");
output["completed"] = await page.Locator(T("complete-header")).InnerTextAsync();
Step("step_two_and_finish");
output["steps_ms"] = steps;
return output;
}
finally
{
await context.CloseAsync();
}
}
try
{
var runs = new JsonArray();
foreach (var user in users) runs.Add(await Checkout(user));
Console.WriteLine(runs.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
await browser.CloseAsync();
}
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL_US
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
const site = "https://www.saucedemo.com" // a public demo shop for testing practice
const password = "secret_sauce" // printed on the shop's login page
// The shop's demo users: a normal one, one whose pages are slow, and one with broken inputs.
var users = []string{"standard_user", "performance_glitch_user", "problem_user"}
var buyer = [][2]string{{"firstName", "Ada"}, {"lastName", "Lovelace"}, {"postalCode", "10115"}}
var items = []string{"sauce-labs-backpack", "sauce-labs-bike-light"}
func t(sel string) string { return `[data-test="` + sel + `"]` }
var notMoney = regexp.MustCompile(`[^0-9.]`)
func money(s string) float64 {
v, _ := strconv.ParseFloat(notMoney.ReplaceAllString(s, ""), 64)
return v
}
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
type run struct {
User string `json:"user"`
EmptyFormError *string `json:"empty_form_error"`
TypedBack map[string]string `json:"typed_back"`
StepOneError *string `json:"step_one_error"`
ItemSum *float64 `json:"item_sum"`
Subtotal *float64 `json:"subtotal"`
Tax *float64 `json:"tax"`
Total *float64 `json:"total"`
TotalsAddUp *bool `json:"totals_add_up"`
Completed *string `json:"completed"`
StepsMs map[string]int64 `json:"steps_ms"`
}
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
// The checkout as one user, in a fresh context: every step waits for the page it leads to
// (WaitForURL), never for a fixed time.
func checkout(browser playwright.Browser, user string) run {
context := must(browser.NewContext())
defer context.Close()
page := must(context.NewPage())
out := run{User: user, StepsMs: map[string]int64{}}
mark := time.Now()
step := func(name string) { out.StepsMs[name] = time.Since(mark).Milliseconds(); mark = time.Now() }
_, err := page.Goto(site)
check(err)
check(page.Locator(t("username")).Fill(user))
check(page.Locator(t("password")).Fill(password))
check(page.Locator(t("login-button")).Click())
check(page.WaitForURL("**/inventory.html"))
step("login")
for _, item := range items {
check(page.Locator(t("add-to-cart-" + item)).Click())
}
check(page.Locator(t("shopping-cart-badge")).Filter(playwright.LocatorFilterOptions{HasText: strconv.Itoa(len(items))}).WaitFor())
check(page.Locator(t("shopping-cart-link")).Click())
check(page.WaitForURL("**/cart.html"))
check(page.Locator(t("checkout")).Click())
check(page.WaitForURL("**/checkout-step-one.html"))
step("cart")
// Step one: an empty form is refused; then type the buyer and read the fields back.
check(page.Locator(t("continue")).Click())
emptyErr := must(page.Locator(t("error")).InnerText())
out.EmptyFormError = &emptyErr
for _, f := range buyer {
check(page.Locator(t(f[0])).Fill(f[1]))
}
out.TypedBack = map[string]string{}
for _, f := range buyer {
out.TypedBack[f[0]] = must(page.Locator(t(f[0])).InputValue())
}
check(page.Locator(t("continue")).Click())
// Either step two loads or the form shows an error: wait for whichever comes first.
check(page.Locator(t("subtotal-label") + ", " + t("error")).First().WaitFor())
if !strings.Contains(page.URL(), "checkout-step-two") {
stepErr := must(page.Locator(t("error")).InnerText())
out.StepOneError = &stepErr
step("step_one")
return out
}
step("step_one")
// Step two: the overview's totals against the item prices.
sum := 0.0
for _, p := range must(page.Locator(t("inventory-item-price")).AllInnerTexts()) {
sum += money(p)
}
itemSum := math.Round(sum*100) / 100
subtotal := money(must(page.Locator(t("subtotal-label")).InnerText()))
tax := money(must(page.Locator(t("tax-label")).InnerText()))
total := money(must(page.Locator(t("total-label")).InnerText()))
addsUp := subtotal == itemSum && math.Abs(subtotal+tax-total) < 0.005
out.ItemSum, out.Subtotal, out.Tax, out.Total, out.TotalsAddUp = &itemSum, &subtotal, &tax, &total, &addsUp
check(page.Locator(t("finish")).Click())
check(page.WaitForURL("**/checkout-complete.html"))
done := must(page.Locator(t("complete-header")).InnerText())
out.Completed = &done
step("step_two_and_finish")
return out
}
func main() {
session := must(launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL_US"), "headless": true}))
pw := must(playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true}))
defer pw.Stop()
browser := must(pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}}))
defer browser.Close()
runs := []run{}
for _, user := range users {
runs = append(runs, checkout(browser, user))
}
out, _ := json.MarshalIndent(runs, "", " ")
fmt.Println(string(out))
}
What we got
| User | Login (ms) | Cart (ms) | Fields read back | Result | Totals |
|---|---|---|---|---|---|
| standard_user | 2674 | 535 | Ada / Lovelace / 10115 | Thank you for your order! | 39.98 + 3.2 = 43.18 ✓ |
| performance_glitch_user | 8004 | 292 | Ada / Lovelace / 10115 | Thank you for your order! | 39.98 + 3.2 = 43.18 ✓ |
| problem_user | 1960 | 280 | Lovelace / ∅ / 10115 | Error: Last Name is required | — |
From the Node.js run on 2026-10-18. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.
Raw output (Node.js)
[
{
"user": "standard_user",
"empty_form_error": "Error: First Name is required",
"typed_back": {
"firstName": "Ada",
"lastName": "Lovelace",
"postalCode": "10115"
},
"step_one_error": null,
"item_sum": 39.98,
"subtotal": 39.98,
"tax": 3.2,
"total": 43.18,
"totals_add_up": true,
"completed": "Thank you for your order!",
"steps_ms": {
"login": 2674,
"cart": 535,
"step_one": 330,
"step_two_and_finish": 175
}
},
{
"user": "performance_glitch_user",
"empty_form_error": "Error: First Name is required",
"typed_back": {
"firstName": "Ada",
"lastName": "Lovelace",
"postalCode": "10115"
},
"step_one_error": null,
"item_sum": 39.98,
"subtotal": 39.98,
"tax": 3.2,
"total": 43.18,
"totals_add_up": true,
"completed": "Thank you for your order!",
"steps_ms": {
"login": 8004,
"cart": 292,
"step_one": 180,
"step_two_and_finish": 162
}
},
{
"user": "problem_user",
"empty_form_error": "Error: First Name is required",
"typed_back": {
"firstName": "Lovelace",
"lastName": "",
"postalCode": "10115"
},
"step_one_error": "Error: Last Name is required",
"item_sum": null,
"subtotal": null,
"tax": null,
"total": null,
"totals_add_up": null,
"completed": null,
"steps_ms": {
"login": 1960,
"cart": 280,
"step_one": 157
}
}
]Takeaways
- Waiting for the next page instead of sleeping handles slow steps.
performance_glitch_userlost about five seconds per slow step in every run — on the login, on reaching the cart, or both — wherestandard_userlogged in within two or three; the same script finished both, because each step waits for its page rather than for a fixed time. A one-second sleep would have clicked into a page that wasn't there yet. - Read back what you typed. For
problem_user, the last name went into the first-name field and the last name stayed empty — the script typed everything correctly, and the form still refused it with "Last Name is required". Reading the fields back before submitting shows where it went wrong instead of a vague failure later. - Check the numbers on the confirmation steps. The overview's subtotal matched the sum of the two item prices and the total matched subtotal + tax; both completed checkouts ended with "Thank you for your order!".
- An empty step-one form was refused with "Error: First Name is required" for every user: wait for the error element, not for a timeout, to learn a step didn't go through.