Java · TestNG
Entertainment with Selenium
Pick city, date and showtime, select seats on a live seat map, add snacks, apply promos and pay with an OTP under a hold timer.
App under test
🎬 ShowTime
- URL
- /tickets
- Login
- not required
- Promos
- MOVIE50 · FIRSTSHOW
- Card OTP
- 123456
- Seat hold
- 5 minutes
Scenarios to automate
Work top to bottom — each one adds a new technique.
- 1
Date strip
BeginnerSeven date-option chips from today; dates are computed from the browser clock.
- 2
Filters
BeginnerCombine genre-* toggles, language-filter and movie-search; assert movie-card count.
- 3
Showtime states
Intermediatedata-status is available / filling / soldout / started — disabled ones can't be clicked.
- 4
Seat map
IntermediatePick seats by data-status, verify selected-seat chips and seat-total per tier.
- 5
Seat limit
IntermediateThe 9th seat shows seat-limit-error.
- 6
Snacks & promo codes
IntermediateMOVIE50 halves tickets (max $10); FIRSTSHOW only works on a first booking.
- 7
OTP payment
AdvancedCard payments open otp-modal; 123456 passes, anything else shows otp-error.
- 8
Async UPI approval
AdvancedUPI shows payment-processing for ~3s before the confirmation page.
- 9
Hold timer
AdvancedSeats are held for 5:00. Mock the clock to hit timer-expired without waiting.
- 10
Cancel a booking
AdvancedCancel from My bookings, confirm the dialog, and find it under the Cancelled tab.
Sample Selenium suite
A working starting point that finds elements every way Selenium can — by role, label, text, id, name, CSS, XPath and data-testid — so you practice them all. Pick your language, switch to data-testid only to compare, or try your own in the Locator Playground.
import org.testng.annotations.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.util.Map;
import org.openqa.selenium.support.ui.*;
import java.time.Duration;
import static org.testng.Assert.*;
import static org.openqa.selenium.support.ui.ExpectedConditions.*;
// Each line uses a different By on purpose — the comments name them.
public class ShowTimeTest {
static final String BASE = "https://www.practiceqaautomation.com";
WebDriver driver;
WebDriverWait wait;
By tid(String id) { return By.cssSelector("[data-testid='" + id + "']"); }
void type(By by, String text) { wait.until(elementToBeClickable(by)).sendKeys(text); }
void click(By by) { wait.until(elementToBeClickable(by)).click(); }
@BeforeMethod
public void setUp() {
ChromeOptions options = new ChromeOptions();
// Turn off Chrome's "Save password?" and "password found in a data breach" pop-ups: after a
// login they take the keyboard and mouse away from the page, so later typing and clicks are lost.
options.setExperimentalOption("prefs", Map.of(
"credentials_enable_service", false,
"profile.password_manager_enabled", false,
"profile.password_manager_leak_detection", false));
driver = new ChromeDriver(options);
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
@AfterMethod(alwaysRun = true)
public void tearDown() { driver.quit(); }
@Test
public void bookTwoSeatsAndPayByCard() {
driver.get(BASE + "/tickets");
wait.until(numberOfElementsToBeMoreThan(tid("date-option"), 1)).get(1).click(); // tomorrow
new Select(wait.until(elementToBeClickable(By.id("city-select")))).selectByVisibleText("London"); // By.id
wait.until(textToBePresentInElementLocated(tid("movie-list"), "Riverside Screens"));
WebElement movie = driver.findElement(By.xpath(
"//li[@data-testid='movie-card'][.//h2[normalize-space()='Nebula Drift']]")); // XPath: a card by its heading
movie.findElement(By.cssSelector("[data-testid='showtime']:enabled")).click();
By freeSeat = By.cssSelector("[data-testid^='seat-'][data-status='available']"); // starts-with + attribute
click(freeSeat);
click(freeSeat); // first *remaining* free seat
wait.until(numberOfElementsToBe(tid("selected-seat"), 2));
click(By.xpath("//button[normalize-space()='Proceed to checkout']")); // By.xpath: exact text
type(By.id("contact-name"), "Grace Hopper");
type(By.cssSelector("input[type='email']"), "grace@example.com"); // by input type
type(By.cssSelector("input[type='tel']"), "5559876543");
click(By.cssSelector("[aria-label='Add one Large Popcorn']")); // aria-label
type(By.cssSelector("[placeholder='Promo code']"), "MOVIE50"); // placeholder
click(By.xpath("//button[normalize-space()='Apply']"));
wait.until(textToBePresentInElementLocated(tid("promo-message"), "applied"));
type(By.id("ticket-card-number"), "4242424242424242");
type(By.cssSelector("[placeholder='MM/YY']"), "1230");
type(By.id("ticket-card-cvv"), "123");
click(By.xpath("//label[contains(., 'non-transferable')]//input")); // via its label
click(By.xpath("//button[starts-with(normalize-space(), 'Pay $')]")); // the text ends with the total
type(tid("otp-input"), "123456");
click(tid("otp-submit"));
String ref = wait.until(visibilityOfElementLocated(tid("booking-ref"))).getText();
assertTrue(ref.matches("ST[A-Z0-9]{6}"), "Unexpected booking ref: " + ref);
}
}