Build a Playwright UI Test Framework
Build a cross-browser Playwright framework for a local agri marketplace.
Introduction
30 Second Summary
Online produce trading depends on each handoff staying clear from the first buyer offer to the final delivery. A missed transition can leave a real order stuck at the worst moment.
In this project, you will build a controlled AgriTrade marketplace with an enterprise UI automation framework. You will use Playwright Test with TypeScript to verify the full buyer-to-delivery journey.
What You'll Build
Run your suite to watch Soybean or Wheat move from an open listing to Delivered across three browsers with every result captured in a report.
By the end of this project, you'll have:
- A controlled buyer-to-delivery marketplace where you can move Soybean or Wheat through bidding, offer acceptance, order placement, payment, fulfillment, and delivery.
- A maintainable suite built with a Page Object Model, typed fixtures, listing-scoped locators, and external test data for smoke, regression, and E2E journeys.
- A cross-browser quality gate in GitHub Actions that runs your tests across Chromium, Firefox, and WebKit before publishing an HTML report.
- Secret Mission: Capture a trace for the tagged fulfillment journey and reconstruct every action and assertion from bid to delivery.
Are there any prerequisites?
You need Windows 11 or later plus an internet connection. No installations or accounts are required before you begin because the setup starts from scratch.
Before We Start
Before any hands-on work begins, this is your moment to commit to a safe testing target and the risk your framework should catch. You will test the controlled, local AgriTrade simulator so broken buyer-to-delivery transitions surface before they affect marketplace users or commerce operations.
Set Up the Windows Toolchain
The controlled AgriTrade simulator needs a supported runtime before its browser tests can run reliably. It also needs version control before the same tests can become a CI quality gate.
This step builds that Windows toolchain. Each verified tool gives your framework a reproducible path from local testing to automation.
In this step, get ready to:
- Install the supported Windows development tools.
- Create the project folder with its package configuration and ignore rules.
- Install Playwright with its managed browser binaries.
Install Node.js, Visual Studio Code, and Git
Node.js supplies the runtime used by Playwright. Visual Studio Code gives the project an editor with an integrated PowerShell terminal.
Git records the repository history. GitHub later runs the automated quality gate.
- Press the Windows key to open Windows search.
- Type PowerShell into the search field.
- Press Enter to open PowerShell.
- Check the installed Node.js version by running this command:
node --version
What does this command do?
The command asks the active Node.js executable to print its version. The result tells you whether the required 24.20.0 LTS runtime is ready.
✔️ I see the required Node.js version
Node.js is ready when PowerShell reports v24.20.0. That gives Playwright the pinned runtime used throughout this project.
ⓧ I see an older version
An older result means Windows is still using a runtime below the project pin. Install the required LTS release before creating the project.
- Download the x64 or ARM64 MSI that matches your computer from the Node.js 24.20.0 release page.
- Run the downloaded MSI installer.
- Keep the recommended installer settings.
- Restart PowerShell after the installation finishes.
- Verify the replacement runtime by running this command:
node --version
What should I see?
PowerShell should now report v24.20.0. Restarting PowerShell lets it find the newly installed executable.
ⓧ Command not found
Windows cannot find Node.js yet. The official MSI adds the runtime required by the framework.
- Download the x64 or ARM64 MSI that matches your computer from the Node.js 24.20.0 release page.
- Run the downloaded MSI installer.
- Keep the recommended installer settings.
- Restart PowerShell after the installation finishes.
- Confirm the new installation by running this command:
node --version
What should I see?
PowerShell should print v24.20.0. That confirms Windows can find the required runtime.
Still seeing the wrong Node.js version?
Close every open PowerShell window after installing Node.js. A terminal opened before the installation can retain the old command path.
Run the version check from a fresh PowerShell window. If the older result remains, reinstall the MSI that matches your computer's architecture.
Help me fix the Node.js version detected by PowerShell.
- Download the recommended User setup from the Visual Studio Code Windows setup page.
- Run the downloaded Visual Studio Code installer.
The User setup adds the code command to your path. The installer may ask for permission to change your device.
- Approve the Windows permission prompt if it appears.
- Keep the recommended installer settings.
- Press the Windows key after installation.
- Type Visual Studio Code into the search field.
- Press Enter to open the editor.
You should see Visual Studio Code open on your Windows desktop. That confirms the editor installation works.
- Restart PowerShell so it can detect the new code command.
- Check the installed Git version by running this command:
git version
What does this command do?
The command prints the Git version available in the current PowerShell session. This project requires Git for Windows 2.55.0 or later.
✔️ I see the required Git version
Git is ready when the reported version is 2.55.0 or later. Your project can now track changes locally.
ⓧ I see an older version
The existing Git installation needs an update before you create the repository.
- Download Git for Windows 2.55.0 or later from the official Windows installation page.
- Run the downloaded installer.
- Keep the recommended installer settings.
- Restart PowerShell after the installation finishes.
- Verify the updated installation by running this command:
git version
What should I see?
PowerShell should report Git for Windows 2.55.0 or later. The exact build suffix can vary.
ⓧ Command not found
PowerShell cannot find Git yet. The official Windows package manager command installs Git for Windows.
- Install Git for Windows by running this command:
winget install --id Git.Git -e --source winget
What does this command do?
The command asks Windows Package Manager to install the exact Git package from the configured source. Windows may display a permission prompt during the installation.
- Approve the Windows permission prompt if it appears.
- Restart PowerShell after the installation finishes.
- Confirm the installation by running this command:
git version
What should I see?
PowerShell should print the installed Git version. Confirm that it is 2.55.0 or later.
Git still unavailable?
Restart PowerShell after the installer completes. If the package manager command fails, use the official Git for Windows installer linked above.
Help me fix Git for Windows in PowerShell.
- Visit GitHub in your browser.
- Sign in with your existing GitHub account.
- Create a free GitHub account if you do not have one.
- Confirm that your GitHub home page loads after authentication.
Your browser access is ready when GitHub shows your signed-in home page. That account becomes the destination for the framework in the final step.
Create the Project Folder and Configuration Files
The folder gives every project file one predictable location. The package configuration pins Playwright before any tests are written.
- Return to the fresh PowerShell window.
- Move to your Desktop by running this command:
cd ~/Desktop
What does this command do?
The command moves PowerShell to your Desktop. The project folder will be easy to find there.
- Create the agritrade-ui-automation folder by running these commands:
mkdir agritrade-ui-automation
cd agritrade-ui-automation
What do these commands do?
- The first command creates the agritrade-ui-automation folder on your Desktop.
- The second command moves PowerShell into that folder.
- Open the current project folder in Visual Studio Code by running this command:
code .
What does this command do?
The code . command opens the current agritrade-ui-automation folder as the Visual Studio Code workspace.
- Select Terminal from the Visual Studio Code menu bar.
- Select New Terminal to open an integrated terminal.
- Select PowerShell from the terminal profile menu if another shell opens.
The integrated terminal starts inside the open workspace. Its prompt should end with agritrade-ui-automation.
- Select the new-file control beside agritrade-ui-automation in the Explorer sidebar.
- Enter package.json as the file name.
- Paste this package configuration into package.json.
{
"name": "agritrade-playwright-ui-framework",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node app/server.mjs",
"test": "playwright test",
"test:smoke": "playwright test --grep @smoke",
"test:regression": "playwright test --grep @regression",
"test:e2e": "playwright test --grep @e2e",
"report": "playwright show-report"
},
"devDependencies": {
"@playwright/test": "1.62.1"
}
}
What does this configuration do?
- The package name identifies the framework when npm creates its lockfile.
- The scripts provide consistent commands for starting the simulator and running each risk tier.
- The module setting allows the project to use modern JavaScript imports.
- The development dependency pins Playwright Test to 1.62.1.
- Save package.json by pressing Ctrl+S.
- Confirm that package.json remains listed beneath agritrade-ui-automation in the Explorer sidebar.
Package file showing an error?
Check every double quote and comma against the reference below. JSON rejects missing commas and trailing commas.
Help me find the syntax problem in package.json.
✔️ My package file matches
Your package configuration is ready. Keep the file saved before moving on.
ⓧ I'd like to double check the full code
{
"name": "agritrade-playwright-ui-framework",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node app/server.mjs",
"test": "playwright test",
"test:smoke": "playwright test --grep @smoke",
"test:regression": "playwright test --grep @regression",
"test:e2e": "playwright test --grep @e2e",
"report": "playwright show-report"
},
"devDependencies": {
"@playwright/test": "1.62.1"
}
}
How to use this reference
Compare this complete file with your saved package.json. Every script name and value should match exactly.
Generated dependencies and test reports can be large. An ignore file keeps those local artifacts out of the repository.
- Select the new-file control beside agritrade-ui-automation in the Explorer sidebar.
- Enter .gitignore as the file name.
- Paste these ignore rules into .gitignore.
node_modules/
playwright-report/
test-results/
What do these rules do?
- The node_modules/ rule excludes downloaded npm packages.
- The playwright-report/ rule excludes generated HTML reports.
- The test-results/ rule excludes generated test evidence.
- Save .gitignore by pressing Ctrl+S.
- Confirm that .gitignore appears beside package.json in the Explorer sidebar.
Ignore file missing?
Make sure the file name starts with a dot. Check that Visual Studio Code did not append another file extension.
Help me create the correct .gitignore file.
✔️ My ignore file matches
Your ignore rules are ready. Generated dependencies and test artifacts will stay outside version control.
ⓧ I'd like to double check the full code
node_modules/
playwright-report/
test-results/
How to use this reference
Compare this complete file with your saved .gitignore. Keep the three paths on separate lines.
Install Playwright and Verify the Toolchain
The package file declares which test runner the project needs. npm now installs that dependency and records the exact dependency tree in package-lock.json.
- Install the dependency declared in package.json by running this command in the integrated PowerShell terminal:
npm install
What does this command do?
npm installs Playwright Test 1.62.1 from the development dependency. It also generates package-lock.json for reproducible CI installation.
The terminal should return to its prompt after the installation finishes. You should also see node_modules and package-lock.json in the Explorer sidebar.
npm installation failed?
Confirm that the terminal prompt is inside agritrade-ui-automation. Check that package.json is saved before rerunning the command.
Help me troubleshoot npm install for this Playwright project.
The next download can take several minutes because Playwright fetches three managed browser binaries. A quiet terminal during part of the download does not mean the installation has stopped.
- Install Playwright-managed Chromium, Firefox, and WebKit by running this command:
npx playwright install
What does this command do?
The command downloads the browser binaries managed by the installed Playwright version. These browsers let the same framework run against Chromium, Firefox, and WebKit later.
The terminal should complete each browser download before returning to the PowerShell prompt. That confirms the local browser layer is available.
Browser download interrupted?
Keep the internet connection active while the browser archives download. Rerun the same installation command if the process stops before PowerShell returns to its prompt.
Help me troubleshoot the Playwright browser installation.
Before you run the final checks, which tools do you expect PowerShell to identify successfully?
- Verify Playwright, Git, and the Visual Studio Code command by running these commands:
npx playwright --version
git version
code .
What do these checks prove?
- The first command confirms the installed Playwright Test version.
- The second command confirms Git is available in the integrated terminal.
- The third command asks Visual Studio Code to open the current project folder.
PowerShell prints Version 1.62.1 before printing the installed Git version. Visual Studio Code opens the agritrade-ui-automation folder.
Your Windows toolchain is ready. The framework now has a pinned runtime, reproducible package lock, managed browsers, editor, version control, and repository access.
One of the final checks failed?
Restart Visual Studio Code after installing Node.js or Git. Existing terminals can retain command paths from before the installation.
Confirm that the Explorer sidebar contains package.json. The Playwright version check depends on that file and its installed dependency.
Help me diagnose the failed toolchain check.
Your project foundation is in place. Next, you will launch the controlled AgriTrade marketplace and move its first listing through the visible workflow.
Launch the Controlled Marketplace
Your Windows toolchain can now run Playwright tests against a real browser. The next step needs a stable marketplace that you can safely automate.
A local controlled simulator gives every test the same listings and starting state. It also keeps your automation away from production systems that you do not own.
In this step, get ready to:
- Build a local server for the controlled marketplace.
- Create two interactive agricultural produce listings.
- Run a complete journey from bid submission to delivery.
Build the local server
The simulator uses Node.js built-in HTTP and file-system APIs. This keeps the target small while still giving Playwright a real web page to control.
- Create an app folder inside agritrade-ui-automation from the file sidebar in Visual Studio Code.
- Create server.mjs inside the app folder from the file sidebar.
- Paste the following server module into app/server.mjs:
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
const host = '127.0.0.1';
const port = 4173;
const htmlFile = new URL('./index.html', import.meta.url);
const server = createServer(async (request, response) => {
if (request.url !== '/' && request.url !== '/index.html') {
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
return;
}
try {
const html = await readFile(htmlFile, { encoding: 'utf8' });
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(html);
} catch {
response.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Unable to load the simulator');
}
});
server.listen(port, host, () => {
console.log(`AgriTrade simulator: http://${host}:${port}`);
});
What does this server do?
- The host and port values keep the simulator on your computer at http://127.0.0.1:4173.
- The request handler serves index.html for the marketplace routes.
- The error branches return clear responses when a route or the HTML file is unavailable.
- Save app/server.mjs.
✔️ Awesome, I've got everything!
Your server module now has a host, a port, a file reader, and an HTTP request handler.
ⓧ I'd like to double check the full code
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
const host = '127.0.0.1';
const port = 4173;
const htmlFile = new URL('./index.html', import.meta.url);
const server = createServer(async (request, response) => {
if (request.url !== '/' && request.url !== '/index.html') {
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
return;
}
try {
const html = await readFile(htmlFile, { encoding: 'utf8' });
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(html);
} catch {
response.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Unable to load the simulator');
}
});
server.listen(port, host, () => {
console.log(`AgriTrade simulator: http://${host}:${port}`);
});
What should match?
Compare every import, route, response, and server value with your saved file. The final log should point to http://127.0.0.1:4173.
The start script keeps the integrated PowerShell terminal occupied while the server is running. Leave that process running while you build the page.
- Start the local server from the integrated PowerShell terminal by running:
npm run start
What does this command do?
The start script runs app/server.mjs. The server stays active so it can return the marketplace page after you create it.
You should see AgriTrade simulator: http://127.0.0.1:4173 in PowerShell. That confirms the server is listening for browser requests.
Server does not start?
Confirm that server.mjs is inside the app folder. Check the imports and closing braces against the reference above.
If the port is already occupied, stop the other local process before trying again.
Help me diagnose why the AgriTrade Node.js server does not start.
Build the marketplace interface
The browser needs a deterministic interface with repeated controls. Two independent produce cards create the realistic locator challenge that your test framework handles in the next step.
- Create index.html inside the app folder from the file sidebar.
- Paste this document shell into app/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AgriTrade Marketplace</title>
<style>
:root {
color-scheme: light;
font-family: Inter, system-ui, sans-serif;
background: #f5f7f1;
color: #17351f;
}
body {
margin: 0;
}
</style>
</head>
<body>
</body>
</html>
What does this shell provide?
- The metadata gives the page a title and a responsive viewport.
- The root styles establish the marketplace typeface and colour palette.
- The empty body gives you a valid page that can receive the marketplace content.
- Save app/index.html.
- Open http://127.0.0.1:4173 in your browser.
You should see a blank light-green page. This proves the running server can read and return app/index.html.
Page does not load?
Confirm that PowerShell still shows the running server. Check that the browser address uses port 4173.
Help me diagnose why the local AgriTrade page does not load.
- Insert the marketplace header and listings container between the opening and closing body tags:
<header>
<p class="eyebrow">Controlled QA simulator</p>
<h1>AgriTrade Marketplace</h1>
<p>Submit a buyer bid, accept the farmer offer, place an order, initiate payment, and complete fulfillment.</p>
</header>
<main>
<section id="listings" class="listings" aria-label="Produce listings"></section>
</main>
What does this markup do?
The header explains the controlled workflow. The listings section becomes the destination for the two generated cards.
- Save app/index.html.
- Refresh the marketplace page in your browser.
You should see the AgriTrade Marketplace heading above the workflow description. The produce area remains empty until the listing data is rendered.
Header is missing?
Check that the header sits inside the body element. Confirm that the server terminal is still running.
Help me find why the AgriTrade header is missing from the page.
- Insert these layout styles below the existing body rule inside the style element:
header,
main {
width: min(980px, calc(100% - 32px));
margin: 0 auto;
}
header {
padding: 40px 0 24px;
}
.eyebrow {
color: #58715f;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
.listings {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
padding-bottom: 40px;
}
What do these styles control?
- The shared width keeps the header and listings aligned.
- The eyebrow style separates the simulator label from the main title.
- The responsive grid places cards beside each other when enough space is available.
- Save app/index.html.
- Refresh the marketplace page.
You should see the header centred within a fixed content width. Its simulator label now uses uppercase lettering.
Layout stays unchanged?
Confirm that the new rules are inside the existing style element. Check each selector for its opening and closing brace.
Help me diagnose why the marketplace layout styles are not applied.
- Insert the card and listing-detail styles below the .listings rule:
article {
background: white;
border: 1px solid #d8e1d5;
border-radius: 16px;
box-shadow: 0 8px 24px rgb(23 53 31 / 8%);
padding: 24px;
}
dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px 16px;
margin: 16px 0;
}
dt {
font-weight: 700;
}
dd {
margin: 0;
}
label {
display: block;
font-weight: 700;
margin-bottom: 6px;
}
How are listing details styled?
Each article becomes a bordered card. The description list aligns labels with values so location, quantity, and price are easy to scan.
- Save app/index.html.
- Refresh the marketplace page.
The visible header remains unchanged because the listing cards have not been generated yet. Your saved stylesheet now has the card rules ready for the next visible update.
Seeing broken spacing?
Check that every declaration stays inside its matching selector. Compare the braces around article and dl with the snippet.
Help me fix the card spacing rules in app/index.html.
- Insert the input and action styles below the label rule:
input {
box-sizing: border-box;
font: inherit;
padding: 10px;
width: 100%;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 14px 0;
}
button {
background: #246b36;
border: 0;
border-radius: 8px;
color: white;
cursor: pointer;
font: inherit;
font-weight: 700;
padding: 10px 14px;
}
How do the controls behave?
Inputs expand to the card width. The action container wraps buttons when the browser window becomes narrow.
- Save app/index.html.
- Refresh the marketplace page.
The control rules are now available for every generated listing. The next styles determine which action is visible and how each status reads.
Controls look unstyled later?
Confirm that input, .actions, and button are separate selectors.
Help me check the marketplace input and button CSS.
- Insert the visibility and status styles below the button rule:
button[hidden] {
display: none;
}
.status {
background: #edf6ec;
border-left: 4px solid #4d8c54;
margin: 0;
padding: 10px;
}
Why hide action buttons?
Each listing reveals one valid next action at a time. The status panel gives the learner and the future tests one visible record of the current stage.
- Save app/index.html.
- Refresh the marketplace page.
Your stylesheet is complete. The page is ready to display the two interactive cards.
Hidden buttons appear later?
Confirm that the selector includes [hidden] directly after button.
Help me diagnose why hidden marketplace action buttons remain visible.
The listing data defines the two controlled scenarios. The rendering template turns each record into an independent card with the same action labels.
- Insert this script immediately above the closing body tag:
<script>
const listings = [
{
id: 'soybean-indore',
crop: 'Soybean',
location: 'Indore',
quantity: '20 quintals',
floorPrice: 4200
},
{
id: 'wheat-sehore',
crop: 'Wheat',
location: 'Sehore',
quantity: '15 quintals',
floorPrice: 2800
}
];
const listingsContainer = document.querySelector('#listings');
</script>
What does the listing data represent?
Each record holds one fictional crop scenario. Stable IDs, quantities, and prices make every browser run repeatable.
- Insert the rendering template above the closing script tag:
listingsContainer.innerHTML = listings.map((listing) => `
<article
data-listing-id="${listing.id}"
data-crop="${listing.crop}"
data-quantity="${listing.quantity}"
>
<h2>${listing.crop}</h2>
<dl>
<dt>Location</dt><dd>${listing.location}</dd>
<dt>Quantity</dt><dd>${listing.quantity}</dd>
<dt>Floor price</dt><dd>₹${listing.floorPrice} per quintal</dd>
</dl>
<label for="bid-${listing.id}">Bid amount (₹ per quintal)</label>
<input id="bid-${listing.id}" type="number" min="${listing.floorPrice}" value="${listing.floorPrice + 100}">
<div class="actions">
<button type="button" data-action="place-bid">Place Bid</button>
<button type="button" data-action="accept-offer" hidden>Accept Offer</button>
<button type="button" data-action="place-order" hidden>Place Order</button>
<button type="button" data-action="initiate-payment" hidden>Initiate Payment</button>
<button type="button" data-action="start-fulfillment" hidden>Start Fulfillment</button>
<button type="button" data-action="mark-delivered" hidden>Mark Delivered</button>
</div>
<p class="status" data-testid="status">Ready for offers</p>
</article>
`).join('');
How are the cards generated?
- The template creates one accessible article for each listing record.
- Every input label includes its listing ID so the field remains associated with the correct card.
- Each card starts with one visible Place Bid button and a Ready for offers status.
- Save app/index.html.
- Refresh the marketplace page.
You should see separate Soybean and Wheat cards. Each card has its own bid field, Place Bid button, and Ready for offers status.
Cards do not appear?
Check that the rendering template sits after listingsContainer and before the closing script tag. Compare every backtick and interpolation marker with the snippet.
Help me diagnose why the Soybean and Wheat cards do not render.
- Insert the stage-update function below the rendering template:
function updateStage(card, status, nextAction) {
card.querySelector('[data-testid="status"]').textContent = status;
card.querySelectorAll('button[data-action]').forEach((button) => {
button.hidden = button.dataset.action !== nextAction;
});
}
What does the stage function control?
The updateStage() function changes the card's status text. It also reveals only the button for the next valid marketplace action.
- Insert the first card interaction below updateStage():
for (const card of document.querySelectorAll('article[data-listing-id]')) {
const input = card.querySelector('input');
const quantity = card.dataset.quantity;
card.querySelector('[data-action="place-bid"]').addEventListener('click', () => {
const amount = Number(input.value);
card.dataset.bid = String(amount);
input.disabled = true;
updateStage(card, `Offer ₹${amount} submitted`, 'accept-offer');
});
}
How does bid submission work?
The loop gives each card its own input and stored bid. Submitting a bid disables that card's input before revealing its Accept Offer button.
- Save app/index.html.
- Refresh the marketplace page.
- Enter 4300 in the Soybean bid field.
- Click Place Bid on the Soybean card.
You should see Offer ₹4300 submitted on the Soybean card. The Wheat card should remain at Ready for offers.
Bid status does not change?
Confirm that the interaction loop is inside the script element. Check that the action value is place-bid.
Help me diagnose why the Soybean Place Bid button does not update its status.
- Insert the offer-acceptance and order-placement listeners before the final closing brace of the card loop:
card.querySelector('[data-action="accept-offer"]').addEventListener('click', () => {
updateStage(card, `Offer ₹${card.dataset.bid} accepted`, 'place-order');
});
card.querySelector('[data-action="place-order"]').addEventListener('click', () => {
updateStage(card, `Order placed for ${quantity}`, 'initiate-payment');
});
What do these transitions add?
The accepted status reuses the bid stored on the selected card. Order placement reads that same card's quantity before revealing payment.
- Save app/index.html.
- Refresh the marketplace page.
- Submit a Soybean bid of 4300.
- Click Accept Offer on the Soybean card.
- Click Place Order on the Soybean card.
You should see Order placed for 20 quintals on the Soybean card. The Initiate Payment button should now be visible.
Order action does not appear?
Confirm that both listeners sit inside the card loop. Check that each data-action value matches the corresponding button.
Help me diagnose why the AgriTrade offer or order transition does not appear.
- Insert the payment, fulfillment, and delivery listeners after the order-placement listener:
card.querySelector('[data-action="initiate-payment"]').addEventListener('click', () => {
updateStage(card, 'Payment initiated', 'start-fulfillment');
});
card.querySelector('[data-action="start-fulfillment"]').addEventListener('click', () => {
updateStage(card, 'Fulfillment in progress', 'mark-delivered');
});
card.querySelector('[data-action="mark-delivered"]').addEventListener('click', () => {
updateStage(card, 'Delivered');
});
How does the journey finish?
Payment reveals the fulfillment action. Fulfillment reveals the delivery action before the final click sets the card to Delivered.
- Save app/index.html.
✔️ Awesome, I've got everything!
Your marketplace file now contains the complete layout, two controlled listings, and every workflow transition.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AgriTrade Marketplace</title>
<style>
:root {
color-scheme: light;
font-family: Inter, system-ui, sans-serif;
background: #f5f7f1;
color: #17351f;
}
body {
margin: 0;
}
header,
main {
width: min(980px, calc(100% - 32px));
margin: 0 auto;
}
header {
padding: 40px 0 24px;
}
.eyebrow {
color: #58715f;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
.listings {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
padding-bottom: 40px;
}
article {
background: white;
border: 1px solid #d8e1d5;
border-radius: 16px;
box-shadow: 0 8px 24px rgb(23 53 31 / 8%);
padding: 24px;
}
dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 8px 16px;
margin: 16px 0;
}
dt {
font-weight: 700;
}
dd {
margin: 0;
}
label {
display: block;
font-weight: 700;
margin-bottom: 6px;
}
input {
box-sizing: border-box;
font: inherit;
padding: 10px;
width: 100%;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 14px 0;
}
button {
background: #246b36;
border: 0;
border-radius: 8px;
color: white;
cursor: pointer;
font: inherit;
font-weight: 700;
padding: 10px 14px;
}
button[hidden] {
display: none;
}
.status {
background: #edf6ec;
border-left: 4px solid #4d8c54;
margin: 0;
padding: 10px;
}
</style>
</head>
<body>
<header>
<p class="eyebrow">Controlled QA simulator</p>
<h1>AgriTrade Marketplace</h1>
<p>Submit a buyer bid, accept the farmer offer, place an order, initiate payment, and complete fulfillment.</p>
</header>
<main>
<section id="listings" class="listings" aria-label="Produce listings"></section>
</main>
<script>
const listings = [
{
id: 'soybean-indore',
crop: 'Soybean',
location: 'Indore',
quantity: '20 quintals',
floorPrice: 4200
},
{
id: 'wheat-sehore',
crop: 'Wheat',
location: 'Sehore',
quantity: '15 quintals',
floorPrice: 2800
}
];
const listingsContainer = document.querySelector('#listings');
listingsContainer.innerHTML = listings.map((listing) => `
<article
data-listing-id="${listing.id}"
data-crop="${listing.crop}"
data-quantity="${listing.quantity}"
>
<h2>${listing.crop}</h2>
<dl>
<dt>Location</dt><dd>${listing.location}</dd>
<dt>Quantity</dt><dd>${listing.quantity}</dd>
<dt>Floor price</dt><dd>₹${listing.floorPrice} per quintal</dd>
</dl>
<label for="bid-${listing.id}">Bid amount (₹ per quintal)</label>
<input id="bid-${listing.id}" type="number" min="${listing.floorPrice}" value="${listing.floorPrice + 100}">
<div class="actions">
<button type="button" data-action="place-bid">Place Bid</button>
<button type="button" data-action="accept-offer" hidden>Accept Offer</button>
<button type="button" data-action="place-order" hidden>Place Order</button>
<button type="button" data-action="initiate-payment" hidden>Initiate Payment</button>
<button type="button" data-action="start-fulfillment" hidden>Start Fulfillment</button>
<button type="button" data-action="mark-delivered" hidden>Mark Delivered</button>
</div>
<p class="status" data-testid="status">Ready for offers</p>
</article>
`).join('');
function updateStage(card, status, nextAction) {
card.querySelector('[data-testid="status"]').textContent = status;
card.querySelectorAll('button[data-action]').forEach((button) => {
button.hidden = button.dataset.action !== nextAction;
});
}
for (const card of document.querySelectorAll('article[data-listing-id]')) {
const input = card.querySelector('input');
const quantity = card.dataset.quantity;
card.querySelector('[data-action="place-bid"]').addEventListener('click', () => {
const amount = Number(input.value);
card.dataset.bid = String(amount);
input.disabled = true;
updateStage(card, `Offer ₹${amount} submitted`, 'accept-offer');
});
card.querySelector('[data-action="accept-offer"]').addEventListener('click', () => {
updateStage(card, `Offer ₹${card.dataset.bid} accepted`, 'place-order');
});
card.querySelector('[data-action="place-order"]').addEventListener('click', () => {
updateStage(card, `Order placed for ${quantity}`, 'initiate-payment');
});
card.querySelector('[data-action="initiate-payment"]').addEventListener('click', () => {
updateStage(card, 'Payment initiated', 'start-fulfillment');
});
card.querySelector('[data-action="start-fulfillment"]').addEventListener('click', () => {
updateStage(card, 'Fulfillment in progress', 'mark-delivered');
});
card.querySelector('[data-action="mark-delivered"]').addEventListener('click', () => {
updateStage(card, 'Delivered');
});
}
</script>
</body>
</html>
What should match?
Compare the full structure with your saved file. Pay close attention to the order of the action listeners and the closing braces around the card loop.
Run the complete delivery journey
Before you test the full journey, what do you expect to happen to the visible button after each status change?
- Refresh http://127.0.0.1:4173 in your browser.
You should see a Soybean card for Indore with 20 quintals. You should also see a Wheat card for Sehore with 15 quintals.
- Enter 4300 in the Soybean Bid amount field.
- Click Place Bid on the Soybean card.
You should see Offer ₹4300 submitted before the Accept Offer button becomes available.
- Click Accept Offer on the Soybean card.
You should see Offer ₹4300 accepted before the Place Order button becomes available.
- Click Place Order on the Soybean card.
You should see Order placed for 20 quintals before the Initiate Payment button becomes available.
- Click Initiate Payment on the Soybean card.
You should see Payment initiated before the Start Fulfillment button becomes available.
- Click Start Fulfillment on the Soybean card.
You should see Fulfillment in progress before the Mark Delivered button becomes available.
- Click Mark Delivered on the Soybean card.
That is the controlled workflow complete. You should see Delivered on Soybean while Wheat remains independent and ready for its own bid.
Journey stops before delivery?
Compare the listener order with the full-file reference. Check that every nextAction value matches the next button's data-action value.
Help me diagnose where the AgriTrade journey stops progressing.
Your controlled marketplace now provides stable repeated controls and a complete delivery journey. Next up, you will use those repeated Place Bid buttons to expose a real locator ambiguity.
Reproduce the Locator Failure
Your controlled AgriTrade simulator is running with two independent produce listings. You are ready to test how Playwright chooses controls when a page repeats the same action.
Both listing cards expose a Place Bid button. A global role locator carries no crop context. This test deliberately reveals the ambiguity.
In this step, get ready to:
- Configure Playwright to test the local simulator in Chromium with an HTML report.
- Create an unscoped test that targets the repeated buyer action.
- Reproduce the strict-mode violation with a focused test run.
Configure Playwright for one browser
Playwright needs a configuration file that connects the test runner to your local target. This first version keeps the experiment focused on Chromium.
- Create playwright.config.ts inside agritrade-ui-automation using the file sidebar in VS Code.
- Set the contents of playwright.config.ts by pasting the code below:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
reporter: [
['html', { open: 'never' }]
],
use: {
baseURL: 'http://127.0.0.1:4173'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI
}
});
What Does This Configuration Do?
- The testDir setting tells Playwright to discover tests inside tests.
- The baseURL setting lets tests navigate to / without repeating the complete local address.
- The projects array limits this experiment to the desktop Chromium profile.
- The webServer section starts the simulator when needed. It also reuses the server that is already running locally.
- Save playwright.config.ts.
- Confirm that playwright.config.ts appears beside package.json in the file sidebar.
Configuration File Not Recognized?
Confirm that the filename ends with .ts. Check that it sits directly inside agritrade-ui-automation.
Ask for help checking the configuration structure.
✔️ Awesome, I've got everything!
Your configuration now connects one Chromium project to the controlled simulator.
ⓧ I'd like to double check the full code
The complete configuration for this step is shown below.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
reporter: [
['html', { open: 'never' }]
],
use: {
baseURL: 'http://127.0.0.1:4173'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI
}
});
Create the unscoped locator test
Playwright's getByRole() locator reads the accessible role and name of a control. Without listing scope, the button name matches both cards.
- Create a tests folder inside agritrade-ui-automation using the file sidebar.
You should see the empty tests folder beneath the project files.
- Create naive.spec.ts inside the tests folder.
- Set the contents of naive.spec.ts by pasting the code below:
import { test } from '@playwright/test';
test('naive unscoped locator fails when listing controls repeat', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Place Bid' }).click();
});
What Does This Test Do?
- The import provides Playwright's test function.
- The page.goto('/') call opens the simulator through the configured base URL.
- The role locator searches the complete page for a button named Place Bid.
- The click() call forces Playwright to resolve the locator to a target.
- Save tests/naive.spec.ts.
- Confirm that the tests folder now contains naive.spec.ts.
Test File Showing Problems?
Check that the file path is exactly tests/naive.spec.ts. Confirm that every quote and closing bracket matches the reference.
Ask for help checking the test syntax.
✔️ Awesome, I've got everything!
Your active test now targets the repeated button without choosing a listing.
ⓧ I'd like to double check the full code
The complete test file for this step is shown below.
import { test } from '@playwright/test';
test('naive unscoped locator fails when listing controls repeat', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Place Bid' }).click();
});
Run the ambiguous locator
Strict locator resolution protects a test from silently choosing an unintended control. The focused run now checks whether your locator identifies a single target.
- Create a second PowerShell terminal in VS Code so the simulator keeps running in the original terminal.
- Confirm that the second terminal opens in agritrade-ui-automation.
Before you run this test, which result do you expect: one matching button or multiple matching buttons? Keep your prediction in mind.
- Run the focused Chromium test from agritrade-ui-automation by running this command:
npx playwright test tests/naive.spec.ts --project=chromium
What Just Happened?
You will see the test fail with a strict-mode violation. The failure details identify both Place Bid buttons as matches.
This failure is intentional. Using .first() would select a button by page order and conceal which crop the test intended to use.
- Inspect the locator details in the failed test output.
- Confirm that the output identifies both Place Bid buttons.
Seeing a Different Result?
- Check that tests/naive.spec.ts still uses the unscoped role locator from the reference.
- Confirm that the original simulator terminal is still serving http://127.0.0.1:4173.
- Verify that both Soybean and Wheat cards appear in the simulator.
Ask for help diagnosing the unexpected test result.
You have reproduced the exact ambiguity that can send a test toward the wrong listing. That failure gives your framework a concrete design requirement: every business action must identify its listing.
With the ambiguity proven, the next step turns crop names into stable listing boundaries through a Page Object and a typed fixture.
Build Page Objects and Fixtures
The strict-mode failure from the last step proved that a global button locator cannot express which crop the buyer intends to trade. The framework now needs a business-level interface that scopes every action to one listing.
A Playwright Page Object Model centralizes that scoping logic. A typed fixture gives each test a ready-to-use marketplace that has already navigated to the simulator.
In this step, get ready to:
- Build crop-scoped marketplace actions with a Page Object.
- Provide the Page Object through a typed fixture.
- Run a Soybean smoke journey through every marketplace status.
Build the crop-scoped Page Object
The Page Object converts repeated selectors into named marketplace actions. Its central rule is simple: find the article with the exact crop heading before locating any control inside it.
- Create a folder named pages inside agritrade-ui-automation using the VS Code file sidebar.
- Create marketplace.page.ts inside the new pages folder.
- Add the Page Object foundation by pasting this code into pages/marketplace.page.ts:
import { type Locator, type Page } from '@playwright/test';
export class MarketplacePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async goto() {
await this.page.goto('/');
}
listing(crop: string): Locator {
return this.page
.getByRole('article')
.filter({
has: this.page.getByRole('heading', { name: crop, exact: true })
});
}
status(crop: string): Locator {
return this.listing(crop).getByTestId('status');
}
}
How does the scoping work?
- The page property keeps the Playwright page available to every method.
- The goto method opens the simulator through the configured base URL.
- The listing(crop) method filters article locators by an exact crop heading.
- The status(crop) method finds the status element inside that crop's article.
- Save pages/marketplace.page.ts.
- Confirm that the editor shows the class without red error underlines.
Seeing errors in the Page Object?
Check that the import comes from @playwright/test. Confirm that the class has one closing brace after the status method.
Ask for help with the class structure.
The foundation identifies one crop card. The next methods keep each buyer action inside that card.
- Place your cursor above the final closing brace in pages/marketplace.page.ts.
- Add the scoped bid action by pasting this method:
async submitBid(crop: string, amount: number) {
const card = this.listing(crop);
await card.getByLabel('Bid amount (₹ per quintal)').fill(String(amount));
await card.getByRole('button', { name: 'Place Bid', exact: true }).click();
}
What does the bid action do?
The method gets the intended crop card before filling the bid field. It clicks the exact Place Bid button inside the same card, which removes the ambiguity from the naive locator.
- Save pages/marketplace.page.ts.
- Confirm that submitBid appears inside MarketplacePage without red error underlines.
Is the bid method outside the class?
Move submitBid above the class's final closing brace. Keep the method below status.
Ask for help placing the method correctly.
- Place your cursor above the final closing brace in pages/marketplace.page.ts.
- Add the offer acceptance action by pasting this method:
async acceptOffer(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Accept Offer', exact: true })
.click();
}
Why scope every button?
Each listing contains its own workflow controls. Calling listing(crop) first preserves the intended crop as the workflow advances.
- Save pages/marketplace.page.ts.
- Confirm that acceptOffer sits below submitBid inside the class.
Does the offer action show an error?
Check the parentheses around the button name options. Confirm that the method's closing brace appears before the class's final closing brace.
Ask for help checking the method.
- Place your cursor above the final closing brace in pages/marketplace.page.ts.
- Add the order placement action by pasting this method:
async placeOrder(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Place Order', exact: true })
.click();
}
What does this method preserve?
The method keeps order placement inside the crop card selected by the test. Tests can now describe the action as placeOrder without repeating selector details.
- Save pages/marketplace.page.ts.
- Confirm that placeOrder appears inside the class without red error underlines.
Is the order locator incomplete?
Confirm that the chain starts with this.listing(crop). Check that the button name matches the simulator code exactly.
Ask for help checking the locator chain.
The remaining methods cover payment through delivery. They retain the same crop-scoped pattern so every transition stays attached to one listing.
- Place your cursor above the final closing brace in pages/marketplace.page.ts.
- Add the payment action by pasting this method:
async initiatePayment(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Initiate Payment', exact: true })
.click();
}
What does the payment method add?
The method exposes payment initiation as a business action. Its exact button locator remains hidden inside the Page Object.
- Save pages/marketplace.page.ts.
- Confirm that initiatePayment appears inside the class without red error underlines.
Is the payment method misplaced?
Keep initiatePayment below placeOrder. Confirm that it remains above the class's final closing brace.
Ask for help with the method placement.
- Place your cursor above the final closing brace in pages/marketplace.page.ts.
- Add the fulfillment action by pasting this method:
async startFulfillment(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Start Fulfillment', exact: true })
.click();
}
Why keep fulfillment crop-scoped?
The workflow still contains two listing cards at this stage. The scoped action prevents fulfillment from drifting to a different crop.
- Save pages/marketplace.page.ts.
- Confirm that startFulfillment appears inside the class without red error underlines.
Does fulfillment show a locator error?
Check that the method calls this.listing(crop) before locating the button. Confirm that the button name uses the same capitalization as the simulator.
Ask for help checking the fulfillment action.
- Place your cursor above the final closing brace in pages/marketplace.page.ts.
- Add the delivery action by pasting this method:
async markDelivered(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Mark Delivered', exact: true })
.click();
}
What completes the Page Object?
The final method advances the chosen listing to its delivered state. The class now covers every marketplace action from bid submission through delivery.
- Save pages/marketplace.page.ts.
- Confirm that the file ends with one brace for markDelivered and one brace for MarketplacePage.
Does the class end with extra braces?
Compare the final two braces with the full-file reference below. One closes markDelivered. The other closes the class.
Ask for help balancing the class braces.
✔️ Awesome, I've got everything!
Your Page Object now scopes every status check and workflow action to a named crop listing.
ⓧ I'd like to double check the full code
import { type Locator, type Page } from '@playwright/test';
export class MarketplacePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async goto() {
await this.page.goto('/');
}
listing(crop: string): Locator {
return this.page
.getByRole('article')
.filter({
has: this.page.getByRole('heading', { name: crop, exact: true })
});
}
status(crop: string): Locator {
return this.listing(crop).getByTestId('status');
}
async submitBid(crop: string, amount: number) {
const card = this.listing(crop);
await card.getByLabel('Bid amount (₹ per quintal)').fill(String(amount));
await card.getByRole('button', { name: 'Place Bid', exact: true }).click();
}
async acceptOffer(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Accept Offer', exact: true })
.click();
}
async placeOrder(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Place Order', exact: true })
.click();
}
async initiatePayment(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Initiate Payment', exact: true })
.click();
}
async startFulfillment(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Start Fulfillment', exact: true })
.click();
}
async markDelivered(crop: string) {
await this.listing(crop)
.getByRole('button', { name: 'Mark Delivered', exact: true })
.click();
}
}
How to use this reference
Compare the method order and braces with your saved file. This version contains the complete crop-scoped Page Object for this step.
Add the typed fixture and retire the failing test
The Page Object defines what the marketplace can do. The fixture controls how each test receives a fresh instance with navigation already complete.
- Create a folder named fixtures inside agritrade-ui-automation using the VS Code file sidebar.
- Create marketplace.fixture.ts inside the new fixtures folder.
- Define the typed marketplace fixture by pasting this code into fixtures/marketplace.fixture.ts:
import { test as base } from '@playwright/test';
import { MarketplacePage } from '../pages/marketplace.page';
type MarketplaceFixtures = {
marketplace: MarketplacePage;
};
export const test = base.extend<MarketplaceFixtures>({
marketplace: async ({ page }, use) => {
const marketplace = new MarketplacePage(page);
await marketplace.goto();
await use(marketplace);
}
});
export { expect } from '@playwright/test';
What does the fixture provide?
- The MarketplaceFixtures type declares the marketplace object available to tests.
- The fixture creates MarketplacePage with Playwright's isolated page.
- The call to marketplace.goto() loads the simulator before the test body starts.
- The exported expect keeps test imports together in one module.
- Save fixtures/marketplace.fixture.ts.
- Confirm that the editor resolves the import from ../pages/marketplace.page without a red error underline.
Can the fixture find MarketplacePage?
Confirm that fixtures and pages are sibling folders. Check that the file is named marketplace.page.ts.
Ask for help with the fixture import.
✔️ Awesome, I've got everything!
Your fixture now supplies a navigated marketplace interface to each test.
ⓧ I'd like to double check the full code
import { test as base } from '@playwright/test';
import { MarketplacePage } from '../pages/marketplace.page';
type MarketplaceFixtures = {
marketplace: MarketplacePage;
};
export const test = base.extend<MarketplaceFixtures>({
marketplace: async ({ page }, use) => {
const marketplace = new MarketplacePage(page);
await marketplace.goto();
await use(marketplace);
}
});
export { expect } from '@playwright/test';
How to use this reference
Compare the imports and fixture callback with your saved file. The callback must navigate before passing the marketplace to the test.
The naive test has completed its job by exposing the ambiguous locator. Skipping it preserves the example without leaving an intentional failure in the active suite.
- Return to tests/naive.spec.ts in the editor.
- Find the active test declaration shown below:
test('naive unscoped locator fails when listing controls repeat', async ({ page }) => {
Why change this declaration?
This declaration keeps the designed obstacle active. The suite needs to retain the example while excluding it from normal test runs.
- Replace test with test.skip so the declaration looks like this:
test.skip('naive unscoped locator fails when listing controls repeat', async ({ page }) => {
What does the skipped test preserve?
The test still documents the unscoped locator that caused the strict-mode violation. Playwright now excludes it from active execution.
- Save tests/naive.spec.ts.
- Confirm that the declaration begins with test.skip.
Is the naive test still active?
Check that the period appears between test and skip. Keep the original test body unchanged.
Ask for help checking the skipped declaration.
✔️ Awesome, I've got everything!
The locator-failure example remains in the repository without breaking active runs.
ⓧ I'd like to double check the full code
import { test } from '@playwright/test';
test.skip('naive unscoped locator fails when listing controls repeat', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Place Bid' }).click();
});
How to use this reference
Compare the complete file with your version. Only the test declaration changes from the intentional failure created earlier.
Prove the full Soybean journey
A Page Object earns its place when a test reads like the business workflow it protects. Web-first assertions check each visible status before the journey moves to its next stage.
- Create marketplace.spec.ts inside the existing tests folder using the VS Code file sidebar.
- Add the hard-coded Soybean smoke journey by pasting this code into tests/marketplace.spec.ts:
import { test, expect } from '../fixtures/marketplace.fixture';
test(
'Soybean buyer-to-fulfillment journey',
{ tag: '@smoke' },
async ({ marketplace }) => {
await marketplace.submitBid('Soybean', 4300);
await expect(marketplace.status('Soybean')).toHaveText('Offer ₹4300 submitted');
await marketplace.acceptOffer('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Offer ₹4300 accepted');
await marketplace.placeOrder('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Order placed for 20 quintals');
await marketplace.initiatePayment('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Payment initiated');
await marketplace.startFulfillment('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Fulfillment in progress');
await marketplace.markDelivered('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Delivered');
}
);
What does this test prove?
- The fixture supplies a marketplace that has already opened the simulator.
- Each business action targets the Soybean article through the Page Object.
- Each toHaveText assertion waits for the exact workflow status.
- The @smoke tag lets the suite run this journey as a focused risk check.
- Save tests/marketplace.spec.ts.
- Confirm that the editor resolves the fixture import without a red error underline.
Does the smoke test show TypeScript errors?
Confirm that the import path starts with ../fixtures. Check every method name against MarketplacePage.
Ask for help comparing the test with the fixture.
✔️ Awesome, I've got everything!
Your smoke test expresses the complete Soybean workflow through business-level actions.
ⓧ I'd like to double check the full code
import { test, expect } from '../fixtures/marketplace.fixture';
test(
'Soybean buyer-to-fulfillment journey',
{ tag: '@smoke' },
async ({ marketplace }) => {
await marketplace.submitBid('Soybean', 4300);
await expect(marketplace.status('Soybean')).toHaveText('Offer ₹4300 submitted');
await marketplace.acceptOffer('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Offer ₹4300 accepted');
await marketplace.placeOrder('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Order placed for 20 quintals');
await marketplace.initiatePayment('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Payment initiated');
await marketplace.startFulfillment('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Fulfillment in progress');
await marketplace.markDelivered('Soybean');
await expect(marketplace.status('Soybean')).toHaveText('Delivered');
}
);
How to use this reference
Compare every business action with the assertion that follows it. The final assertion must check the exact Delivered status.
Before you run the smoke check, do you expect the crop-scoped locator to choose one Place Bid button or both?
- Run the focused Chromium smoke test from the VS Code PowerShell terminal:
npx playwright test tests/marketplace.spec.ts --project=chromium --grep @smoke
What does this command check?
The command runs the marketplace test in the configured Chromium project. The grep filter selects the test carrying the @smoke tag.
You should see the Soybean journey pass in Chromium. The final Delivered assertion completes without the earlier strict-mode violation.
Did the smoke journey fail?
If the run reports multiple matching buttons, confirm that every action starts with this.listing(crop). If a status assertion fails, compare its text with the simulator status exactly.
Ask for help diagnosing the smoke test.
That is the ambiguity fixed: the framework now drives one named listing from bid submission to delivery through reusable business actions.
Your scoped Soybean journey is green. Next, you will replace the hard-coded scenario with external data, add risk tiers, expand across three browsers, and turn the suite into a CI quality gate.
Scale the Suite and Add CI
Your scoped Playwright journey now carries Soybean from bid to delivery in Chromium. A single passing journey only proves one path in one browser.
External journey data adds distinct risk boundaries. Cross-browser execution widens coverage. GitHub Actions turns those checks into an automated quality gate for every change.
In this step, get ready to:
- Drive smoke, regression, and E2E journeys from typed external data.
- Run the suite across Chromium, Firefox, and WebKit with failure evidence.
- Publish the framework with an automated CI quality gate.
Drive tests from risk-tier data
The temporary smoke test duplicates one complete journey inside the test file. A typed data module lets each record declare its crop, bid, risk tag, and stopping boundary.
- Create data/journeys.ts inside the agritrade-ui-automation folder by adding this code:
export type Journey = {
crop: 'Soybean' | 'Wheat';
bidAmount: number;
quantity: string;
tag: '@smoke' | '@regression' | '@e2e';
stopAfter: 'bid' | 'accept' | 'delivered';
};
export const journeys: Journey[] = [
{
crop: 'Soybean',
bidAmount: 4300,
quantity: '20 quintals',
tag: '@smoke',
stopAfter: 'bid'
},
{
crop: 'Wheat',
bidAmount: 2900,
quantity: '15 quintals',
tag: '@regression',
stopAfter: 'accept'
},
{
crop: 'Soybean',
bidAmount: 4400,
quantity: '20 quintals',
tag: '@e2e',
stopAfter: 'delivered'
}
];
How do the journey records work?
- The Journey type restricts every record to a known crop, tag, and stopping boundary.
- The smoke record checks bid submission for Soybean.
- The regression record checks offer acceptance for Wheat.
- The E2E record follows Soybean through delivery.
- Save data/journeys.ts.
- Confirm that the VS Code Explorer shows journeys.ts inside the new data folder.
Cannot find the journey file?
Check that journeys.ts sits inside agritrade-ui-automation/data. A file beside the data folder gives the test import the wrong path.
Ask for help checking the file location and TypeScript structure.
✔️ Awesome, I've got everything!
Great. Your three risk-tier records are saved in one typed data module.
ⓧ I'd like to double check the full code
export type Journey = {
crop: 'Soybean' | 'Wheat';
bidAmount: number;
quantity: string;
tag: '@smoke' | '@regression' | '@e2e';
stopAfter: 'bid' | 'accept' | 'delivered';
};
export const journeys: Journey[] = [
{
crop: 'Soybean',
bidAmount: 4300,
quantity: '20 quintals',
tag: '@smoke',
stopAfter: 'bid'
},
{
crop: 'Wheat',
bidAmount: 2900,
quantity: '15 quintals',
tag: '@regression',
stopAfter: 'accept'
},
{
crop: 'Soybean',
bidAmount: 4400,
quantity: '20 quintals',
tag: '@e2e',
stopAfter: 'delivered'
}
];
What should match?
Compare each crop, bid amount, quantity, tag, and stopping boundary with your saved file. These values determine the exact coverage of each risk tier.
The test loop now needs to read those records. Each journey runs the same business actions until its declared boundary tells the test to return.
- Select tests/marketplace.spec.ts in the VS Code Explorer.
- Replace the current hard-coded test with the first half below.
- Keep the file unsaved until the second half closes the test loop.
import { test, expect } from '../fixtures/marketplace.fixture';
import { journeys } from '../data/journeys';
for (const journey of journeys) {
test(
`${journey.crop} buyer-to-fulfillment journey`,
{ tag: journey.tag },
async ({ marketplace }) => {
await marketplace.submitBid(journey.crop, journey.bidAmount);
await expect(marketplace.status(journey.crop)).toHaveText(
`Offer ₹${journey.bidAmount} submitted`
);
if (journey.stopAfter === 'bid') {
return;
}
await marketplace.acceptOffer(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText(
`Offer ₹${journey.bidAmount} accepted`
);
if (journey.stopAfter === 'accept') {
return;
}
What does the first half do?
- The second import brings the external journeys array into the test file.
- The loop creates one test from each record.
- The test details expose journey.tag to Playwright filtering.
- The two early returns stop smoke after the bid and regression after acceptance.
- Complete the open test loop by adding this second half directly below the first:
await marketplace.placeOrder(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText(
`Order placed for ${journey.quantity}`
);
await marketplace.initiatePayment(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText('Payment initiated');
await marketplace.startFulfillment(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText(
'Fulfillment in progress'
);
await marketplace.markDelivered(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText('Delivered');
}
);
}
What does the second half do?
Only the E2E record reaches this section because the earlier records have already returned. The remaining assertions verify order placement, payment, fulfillment, and delivery.
- Save tests/marketplace.spec.ts.
Before you run the filtered suites, which journey do you expect each tag to select?
- Run the three risk tiers from the VS Code PowerShell terminal with these commands:
npm run test:smoke
npm run test:regression
npm run test:e2e
What do these commands prove?
- The smoke command selects the Soybean bid boundary.
- The regression command selects the Wheat acceptance boundary.
- The E2E command selects the complete Soybean delivery boundary.
You should see each filtered run pass in Chromium. The three tags now represent different levels of business risk without duplicating the workflow.
Is a risk tier failing?
Check that every tag starts with @. Check that each stopAfter value matches its exact union value.
Ask for help comparing the journey records with the test loop.
✔️ Awesome, I've got everything!
Your smoke, regression, and E2E tags now select separate business boundaries.
ⓧ I'd like to double check the full code
import { test, expect } from '../fixtures/marketplace.fixture';
import { journeys } from '../data/journeys';
for (const journey of journeys) {
test(
`${journey.crop} buyer-to-fulfillment journey`,
{ tag: journey.tag },
async ({ marketplace }) => {
await marketplace.submitBid(journey.crop, journey.bidAmount);
await expect(marketplace.status(journey.crop)).toHaveText(
`Offer ₹${journey.bidAmount} submitted`
);
if (journey.stopAfter === 'bid') {
return;
}
await marketplace.acceptOffer(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText(
`Offer ₹${journey.bidAmount} accepted`
);
if (journey.stopAfter === 'accept') {
return;
}
await marketplace.placeOrder(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText(
`Order placed for ${journey.quantity}`
);
await marketplace.initiatePayment(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText('Payment initiated');
await marketplace.startFulfillment(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText(
'Fulfillment in progress'
);
await marketplace.markDelivered(journey.crop);
await expect(marketplace.status(journey.crop)).toHaveText('Delivered');
}
);
}
What should match?
Confirm that the loop imports journeys. Confirm that it returns only at the bid and accept boundaries.
Add cross-browser reporting
Chromium alone cannot reveal engine-specific behavior. The final configuration runs isolated journeys through Chromium, Firefox, and WebKit.
CI safeguards reject focused or flaky tests. Screenshots and traces preserve evidence when a run fails.
- Select playwright.config.ts in the VS Code Explorer.
- Replace the current configuration with the first half below.
- Keep the file unsaved until the browser projects close the configuration.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
failOnFlakyTests: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['list'],
['html', { open: 'never' }]
],
use: {
baseURL: 'http://127.0.0.1:4173',
screenshot: 'only-on-failure',
trace: 'on-first-retry'
},
What does the shared configuration do?
- Local runs use available workers while CI uses one worker and one retry.
- The list reporter keeps terminal progress visible.
- The HTML reporter saves a browsable test report.
- Failure screenshots and first-retry traces preserve diagnostic evidence.
- Complete the configuration by adding these browser projects and web server settings directly below the first half:
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI
}
});
Why configure projects and a web server?
Each project applies a desktop browser profile to the same isolated journey records. The web server setting starts the controlled simulator before tests use the configured base URL.
- Save playwright.config.ts.
Before you run the full suite, do you expect the same journey records to behave consistently in all three browser engines?
- Run every risk tier, the full suite, and the HTML report with these commands:
npm run test:smoke
npm run test:regression
npm run test:e2e
npm test
npm run report
What does this test sequence cover?
The first three commands verify tag filtering. The full test command executes every journey across all configured projects.
The final command serves the generated HTML report. It gives you one place to inspect browser-specific results and test evidence.
You should see successful runs for Chromium, Firefox, and WebKit. The report should list the tagged marketplace journeys under their browser projects.
- Confirm that the HTML report contains results for Chromium, Firefox, and WebKit.
- Return to the VS Code PowerShell terminal after checking the report.
- Stop the report server with Ctrl+C.
Is one browser project failing?
Confirm that all three Playwright-managed browser binaries remain installed. Check the failed project in the HTML report for its screenshot or retry trace.
Ask for help interpreting the cross-browser result.
✔️ Awesome, I've got everything!
Your local quality gate now covers three risk tiers across three browser engines.
ⓧ I'd like to double check the full code
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
failOnFlakyTests: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['list'],
['html', { open: 'never' }]
],
use: {
baseURL: 'http://127.0.0.1:4173',
screenshot: 'only-on-failure',
trace: 'on-first-retry'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI
}
});
What should match?
Compare the CI safeguards, reporters, evidence settings, three projects, and local web server with your saved configuration.
Publish the CI quality gate
A public GitHub repository makes the framework reviewable. Its workflow repeats the same installation and test path on a clean hosted runner.
The README explains the controlled target and non-affiliation boundary. It also gives another engineer the exact commands needed to run each risk tier.
- Create README.md inside the agritrade-ui-automation folder by adding this opening section:
# AgriTrade Playwright UI Framework
A controlled agricultural marketplace simulator and Playwright Test portfolio project. It models synthetic produce bidding, offer acceptance, order placement, payment initiation, fulfillment, and delivery journeys.
This repository is not affiliated with ITC. It does not automate ITC e-Choupal or ITCMAARS production systems.
## Stack
- Node.js 24.20.0 LTS
- Playwright Test 1.62.1
- TypeScript tests
- Chromium, Firefox, and WebKit
- GitHub Actions
Why state the project boundary?
The opening identifies the marketplace as synthetic and controlled. It prevents readers from mistaking the simulator for an ITC product or production automation target.
- Save README.md.
- Open the VS Code Markdown preview to confirm that the project title and stack render as headings and lists.
- Add the local run section directly below the stack:
## Run locally
```powershell
npm install
npx playwright install
npm test
npm run report
```
What does the local section provide?
A new contributor can install the locked project dependencies, download the managed browsers, run the suite, and open the report from one short sequence.
- Save README.md.
- Refresh the Markdown preview to confirm that the PowerShell commands appear in a formatted code block.
- Finish the README by adding the risk tiers and design sections:
## Risk tiers
```powershell
npm run test:smoke
npm run test:regression
npm run test:e2e
```
## Design
- `pages/` stores application-specific Page Objects.
- `fixtures/` provides typed, isolated test setup.
- `data/` stores reusable business journey records.
- `tests/` contains tagged orchestration and assertions.
- `app/` is the deterministic local test target.
How does the design section help?
The risk-tier commands expose the three quality gates. The design list shows where business actions, setup, data, orchestration, and the controlled target belong.
- Save README.md.
- Refresh the Markdown preview to confirm that the risk commands and five design folders render correctly.
✔️ Awesome, I've got everything!
Your README now explains the target, stack, commands, risk tiers, and framework structure.
ⓧ I'd like to double check the full code
# AgriTrade Playwright UI Framework
A controlled agricultural marketplace simulator and Playwright Test portfolio project. It models synthetic produce bidding, offer acceptance, order placement, payment initiation, fulfillment, and delivery journeys.
This repository is not affiliated with ITC. It does not automate ITC e-Choupal or ITCMAARS production systems.
## Stack
- Node.js 24.20.0 LTS
- Playwright Test 1.62.1
- TypeScript tests
- Chromium, Firefox, and WebKit
- GitHub Actions
## Run locally
```powershell
npm install
npx playwright install
npm test
npm run report
```
## Risk tiers
```powershell
npm run test:smoke
npm run test:regression
npm run test:e2e
```
## Design
- `pages/` stores application-specific Page Objects.
- `fixtures/` provides typed, isolated test setup.
- `data/` stores reusable business journey records.
- `tests/` contains tagged orchestration and assertions.
- `app/` is the deterministic local test target.
What should match?
Compare every heading, command, tool version, project boundary, and folder description with your saved README.
The workflow installs from package-lock.json with npm ci. This gives CI the same pinned dependency graph represented by your local lockfile.
- Create .github/workflows/playwright.yml inside the agritrade-ui-automation folder by adding this workflow skeleton:
name: Playwright UI Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
What does the workflow skeleton define?
Pushes and pull requests to main trigger the workflow. The test job runs on the standard Ubuntu-hosted runner with read-only repository contents.
- Save .github/workflows/playwright.yml.
- Confirm that the VS Code Explorer shows playwright.yml inside .github/workflows.
- Complete the test job by adding these steps directly below the checkout action:
- uses: actions/setup-node@v7
with:
node-version: 24.20.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run smoke gate
run: npm run test:smoke
- name: Run full cross-browser suite
run: npm test
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report/
retention-days: 7
How does the quality gate work?
The job sets up Node.js 24.20.0. It restores the locked dependencies and installs browser system dependencies.
The smoke gate runs before the full cross-browser suite. The report upload still runs after a test failure unless the workflow was cancelled.
- Save .github/workflows/playwright.yml.
✔️ Awesome, I've got everything!
Your workflow now defines a smoke gate, full browser suite, and seven-day report artifact.
ⓧ I'd like to double check the full code
name: Playwright UI Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
test:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.20.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run smoke gate
run: npm run test:smoke
- name: Run full cross-browser suite
run: npm test
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report/
retention-days: 7
What should match?
Compare the triggers, permissions, runner, action versions, Node.js version, install commands, test commands, artifact path, and retention period with your saved workflow.
The remote repository should start empty. This avoids merge conflicts when your existing local project becomes its first commit.
- Return to GitHub in your browser.
- Select New repository from the repository menu.
- Enter agritrade-playwright-ui-framework as the repository name.
- Choose public visibility.
- Leave the README option unselected.
- Leave the .gitignore option unselected.
- Leave the license option unselected.
- Select Create repository.
Why use a public empty repository?
The empty repository accepts your existing history without an initial-file conflict. Standard GitHub-hosted runners are free for public repositories.
- Copy the new repository URL from GitHub.
- Record the copied URL here: your GitHub repository URL.
- Replace REMOTE-URL with your GitHub repository URL before running the commands.
- Complete browser authentication if Git requests it.
Before you publish the project, do you expect the workflow to start when main reaches GitHub?
- Initialize the repository, commit the framework, connect the remote, and push main by running these commands:
git init -b main
git add .
git commit -m "Build Playwright agri marketplace UI framework"
git remote add origin REMOTE-URL
git push -u origin main
What do the Git commands do?
The commands create the local main branch and stage the complete framework. They save the first commit and connect it to your GitHub repository.
The final push publishes the branch. That push matches the workflow trigger in playwright.yml.
Did the push fail?
Confirm that you replaced REMOTE-URL with the copied repository URL. Complete the browser authentication flow if Git requests access.
Ask for help diagnosing the repository connection or Git identity setup.
The first workflow run spends time installing dependencies and browser support on a clean runner. That setup work is expected before the tests begin.
Before you check the run, which workflow stages do you expect to finish before the report becomes downloadable?
- Open the public agritrade-playwright-ui-framework repository in GitHub.
- Select the Actions tab.
- Select the run named Playwright UI Tests.
- Wait for the test job to finish.
- Confirm that the completed run exposes the playwright-report artifact.
You should see a green Playwright UI Tests run. Its downloadable playwright-report artifact is retained for seven days.
That completes the quality gate. Your framework now proves its tagged journeys locally and on a clean CI runner.
Secret mission
Trace a Fulfillment Journey
Force a trace for the tagged Soybean fulfillment journey. Use the timeline to reconstruct every scoped action through the final Delivered assertion. Finish by rerunning the normal E2E suite.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your framework available, pause your work, or remove the project entirely. Your public repository uses standard GitHub-hosted runners, so the main project path costs $0.
Resources you used:
- Local project files in the agritrade-ui-automation folder, including package-lock.json plus the ignored test-results/ and playwright-report/ evidence directories.
- Public GitHub repository named agritrade-playwright-ui-framework, including its main branch, GitHub Actions workflow, workflow runs, and seven-day report artifacts.
Keep everything running
No action is needed. Choose this option if you want the framework ready for future testing or portfolio demonstrations.
- Keep the agritrade-ui-automation folder with its source code, local trace evidence, and HTML report.
- Keep the public agritrade-playwright-ui-framework repository available with its green quality gate.
- Expect the workflow to remain idle until a push or pull request triggers it.
- Expect each uploaded report artifact to remain available for seven days under the workflow configuration.
- Keep the Playwright-managed browser binaries available for other Playwright projects.
Pause - I'll come back to this later
Pause any local processes while preserving your files and repository. The workflow has no schedule, so it stays idle while you are away.
- Stop any active simulator or report process by pressing Ctrl+C in the Visual Studio Code PowerShell terminal.
- Leave the agritrade-ui-automation folder in place so its framework files and local evidence remain available.
- Leave the agritrade-playwright-ui-framework repository unchanged so the pushed framework remains available.
Delete - I don't want to use this again
This option gives you a clean start by removing the local project and public repository. Deletion is permanent, but your installed developer tools and GitHub account stay untouched.
Delete the local project:
- Close the Visual Studio Code window that has agritrade-ui-automation open.
- Locate the agritrade-ui-automation folder in the Windows file manager.
- Delete the entire agritrade-ui-automation folder.
- Confirm that the agritrade-ui-automation folder no longer appears in its previous location.
Delete the public repository:
- Return to the public agritrade-playwright-ui-framework repository on GitHub.
- Select Settings in the repository navigation.
- Select General in the settings sidebar.
- Scroll to Danger Zone at the bottom of the page.
- Select Delete this repository.
- Read the repository deletion warnings.
- Enter agritrade-playwright-ui-framework when GitHub asks you to confirm the repository name.
- Complete the repository deletion confirmation.
The repository page no longer displays your project after GitHub completes the deletion.
Nice Work!
Nice Work!
You did it! Your controlled AgriTrade marketplace now runs against an enterprise Playwright Test automation framework.
What you learned:
- Built a controlled AgriTrade marketplace with a visible buyer-to-delivery journey. The simulator gives every test stable listings with repeatable state.
- Turned a strict-mode locator failure into a maintainable Playwright Test framework. Listing-scoped locators identify the intended produce card. The Page Object Model expresses each workflow action in business language. Typed fixtures give every test an isolated interface.
- Created data-driven risk tiers for smoke, regression, and E2E journeys. Ran the same journeys across Chromium, Firefox, and WebKit. Published a green GitHub Actions quality gate with a downloadable HTML report.
- Secret Mission: Captured a fulfillment trace for the tagged Chromium E2E journey. Used its action timeline to reconstruct every listing-scoped action through the final Delivered assertion. Confirmed the suite stayed green without adding fixed waits.
Ready to quiz yourself?