Hotel Check-In Test Automation Framework
Build a Playwright test suite with Page Object Model and cross-device testing.
Introduction
30 Second Summary
Every time you tap "Check In" on a hotel app or request extra towels from your room, someone tested those flows before they reached your phone. That someone is a test automation engineer, and the framework they built is exactly what you are about to create.
In this project, you will build a complete Playwright test automation framework modeled on a hotel check-in module. You will create a mock hotel web page, write tests using the Page Object Model pattern, and run them across three device configurations with a single command.
What You'll Build
Run a single command in your terminal and watch 33 tests execute across Desktop Chrome, a Pixel 8, and an iPhone 15 Pro, all verifying that your hotel check-in flow works on every device.
By the end of this project, you'll have:
- A Page Object Model test architecture that keeps every page interaction in a dedicated class, so your tests read like plain English and a selector change only needs updating in one place.
- Cross-device test execution across Desktop Chrome, Pixel 8 (Android), and iPhone 15 Pro (iOS) that runs your entire suite with a single command.
- Data-driven amenity request tests that automatically generate test cases for each item and quantity, with results displayed in an interactive HTML test report grouped by device.
- Secret Mission: Add scroll behavior and share reservation tests for below-the-fold content on mobile viewports.
Are there any prerequisites?
No prior experience with test automation is required. This project walks you through installing every tool you need, including Node.js, VS Code, and Playwright.
Before We Start
You are about to build a test automation framework from scratch. Before writing any code, take a moment to lock in what you are building and why it matters for your goals.
Set Up Your Automation Environment
Every test automation project rests on three tools. Node.js runs JavaScript outside the browser. VS Code gives you an editor with a built-in terminal. Playwright is the test framework that automates browsers for you.
In this step, you will install all three, create your project folder, and scaffold a Playwright project that passes its built-in example test on the first run.
In this step, get ready to:
- Install Node.js and verify it with a version check.
- Install VS Code and create the project folder.
- Scaffold a Playwright project and run the default test.
Install Node.js
Node.js is the JavaScript runtime that powers Playwright. Playwright runs your test scripts through Node.js, so it needs to be installed first.
- Open https://nodejs.org in your browser.
- Download the LTS version (currently 22.x).
- Run the installer and accept the default settings.
After installation completes, open a terminal to verify it worked.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter.
- Check your Node.js version by running this command:
node --version
✔️ I see version 22 or higher
You are all set. Node.js is installed and ready for Playwright.
ⓧ I see an older version
Playwright requires Node.js 22.x or higher. The Node.js installer replaces your existing version automatically.
- Go to https://nodejs.org and download the LTS installer.
- Run the installer and accept the defaults.
- Close your current terminal, open a new one, and run node --version again.
- You should now see v22 or higher.
ⓧ Command not found
Node.js is not installed yet.
- Go to https://nodejs.org and download the LTS installer for your operating system.
- Run the installer and accept all default settings.
- Close your current terminal and open a brand-new one.
- Run node --version again to confirm it prints a version starting with v22 or higher.
Why a brand-new terminal?
Your terminal loads its PATH variable when it first opens. If you installed Node.js while the terminal was already running, it will not know about the new node command until you open a fresh window.
Still seeing an error?
Make sure you downloaded the LTS version, not the Current version. If the installer finished but the command still fails, restart your computer and try node --version in a new terminal.
Help me troubleshoot my Node.js installation.
Install VS Code and create the project folder
VS Code is the code editor you will use throughout this project. Its built-in terminal means you can edit files and run commands in the same window.
- Open https://code.visualstudio.com in your browser and download the installer for your operating system.
- Run the installer and accept the default settings.
Windows users: Add to PATH
During the Windows installer, check the box labeled Add to PATH when prompted. This lets you launch VS Code from any terminal later.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click Terminal in the top menu bar.
- Click New Terminal.
A terminal panel opens at the bottom of VS Code. Now create your project folder on the Desktop so you can find it easily.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir hotel-checkin-tests
cd hotel-checkin-tests
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the hotel-checkin-tests folder, and click Open.
Trust the folder
VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.
- Open a new terminal inside the project by clicking Terminal in the menu bar, then New Terminal.
The terminal prompt now shows you are inside the hotel-checkin-tests folder. You will run all commands from this terminal for the rest of the project.
Scaffold the Playwright project
Playwright provides a scaffold command that sets up your config file, test folder, and browser engines in one shot. The command is interactive, so it will ask a few questions before generating anything.
- Scaffold your Playwright project by running this command:
npm init playwright@latest
What does this command do?
npm init playwright@latest downloads the latest Playwright setup wizard and runs it. It creates a playwright.config.js file, a tests folder with an example test, and installs the Chromium, Firefox, and WebKit browser engines that Playwright uses to run tests.
The wizard asks four questions. Answer each one as follows.
- When asked to choose between TypeScript and JavaScript, select JavaScript.
- When asked for the name of your tests folder, press Enter to accept the default (tests).
- When asked to add a GitHub Actions workflow, select false.
- When asked to install Playwright browsers, press Enter to accept the default (yes).
Playwright now downloads browser engines for Chromium, Firefox, and WebKit. This may take a minute or two depending on your internet speed.
- Confirm the scaffold created your project files by running:
ls
You should see node_modules, package.json, package-lock.json, playwright.config.js, tests, and tests-examples listed in the output.
Don't see these files?
Make sure you ran npm init playwright@latest inside the hotel-checkin-tests folder. If the command failed partway through, delete any generated files and run it again.
Help me troubleshoot my Playwright scaffold.
Before you run the default test, what do you think will happen? Playwright has an example test file that navigates to a live website. Will all the tests pass on your first try?
- Run the default example test by running this command:
npx playwright test
You should see output that includes passed in the results. The scaffolded example test ran successfully against the browser engines Playwright installed.
Seeing test failures?
The default example test navigates to an external website, so it requires an internet connection. Make sure you are connected to the internet and try again.
If you see a browser-related error, the browser engines may not have downloaded fully. Run this command to reinstall them:
npx playwright install
Then run npx playwright test again.
Help me get my Playwright tests passing.
Your automation environment is ready. Next up, you will build the mock hotel check-in page that your tests will run against.
Build the Mock Hotel Check-In Page
Your automation environment is ready. Now you need something to actually test.
Automated tests need a target application to interact with. In this step, you will build a mock hotel check-in web page that mirrors the flows a real hotel app uses: checking in and out, requesting amenities, tapping quick-action buttons, and viewing reservation details. You will also create a simple server to host the page so Playwright can reach it later.
In this step, get ready to:
- Create a mock hotel check-in web page with interactive buttons, amenity controls, and reservation details.
- Build a local Node.js server to serve the page.
- Verify the page works in your browser.
Create the mock hotel web page
Every test suite needs a real target to point at. Instead of relying on a third-party website that could change at any time, you will build your own mock page that stays exactly the way you designed it.
- Create the mock-app folder inside your hotel-checkin-tests project by running this command in the VS Code terminal:
mkdir mock-app
- Confirm the folder was created by running:
ls
You should see mock-app listed alongside node_modules, package.json, and the other scaffolded files.
- In the VS Code sidebar, right-click the mock-app folder.
- Click New File.
- Name the file index.html and press Enter.
The file opens in the editor. You will now build the page section by section, starting with the HTML head and CSS styles.
- Paste the following into mock-app/index.html to set up the HTML skeleton and the first group of CSS styles:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hotel Check-In Module</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f5f5f5; }
.app-header { background: #1a1a2e; color: white; padding: 16px; text-align: center; }
.app-header h1 { font-size: 20px; }
.guest-info { background: white; margin: 16px; padding: 16px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.guest-info h2 { font-size: 18px; margin-bottom: 8px; }
.guest-info p { color: #666; font-size: 14px; }
.status-badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: bold; margin-top: 8px; }
.status-badge.pending { background: #fff3cd; color: #856404; }
.status-badge.checked-in { background: #d4edda; color: #155724; }
.status-badge.checked-out { background: #f8d7da; color: #721c24; }
.action-buttons { margin: 16px; display: grid; gap: 12px; }
.btn { padding: 14px; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; text-align: center; }
.btn-primary { background: #e94560; color: white; }
.btn-secondary { background: #16213e; color: white; }
.btn-outline { background: white; color: #16213e; border: 2px solid #16213e; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.quick-actions { margin: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.quick-action-btn { display: flex; flex-direction: column; align-items: center; padding: 16px; background: white; border: 1px solid #e0e0e0; border-radius: 8px; cursor: pointer; font-size: 14px; }
.quick-action-btn span { margin-top: 8px; }
What do these styles do?
Each CSS rule targets a section of the hotel check-in page you are about to build. The .status-badge classes use colour changes to show whether a guest is pending, checked in, or checked out. The .btn:disabled style dims buttons that should not be clickable.
- Directly below the last line you just pasted, add the remaining CSS styles and the opening <body> tag:
.amenity-section { background: white; margin: 16px; padding: 16px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.amenity-section h3 { margin-bottom: 12px; }
.amenity-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid #eee; }
.amenity-item:last-child { border-bottom: none; }
.quantity-control { display: flex; align-items: center; gap: 12px; }
.quantity-btn { width: 32px; height: 32px; border-radius: 50%; border: 1px solid #ccc; background: white; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; }
.quantity-value { font-size: 16px; min-width: 20px; text-align: center; }
.submit-request-btn { width: 100%; margin-top: 16px; padding: 12px; background: #e94560; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; }
.reservation-details { background: white; margin: 16px; padding: 16px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.reservation-details h3 { margin-bottom: 12px; }
.detail-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
.detail-label { color: #666; }
.detail-value { font-weight: bold; }
.share-btn { width: 100%; margin-top: 12px; padding: 12px; background: #16213e; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.active { display: flex; }
.modal { background: white; padding: 24px; border-radius: 12px; max-width: 320px; width: 90%; text-align: center; }
.modal h3 { margin-bottom: 12px; }
.modal p { color: #666; margin-bottom: 16px; }
.modal .btn { width: 100%; margin-top: 8px; }
.success-message { display: none; background: #d4edda; color: #155724; padding: 12px; margin: 16px; border-radius: 8px; text-align: center; }
.success-message.visible { display: block; }
</style>
</head>
<body>
- Save mock-app/index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Your file now has a complete set of styles and an empty body. Next, you will add the visible page content.
- Inside the <body> tag, paste the page header, guest information, action buttons, and quick-action buttons:
<header class="app-header"><h1>Hotel Stay</h1></header>
<section class="guest-info" data-testid="guest-info">
<h2 data-testid="guest-name">Welcome, Alex Johnson</h2>
<p>Room 412 - King Suite</p>
<p>Aug 24 - Aug 27, 2026</p>
<div class="status-badge pending" data-testid="status-badge">Pending Check-In</div>
</section>
<div class="action-buttons">
<button class="btn btn-primary" data-testid="checkin-btn" onclick="handleCheckIn()">Check In</button>
<button class="btn btn-secondary" data-testid="checkout-btn" disabled onclick="handleCheckOut()">Check Out</button>
</div>
<div id="success-msg" class="success-message" data-testid="success-message"></div>
<div class="quick-actions">
<button class="quick-action-btn" data-testid="call-btn" onclick="showModal('call-modal')"><span>Call Front Desk</span></button>
<button class="quick-action-btn" data-testid="chat-btn" onclick="showModal('chat-modal')"><span>Chat</span></button>
<button class="quick-action-btn" data-testid="food-btn" onclick="showModal('food-modal')"><span>Order Food</span></button>
<button class="quick-action-btn" data-testid="shuttle-btn" onclick="showModal('shuttle-modal')"><span>Shuttle</span></button>
</div>
What are data-testid attributes?
Every interactive element has a data-testid attribute like data-testid="checkin-btn". These are stable hooks that your Playwright tests will use to find elements on the page. Unlike CSS class names or text content, test IDs never change when the design evolves.
This is a best practice in production apps. QA engineers work with developers to add data-testid attributes so automated tests stay reliable even when the UI is redesigned.
- Below the quick actions, paste the amenity request section with quantity controls for Towels, Hair Brush, and Extra Pillows:
<section class="amenity-section" data-testid="amenity-section">
<h3>Request Amenities</h3>
<div class="amenity-item" data-testid="amenity-towels">
<span>Towels</span>
<div class="quantity-control">
<button class="quantity-btn" data-testid="towels-decrease" onclick="updateQuantity('towels', -1)">-</button>
<span class="quantity-value" data-testid="towels-quantity">0</span>
<button class="quantity-btn" data-testid="towels-increase" onclick="updateQuantity('towels', 1)">+</button>
</div>
</div>
<div class="amenity-item" data-testid="amenity-hairbrush">
<span>Hair Brush</span>
<div class="quantity-control">
<button class="quantity-btn" data-testid="hairbrush-decrease" onclick="updateQuantity('hairbrush', -1)">-</button>
<span class="quantity-value" data-testid="hairbrush-quantity">0</span>
<button class="quantity-btn" data-testid="hairbrush-increase" onclick="updateQuantity('hairbrush', 1)">+</button>
</div>
</div>
<div class="amenity-item" data-testid="amenity-pillows">
<span>Extra Pillows</span>
<div class="quantity-control">
<button class="quantity-btn" data-testid="pillows-decrease" onclick="updateQuantity('pillows', -1)">-</button>
<span class="quantity-value" data-testid="pillows-quantity">0</span>
<button class="quantity-btn" data-testid="pillows-increase" onclick="updateQuantity('pillows', 1)">+</button>
</div>
</div>
<button class="submit-request-btn" data-testid="submit-amenity-btn" onclick="submitAmenityRequest()">Submit Request</button>
</section>
- Save mock-app/index.html.
- In your file manager (Finder on macOS, File Explorer on Windows), navigate to your hotel-checkin-tests/mock-app folder on your Desktop.
- Double-click index.html to open it in your default browser.
You should see a dark header reading "Hotel Stay", the guest info card for Alex Johnson, Check In and Check Out buttons, and four quick-action buttons. The amenity section with Towels, Hair Brush, and Extra Pillows appears below.
See a blank page?
Make sure you saved the file after each paste. Check that the <body> tag from the second CSS chunk appears right before the header HTML. If the page still looks wrong, compare the opening <!DOCTYPE html> line through the </section> closing tag of the amenity section against what you have pasted so far.
help me debug my blank index.html page
Two more sections to go. The reservation details and the JavaScript that powers the interactions come next.
- Below the amenity section, paste the reservation details, all five modals, the JavaScript, and the closing tags:
<section class="reservation-details" data-testid="reservation-details">
<h3>Reservation Details</h3>
<div class="detail-row"><span class="detail-label">Confirmation #</span><span class="detail-value" data-testid="confirmation-number">HTL-2026-98421</span></div>
<div class="detail-row"><span class="detail-label">Room Type</span><span class="detail-value">King Suite</span></div>
<div class="detail-row"><span class="detail-label">Check-In</span><span class="detail-value">Aug 24, 2026</span></div>
<div class="detail-row"><span class="detail-label">Check-Out</span><span class="detail-value">Aug 27, 2026</span></div>
<div class="detail-row"><span class="detail-label">Guests</span><span class="detail-value">2 Adults</span></div>
<div class="detail-row"><span class="detail-label">Rate</span><span class="detail-value">$249/night</span></div>
<button class="share-btn" data-testid="share-btn" onclick="shareReservation()">Share Reservation</button>
</section>
Why a Share Reservation button?
Guests often need to share their booking confirmation with travel companions. This button triggers a modal that displays the confirmation number and room details. You will write tests against this flow later.
- Directly below the reservation section, paste the five modal overlays that appear when the learner taps each quick-action button or the Share button:
<div class="modal-overlay" id="call-modal" data-testid="call-modal"><div class="modal"><h3>Call Front Desk</h3><p>You will be connected to the front desk.</p><button class="btn btn-primary" onclick="hideModal('call-modal')">Call Now</button><button class="btn btn-outline" onclick="hideModal('call-modal')">Cancel</button></div></div>
<div class="modal-overlay" id="chat-modal" data-testid="chat-modal"><div class="modal"><h3>Chat with Us</h3><p>Start a conversation with our team.</p><button class="btn btn-primary" onclick="hideModal('chat-modal')">Start Chat</button><button class="btn btn-outline" onclick="hideModal('chat-modal')">Cancel</button></div></div>
<div class="modal-overlay" id="food-modal" data-testid="food-modal"><div class="modal"><h3>Order Food</h3><p>Browse our room service menu.</p><button class="btn btn-primary" onclick="hideModal('food-modal')">View Menu</button><button class="btn btn-outline" onclick="hideModal('food-modal')">Cancel</button></div></div>
<div class="modal-overlay" id="shuttle-modal" data-testid="shuttle-modal"><div class="modal"><h3>Airport Shuttle</h3><p>Schedule your airport shuttle for checkout day.</p><button class="btn btn-primary" onclick="hideModal('shuttle-modal')">Schedule</button><button class="btn btn-outline" onclick="hideModal('shuttle-modal')">Cancel</button></div></div>
<div class="modal-overlay" id="share-modal" data-testid="share-modal"><div class="modal"><h3>Share Reservation</h3><p>Reservation details copied to clipboard!</p><p data-testid="share-content">Confirmation: HTL-2026-98421 | Room 412 - King Suite | Aug 24-27, 2026</p><button class="btn btn-primary" onclick="hideModal('share-modal')">Done</button></div></div>
How do the modals work?
Each modal starts hidden with display: none in the CSS. When a button calls showModal(), it adds the .active class, which switches the overlay to display: flex and makes it visible. Your Playwright tests will check for this visibility change.
- Below the modals, paste the JavaScript that powers all the page interactions, then the closing tags:
<script>
const quantities = { towels: 0, hairbrush: 0, pillows: 0 };
function handleCheckIn() { const badge = document.querySelector('[data-testid="status-badge"]'); badge.textContent = 'Checked In'; badge.className = 'status-badge checked-in'; document.querySelector('[data-testid="checkin-btn"]').disabled = true; document.querySelector('[data-testid="checkout-btn"]').disabled = false; showSuccess('Successfully checked in! Welcome to your stay.'); }
function handleCheckOut() { const badge = document.querySelector('[data-testid="status-badge"]'); badge.textContent = 'Checked Out'; badge.className = 'status-badge checked-out'; document.querySelector('[data-testid="checkout-btn"]').disabled = true; document.querySelector('[data-testid="checkin-btn"]').disabled = true; showSuccess('Successfully checked out. Thank you for your stay!'); }
function updateQuantity(item, change) { quantities[item] = Math.max(0, quantities[item] + change); document.querySelector('[data-testid="' + item + '-quantity"]').textContent = quantities[item]; }
function submitAmenityRequest() { const requested = Object.entries(quantities).filter(function(entry) { return entry[1] > 0; }); if (requested.length === 0) { showSuccess('Please select at least one amenity.'); return; } const items = requested.map(function(entry) { return entry[1] + 'x ' + entry[0]; }).join(', '); showSuccess('Amenity request submitted: ' + items); Object.keys(quantities).forEach(function(key) { quantities[key] = 0; document.querySelector('[data-testid="' + key + '-quantity"]').textContent = '0'; }); }
function showModal(id) { document.getElementById(id).classList.add('active'); }
function hideModal(id) { document.getElementById(id).classList.remove('active'); }
function shareReservation() { showModal('share-modal'); }
function showSuccess(message) { var el = document.getElementById('success-msg'); el.textContent = message; el.classList.add('visible'); setTimeout(function() { el.classList.remove('visible'); }, 3000); }
</script>
</body>
</html>
What does the JavaScript do?
The handleCheckIn() function changes the status badge to "Checked In", disables the Check In button, and enables Check Out. The updateQuantity() function tracks amenity counts and prevents them from going below zero.
The submitAmenityRequest() function reads the current quantities, displays a success message listing what was requested, and resets all counts to zero. All of these are the exact behaviors your Playwright tests will verify.
- Save mock-app/index.html.
- Switch back to the browser tab where you opened index.html earlier and refresh the page.
You should now see the full page: reservation details at the bottom with confirmation number HTL-2026-98421, and a Share Reservation button.
✔️ Awesome, I've got everything!
Great. Make sure you have saved mock-app/index.html.
ⓧ I'd like to double check the full code
Your mock-app/index.html is built from seven code blocks you pasted above. Scroll up and confirm each one is present in this order:
- HTML head and first CSS styles (from <!DOCTYPE html> through .quick-action-btn span).
- Remaining CSS styles (from .amenity-section through <body>).
- Header, guest info, action buttons, and quick actions.
- Amenity request section with three amenity items.
- Reservation details section.
- Five modal overlays (call, chat, food, shuttle, share).
- JavaScript <script> block through </html>.
Create a local server to serve the page
Playwright navigates to URLs, not local files. You need a server that hosts your HTML page at a web address so Playwright can reach it during test runs.
- In the VS Code sidebar, right-click the hotel-checkin-tests folder (the project root, not mock-app).
- Click New File.
- Name the file server.js and press Enter.
- Paste the following code into server.js:
// Load built-in modules for HTTP, file system, and path handling
const http = require('http');
const fs = require('fs');
const path = require('path');
// Create a server that serves the mock hotel page
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, 'mock-app', 'index.html');
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
});
});
// Start listening on port 3000
server.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
What does this server do?
This script uses Node.js built-in http module to create a web server. When any request comes in, it reads mock-app/index.html from disk and sends it back as the response.
The server runs on port 3000. Later, your Playwright config will point tests at http://localhost:3000 so they load this exact page.
- Save server.js.
✔️ Awesome, I've got everything!
Great. Confirm server.js is saved and you see it in the project root alongside package.json.
ⓧ I'd like to double check the full code
// Load built-in modules for HTTP, file system, and path handling
const http = require('http');
const fs = require('fs');
const path = require('path');
// Create a server that serves the mock hotel page
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, 'mock-app', 'index.html');
fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404);
res.end('Not found');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
});
});
// Start listening on port 3000
server.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
Start the server and verify the page
The HTML file and the server are ready. Time to start the server and confirm everything works through the browser.
- Start the server by running this command in your VS Code terminal:
node server.js
You should see this output in your terminal:
Server running at http://localhost:3000
Before you open the browser, think about what you expect to see at that URL. Which sections of the page do you think will appear?
- Open your browser.
- Navigate to http://localhost:3000.
You should see the complete hotel check-in page. "Welcome, Alex Johnson" appears at the top, followed by a Check In button, four quick-action buttons, the amenity request section, and the reservation details at the bottom.
- Click the Check In button.
The status badge changes from "Pending Check-In" (yellow) to "Checked In" (green). The Check In button dims out and the Check Out button becomes active.
- Tap the + button next to Towels a few times to increase the quantity.
- Click Submit Request.
A green success message appears showing your amenity request with the quantity you selected. The count resets to zero afterward.
- Click the Call Front Desk button.
A modal overlay slides in with "Call Front Desk" and a Call Now button. Click Cancel to dismiss it.
Page not loading at localhost:3000?
Make sure the terminal still shows "Server running at http://localhost:3000" with no errors. If you see an error about the port being in use, another process is already using port 3000. Close any other terminals or applications that might be running a server.
If you see "Not found" in the browser, check that mock-app/index.html exists in the right location. The mock-app folder must be directly inside your hotel-checkin-tests project folder.
help me debug my server not serving the hotel page
- Switch back to the VS Code terminal and press Ctrl+C to stop the server.
You should see the terminal return to the command prompt. The server is no longer running.
Why stop the server?
You do not need to keep the server running manually. In the next step, you will configure Playwright to start and stop the server automatically before each test run using the webServer config option.
Your mock hotel check-in page is live and every button, modal, and quantity control works. Next up, you will write your first Page Object class and Playwright tests to automate everything you just clicked through by hand.
Write Your First Page Object and Test
Your mock hotel check-in page is live and interactive. Now you need to write automated tests against it.
Scattering raw selectors like [data-testid="checkin-btn"] across every test file creates brittle, unmaintainable code. The Page Object Model pattern solves this by wrapping all page interactions in a dedicated class. Your tests read like plain English, and a selector change only needs updating in one place.
In this step, get ready to:
- Create a CheckInPage page object class with locators and methods.
- Write 7 tests covering guest info, check-in, check-out, and quick action modals.
- Configure Playwright to run tests against your local server.
Create the CheckInPage page object
A page object is a JavaScript class that represents one page (or section) of your application. It stores all the locators and actions in a single file, so your test files never need to know how the HTML is structured.
Why Page Object Model instead of inline selectors?
Imagine your hotel app renames data-testid="checkin-btn" to data-testid="check-in-button". With inline selectors scattered across 20 test files, you would need to update every single one.
With a page object, you update one line in one file, and all 20 tests keep passing. That is why QA teams treat the Page Object Model as an industry standard.
- Create a pages folder in your project root by running this command in the VS Code terminal:
mkdir pages
- Confirm the folder was created by running:
ls
You should see pages listed alongside mock-app, tests, server.js, and playwright.config.js.
- In the VS Code Explorer sidebar, right-click the pages folder.
- Click New File and name it CheckInPage.js.
- Start building the page object by pasting the class constructor with all its locators:
class CheckInPage {
// Store the page object and create locators for every testable element
constructor(page) {
this.page = page;
// Guest info locators
this.guestName = page.getByTestId('guest-name');
this.statusBadge = page.getByTestId('status-badge');
// Check-in and check-out button locators
this.checkInButton = page.getByTestId('checkin-btn');
this.checkOutButton = page.getByTestId('checkout-btn');
this.successMessage = page.getByTestId('success-message');
// Quick action button locators
this.callButton = page.getByTestId('call-btn');
this.chatButton = page.getByTestId('chat-btn');
this.foodButton = page.getByTestId('food-btn');
this.shuttleButton = page.getByTestId('shuttle-btn');
// Modal overlay locators
this.callModal = page.getByTestId('call-modal');
this.chatModal = page.getByTestId('chat-modal');
this.foodModal = page.getByTestId('food-modal');
this.shuttleModal = page.getByTestId('shuttle-modal');
}
What does this code do?
The constructor receives Playwright's page object and immediately creates locators for every element your tests will interact with.
- page.getByTestId('guest-name') finds the element whose data-testid attribute matches. This is Playwright's recommended way to target elements because test IDs stay stable even when CSS classes or text change.
- Each locator is stored as a property (like this.checkInButton) so tests can reference it by name instead of repeating the selector string.
- Add the action methods and the export statement below the constructor's closing brace:
// Navigate to the check-in page
async goto() {
await this.page.goto('/');
}
async checkIn() {
await this.checkInButton.click();
}
async checkOut() {
await this.checkOutButton.click();
}
// Quick action button methods
async tapCallButton() {
await this.callButton.click();
}
async tapChatButton() {
await this.chatButton.click();
}
async tapFoodButton() {
await this.foodButton.click();
}
async tapShuttleButton() {
await this.shuttleButton.click();
}
}
module.exports = { CheckInPage };
What does this code do?
- goto() navigates the browser to the root URL. Tests call this instead of hard-coding page.goto('/') themselves.
- checkIn() and checkOut() click the corresponding buttons. Each method wraps a single user action so your test reads checkInPage.checkIn() instead of a raw selector.
- module.exports = { CheckInPage } exports the class so your test files can import it with require.
- Save CheckInPage.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- In the Explorer sidebar, expand the pages folder. You should see CheckInPage.js listed inside it.
Don't see the file?
- Make sure you created the file inside the pages folder, not in the project root. The path should be pages/CheckInPage.js.
- Check that the filename is exactly CheckInPage.js with a capital C, I, and P. JavaScript file names are case-sensitive.
Why isn't my CheckInPage.js file appearing in the pages folder?
✔️ Awesome, I've got everything!
Great. Make sure you have saved CheckInPage.js before moving on.
ⓧ I'd like to double check the full code
Your pages/CheckInPage.js has two sections you built above. Scroll up and confirm both are present, in order:
- The class declaration and constructor with 14 locator properties (guestName through shuttleModal).
- Seven methods (goto, checkIn, checkOut, and the four tap...Button methods) followed by the module.exports line at the bottom.
Write check-in flow tests
With the page object ready, you can write tests that use its clean methods. Each test will create a fresh CheckInPage instance and call its actions instead of touching any selectors directly.
- First, delete the scaffolded example test. In the VS Code terminal, run:
rm tests/example.spec.js
- In the Explorer sidebar, right-click the tests folder.
- Click New File and name it checkin.spec.js.
- Start the test file by pasting the imports, setup, and first two tests:
const { test, expect } = require('@playwright/test');
const { CheckInPage } = require('../pages/CheckInPage');
test.describe('Hotel Check-In Module', () => {
let checkInPage;
// Create a fresh CheckInPage and navigate before each test
test.beforeEach(async ({ page }) => {
checkInPage = new CheckInPage(page);
await checkInPage.goto();
});
test('should display guest information', async () => {
// Verify the page loads with correct guest name and status
await expect(checkInPage.guestName).toHaveText('Welcome, Alex Johnson');
await expect(checkInPage.statusBadge).toHaveText('Pending Check-In');
});
test('should complete check-in flow', async () => {
// Status starts as pending
await expect(checkInPage.statusBadge).toHaveText('Pending Check-In');
await checkInPage.checkIn();
// After check-in, status updates and buttons toggle
await expect(checkInPage.statusBadge).toHaveText('Checked In');
await expect(checkInPage.checkInButton).toBeDisabled();
await expect(checkInPage.checkOutButton).toBeEnabled();
await expect(checkInPage.successMessage).toContainText('Successfully checked in');
});
What does this code do?
- test.describe() groups related tests under a shared label. All 7 tests belong to the "Hotel Check-In Module" group.
- test.beforeEach() runs before every single test. It creates a new CheckInPage and navigates to the page, so each test starts from a clean state.
- The check-in flow test asserts multiple things: the status badge text changes, the check-in button disables, the check-out button enables, and a success message appears. Each assertion uses the page object's named properties.
- Add the remaining five tests below the check-in flow test. These cover check-out and the four quick action modals:
test('should complete check-out flow', async () => {
// Must check in first, then check out
await checkInPage.checkIn();
await checkInPage.checkOut();
// Status changes and check-out button disables
await expect(checkInPage.statusBadge).toHaveText('Checked Out');
await expect(checkInPage.checkOutButton).toBeDisabled();
await expect(checkInPage.successMessage).toContainText('Successfully checked out');
});
test('should open call front desk modal', async () => {
await checkInPage.tapCallButton();
await expect(checkInPage.callModal).toBeVisible();
});
test('should open chat modal', async () => {
await checkInPage.tapChatButton();
await expect(checkInPage.chatModal).toBeVisible();
});
test('should open order food modal', async () => {
await checkInPage.tapFoodButton();
await expect(checkInPage.foodModal).toBeVisible();
});
test('should open shuttle modal', async () => {
await checkInPage.tapShuttleButton();
await expect(checkInPage.shuttleModal).toBeVisible();
});
});
What does this code do?
- The check-out test calls checkIn() first because the mock app only enables the check-out button after check-in. This mirrors a real guest flow.
- Each modal test clicks the corresponding quick action button and asserts the modal overlay becomes visible. The toBeVisible() assertion checks the element's computed CSS. The modal starts with display: none and switches to display: flex when opened.
- Save checkin.spec.js.
- Confirm the test file exists by running:
ls tests/
You should see checkin.spec.js listed. The old example.spec.js should be gone.
Still see example.spec.js?
- If the delete command failed, make sure you spelled the filename exactly as tests/example.spec.js. You can also right-click it in the VS Code Explorer and select Delete.
I can't delete the example spec file from the tests folder. What should I try?
✔️ Awesome, I've got everything!
Great. Make sure you saved checkin.spec.js before moving on.
ⓧ I'd like to double check the full code
Your tests/checkin.spec.js has two sections you pasted above. Scroll up and confirm both are present, in order:
- The imports, test.describe wrapper, test.beforeEach hook, and the first two tests (display guest info, complete check-in).
- The five remaining tests (check-out flow, call modal, chat modal, food modal, shuttle modal) and the closing }); for the describe block.
Configure Playwright and run the tests
The scaffolded Playwright config does not know about your local server or your project structure. You need to replace it with a custom configuration that points to your test files, sets the base URL, and auto-starts server.js before each test run.
- Open playwright.config.js in the VS Code editor by clicking it in the Explorer sidebar.
- Select all the existing content with Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
- Replace the config with your custom configuration by pasting this code:
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
// Point to the tests directory
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
// Generate an HTML report after each run
reporter: 'html',
use: {
// Base URL for all page.goto('/') calls
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
],
// Auto-start the server before tests run
webServer: {
command: 'node server.js',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
What does this config do?
- baseURL: 'http://localhost:3000' means every page.goto('/') call in your tests automatically navigates to http://localhost:3000/.
- The webServer block tells Playwright to start your server automatically before tests run and shut it down after. You no longer need to manually run node server.js in a separate terminal.
- The projects array defines which browsers to test on. Right now it only has Desktop Chrome.
- Save playwright.config.js.
✔️ Awesome, I've got everything!
Great. Double check you saved the file before running the tests.
ⓧ I'd like to double check the full code
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'node server.js',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Everything is in place. Before you run the tests, think about this: you have 7 test cases and 1 browser project configured. How many total test results do you expect to see?
- Run your full test suite by entering this command:
npx playwright test
Playwright auto-starts your server, runs all 7 tests on Desktop Chrome, then shuts the server down. You should see "7 passed" in the terminal output.
Seeing test failures or errors?
- If you see a connection error or timeout, make sure no other process is using port 3000. Stop any running node server.js process with Ctrl+C first. The webServer config handles starting the server for you.
- If you see "Cannot find module '../pages/CheckInPage'", check that your file is at pages/CheckInPage.js (not tests/pages/CheckInPage.js). The pages folder should be at the project root, next to tests.
- If a specific test fails with a locator error, double check that the data-testid strings in CheckInPage.js match the ones in index.html exactly.
My Playwright tests are failing. Help me debug the errors.
- Open the interactive HTML test report by running:
npx playwright show-report
Your browser opens with the Playwright report showing all 7 tests in green under the Desktop Chrome project. Click any test name to see the steps it ran.
What is the HTML report?
Playwright generates this report after every test run because you set reporter: 'html' in the config. It shows pass/fail status, duration, and detailed traces for any failures. QA teams share these reports with developers as proof that features work correctly.
You have a clean, production-structured test suite with 7 passing tests. Every test reads like a guest's real journey through the hotel app. Next up, you will expand this suite to run across mobile devices without changing a single test.
Add Cross-Device Mobile Testing
Your 7 check-in tests all pass on Desktop Chrome. That proves the page logic works in a full-sized browser window.
Hotel apps serve guests on their phones. An Android user on a Pixel 8 sees a different viewport size and uses a different browser engine than an iPhone user on Safari. Without testing across these devices, you are only covering one-third of your user base.
In this step, get ready to:
- Add Pixel 8 and iPhone 15 Pro device emulation projects to your Playwright config.
- Run the full test suite across all three devices and view the HTML report grouped by device.
Add Pixel 8 and iPhone 15 Pro device projects
Playwright ships with a built-in devices object that contains pre-configured profiles for dozens of real phones and tablets. Each profile sets the correct viewport size, user agent string, and browser engine for that device.
- In VS Code, switch back to playwright.config.js.
- Find the projects array. It currently has one entry for Desktop Chrome:
projects: [
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
],
- Add two new device entries after the Desktop Chrome closing brace }, by copying this code below it:
// Android device - runs on Chromium browser engine
{
name: 'Pixel 8',
use: { ...devices['Pixel 8'] },
},
// iOS device - runs on WebKit (Safari) browser engine
{
name: 'iPhone 15 Pro',
use: { ...devices['iPhone 15 Pro'] },
},
What do these device entries do?
The spread syntax ...devices['Pixel 8'] loads Playwright's built-in profile for the Pixel 8. This sets the viewport to 412x915 pixels, configures an Android user agent, and runs tests on Chromium.
The ...devices['iPhone 15 Pro'] entry does the same for iOS. It uses WebKit, Safari's browser engine, so you catch rendering differences that only appear on Apple devices.
- Save playwright.config.js.
Seeing a syntax error when you save?
- Check that the Desktop Chrome entry ends with a comma after its closing brace: },. A missing comma between project entries causes a parse error.
- Make sure the device names are spelled exactly as shown: 'Pixel 8' and 'iPhone 15 Pro'. Playwright's device lookup is case-sensitive and space-sensitive.
- Confirm the closing square bracket ], for the projects array is still intact after your new entries.
- Why is my Playwright config throwing a syntax error?
✔️ Awesome, I've got everything!
Make sure you have saved playwright.config.js. Your projects array should now contain three entries: Desktop Chrome, Pixel 8, and iPhone 15 Pro.
ⓧ I'd like to double check the full code
Your projects array in playwright.config.js should look exactly like this:
projects: [
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Pixel 8',
use: { ...devices['Pixel 8'] },
},
{
name: 'iPhone 15 Pro',
use: { ...devices['iPhone 15 Pro'] },
},
],
The rest of the config (the use block, webServer, and the settings at the top) should be unchanged from Step 3.
Run the cross-device suite
Your config now targets three devices. Before you run the tests, think about how many total test runs you expect. You have 7 tests and 3 device configurations.
- Run the full test suite by running this command in your terminal:
npx playwright test
Playwright executes all 7 tests on Desktop Chrome, then all 7 on the Pixel 8 viewport, then all 7 on the iPhone 15 Pro viewport. You should see 21 passed in the terminal output.
Why 21 and not 7?
Each entry in the projects array creates a separate test run for every test file. 7 tests multiplied by 3 device projects equals 21 total runs. You did not duplicate a single test file. Playwright handles the multiplication for you.
- Open the interactive HTML report by running:
npx playwright show-report
The report opens in your browser. You should see all 21 results listed, grouped by device project: Desktop Chrome, Pixel 8, and iPhone 15 Pro.
Why not test on real phones instead?
Device emulation renders the page at the exact viewport dimensions and browser engine of each device. For layout, responsive behavior, and JavaScript execution, this catches the same bugs you would find on a physical phone.
Real device labs are useful for testing hardware features like cameras or GPS. For UI flow testing like check-in buttons and amenity requests, emulation gives you the coverage without needing a rack of phones.
Not seeing 21 passed?
- If you see fewer than 21 runs, check that your projects array has exactly three entries. A missing entry means Playwright skipped that device.
- If tests fail on iPhone 15 Pro, Playwright needs the WebKit browser installed. Run npx playwright install to install any missing browser engines, then re-run the tests.
- If you see a server error, make sure no other process is already using port 3000. Close any running node server.js instances with Ctrl+C first.
- Why are my Playwright tests failing on mobile device emulation?
Your test suite now covers desktop, Android, and iOS in a single command. Next up, you will build data-driven tests that automatically generate test cases for every amenity on the check-in page.
Test Amenity Requests with Data-Driven Tests
Your 21 check-in tests cover the guest journey, but the amenity request section is completely untested. Guests request towels, hair brushes, and extra pillows through the same flow: select an amenity, tap the quantity buttons, and submit.
Writing a separate test for each amenity would mean duplicating the same logic over and over. Data-driven testing solves this. You define the test data in an array and let Playwright loop through each combination automatically.
In this step, get ready to:
- Create an AmenityRequestPage page object for the amenity request section.
- Write data-driven tests that auto-generate test cases from an array of amenities.
- Run the full suite and verify 33 tests pass across all three devices.
Create the AmenityRequestPage page object
Just like CheckInPage wraps the check-in flow, you need a dedicated Page Object Model class for the amenity section. This class will handle dynamic locators that target any amenity by name, so one method works for towels, hair brushes, and pillows alike.
- Create a new file called AmenityRequestPage.js inside the pages folder.
- Add the class declaration, constructor, navigation method, and the quantity increase method by pasting the following code:
class AmenityRequestPage {
constructor(page) {
this.page = page;
// Locator for the submit button in the amenity section
this.submitButton = page.getByTestId('submit-amenity-btn');
// Locator for the success/error message banner
this.successMessage = page.getByTestId('success-message');
}
// Navigate to the hotel check-in page
async goto() {
await this.page.goto('/');
}
// Click the + button for a specific amenity a set number of times
async increaseQuantity(amenity, times) {
const increaseBtn = this.page.getByTestId(`${amenity}-increase`);
for (let i = 0; i < times; i++) {
await increaseBtn.click();
}
}
What does this code do?
The constructor stores locators for the submit button and the success message, just like CheckInPage does for its elements.
The increaseQuantity(amenity, times) method is the interesting one. It builds the test ID dynamically using a template literal. Pass in 'towels' and it targets towels-increase. Pass in 'hairbrush' and it targets hairbrush-increase. One method, any amenity.
- Add the remaining three methods directly below the increaseQuantity method, then close the class and export it:
// Click the - button for a specific amenity
async decreaseQuantity(amenity) {
await this.page.getByTestId(`${amenity}-decrease`).click();
}
// Return the quantity display locator for a specific amenity
getQuantity(amenity) {
return this.page.getByTestId(`${amenity}-quantity`);
}
// Click the submit request button
async submitRequest() {
await this.submitButton.click();
}
}
module.exports = { AmenityRequestPage };
What do these methods do?
- decreaseQuantity(amenity) clicks the minus button for a given amenity. It uses the same dynamic test ID pattern as increaseQuantity.
- getQuantity(amenity) returns the locator for the quantity display (not the value itself). This lets your tests assert on the displayed number using Playwright's built-in matchers.
- submitRequest() clicks the submit button to send the amenity request.
- Save pages/AmenityRequestPage.js.
- Confirm both page object files exist by running:
ls pages/
You should see AmenityRequestPage.js and CheckInPage.js listed side by side.
Only see CheckInPage.js?
- Make sure you created the file inside the pages folder, not the project root.
- Check the filename is exactly AmenityRequestPage.js with a capital A, R, and P.
Help me figure out why my AmenityRequestPage.js file is missing from the pages folder.
✔️ Awesome, I've got everything!
Great. Make sure you have saved pages/AmenityRequestPage.js before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm both chunks are present in your file, in this order:
- Chunk 1: The class AmenityRequestPage declaration, constructor, goto(), and increaseQuantity().
- Chunk 2: decreaseQuantity(), getQuantity(), submitRequest(), the closing } of the class, and the module.exports line.
Write data-driven amenity tests
The core idea behind data-driven testing is simple: define your test scenarios as data, then loop through them to generate tests automatically. Instead of copying and pasting the same test logic for every amenity, you write it once and feed in different inputs.
- Create a new file called amenity-request.spec.js inside the tests folder.
- Add the imports, data array, test setup, and the data-driven loop by pasting the following code:
const { test, expect } = require('@playwright/test');
const { AmenityRequestPage } = require('../pages/AmenityRequestPage');
// Test data: each entry generates its own test automatically
const amenities = [
{ name: 'towels', quantity: 3 },
{ name: 'hairbrush', quantity: 1 },
];
test.describe('Amenity Requests', () => {
let amenityPage;
// Create a fresh page object before every test
test.beforeEach(async ({ page }) => {
amenityPage = new AmenityRequestPage(page);
await amenityPage.goto();
});
// Loop through the amenities array to generate one test per item
for (const { name, quantity } of amenities) {
test(`should request ${quantity} ${name}`, async () => {
await amenityPage.increaseQuantity(name, quantity);
await expect(amenityPage.getQuantity(name)).toHaveText(String(quantity));
await amenityPage.submitRequest();
await expect(amenityPage.successMessage).toContainText('Amenity request submitted');
});
}
});
How does the data-driven loop work?
The amenities array holds your test data. Each object has a name (which amenity to target) and a quantity (how many to request).
The for...of loop runs at file load time, before any test executes. It registers a separate test for each entry in the array. So two array entries produce two distinct tests, each with its own name in the report. To add a new amenity test, you just add another object to the array.
- Save tests/amenity-request.spec.js.
- Run only the amenity tests to see the data-driven loop in action by running:
npx playwright test tests/amenity-request.spec.js
You should see 6 tests pass (2 amenity tests x 3 devices). Each amenity in your data array generated its own test, and each ran on Desktop Chrome, Pixel 8, and iPhone 15 Pro.
Seeing test failures?
- Check that the require path in your test file is '../pages/AmenityRequestPage' with two dots at the start. A single dot will look in the wrong folder.
- Verify that module.exports = { AmenityRequestPage } is present at the bottom of pages/AmenityRequestPage.js. Without it, the import in the test file gets undefined.
- Make sure the amenity names in the data array ('towels', 'hairbrush') match the data-testid prefixes in index.html exactly.
Help me debug why my data-driven amenity tests are failing in Playwright.
Two data entries, six test runs. Now add edge case tests that catch boundary conditions the happy path misses.
- In tests/amenity-request.spec.js, find the closing }); at the very bottom of the file.
- Add the following two edge case tests just above that closing });:
// Edge case: submitting without selecting any amenity
test('should not submit with zero quantities', async () => {
await amenityPage.submitRequest();
await expect(amenityPage.successMessage).toContainText('Please select at least one amenity');
});
// Edge case: quantity should never go below zero
test('should not decrease below zero', async () => {
await amenityPage.decreaseQuantity('towels');
await expect(amenityPage.getQuantity('towels')).toHaveText('0');
});
Why test edge cases?
The first test checks what happens when a guest taps submit without selecting anything. The app should show a helpful message instead of silently succeeding.
The second test verifies the quantity never drops below zero. In a real app, a negative quantity could mean a guest accidentally "returns" amenities they never requested.
- Save tests/amenity-request.spec.js.
Before you run the full suite, predict how many tests will pass. You have 7 check-in tests and now 4 amenity tests, each running across 3 devices. What total do you expect?
- Run the full test suite by running:
npx playwright test
You should see "33 passed" in the terminal. That is 11 tests (7 check-in + 4 amenity) multiplied by 3 device configurations.
- Open the HTML report to see the results grouped by device and test type by running:
npx playwright show-report
The report shows individual entries for each amenity test. You can see "should request 3 towels" and "should request 1 hairbrush" listed separately for each device.
Not seeing 33 passed?
- If you see 27 passed, the edge case tests might not have been saved. Reopen tests/amenity-request.spec.js and confirm the two edge case tests appear before the final });.
- If you see fewer than 27, check that tests/checkin.spec.js still has all 7 tests from the previous step.
Help me figure out why my Playwright test count doesn't match 33 passed.
✔️ Awesome, I've got everything!
Great. Make sure you have saved tests/amenity-request.spec.js before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm both chunks are present in your file, in this order:
- Chunk 1: The imports, amenities data array, test.describe block with test.beforeEach, and the for...of loop generating data-driven tests.
- Chunk 2: The two edge case tests ('should not submit with zero quantities' and 'should not decrease below zero') placed before the closing });.
You now have 33 tests running across three devices with a single command. Your framework covers the full guest journey from check-in through amenity requests, and the data-driven pattern means adding new amenity tests is just one more line in an array. Next up, you will push this framework further with a Secret Mission.
Secret mission
Add Scroll and Share Reservation Tests
On mobile viewports, reservation details sit below the fold and require scrolling to reach. Can you write tests that scroll elements into view and verify both the reservation details and the share flow work across all three device configurations?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files to continue practicing, or delete them to free up disk space.
Resources you used:
- hotel-checkin-tests project folder on your Desktop (contains all test files, page objects, mock app, and node_modules dependencies).
Keep everything running
No action needed. Choose this if you want to keep building on this framework, adding more tests or page objects.
- Your hotel-checkin-tests folder stays on your Desktop, ready for you to open in VS Code anytime.
- Node.js and VS Code remain installed on your machine for other projects.
- Playwright browser binaries remain cached on your system for future test runs.
Pause - I'll come back to this later
There are no running processes or servers to shut down. The test server only starts when you run npx playwright test and stops automatically when tests finish.
- Your project folder is saved on your Desktop. When you are ready to resume, open the hotel-checkin-tests folder in VS Code and pick up where you left off.
- No further action is needed to pause this project.
Delete - I don't want to use this again
Remove the project folder and its contents to free up disk space.
- Delete the project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/hotel-checkin-tests
Windows
rmdir /s /q %USERPROFILE%\Desktop\hotel-checkin-tests
- Confirm the folder is gone by running ls ~/Desktop (macOS) or dir %USERPROFILE%\Desktop (Windows). You should no longer see hotel-checkin-tests listed.
- Node.js and VS Code are general-purpose tools you will likely use for other projects. There is no need to uninstall them unless you want to.
- Playwright caches browser binaries on your system separately from the project folder. Refer to the Playwright documentation for instructions on removing cached browsers if you want to reclaim that disk space.
Nice Work!
Nice Work!
You just built a complete Playwright test automation framework from scratch, structured the way real QA teams do it. You have 39 passing tests running across three device configurations with a single command.
You've learned how to:
- Structure a test framework using the Page Object Model pattern, with CheckInPage and AmenityRequestPage classes that keep selectors and actions in one place so tests read like plain English.
- Configure cross-device testing across Desktop Chrome, Pixel 8 (Chromium), and iPhone 15 Pro (WebKit) with a single npx playwright test command and an interactive HTML report grouped by device.
- Write data-driven tests that loop through an array of amenities and quantities, automatically generating a test case for each combination instead of duplicating code.
- Secret Mission: Added scroll behavior and share reservation tests that verify below-the-fold content is reachable and interactive across all three device viewports.
Ready to quiz yourself?