Write your first test
Build a test from scratch that signs in to an online store and adds a product to the cart.
You'll write a complete test by hand: open a store, sign in, add a backpack to the cart, and check that it worked. Along the way you'll meet the four files every Optics project uses, and see what happens when a test fails.
The test runs in a browser against Sauce Labs' demo store, a public site built for practising test automation. You don't need a phone, an emulator or an account.
Create an empty project#
$ optics init first-testOptics asks which template to start from. Type
1for (Blank project) and press Enter. You get a folder with empty files and headers already in place:first-test/ ├── config.yaml ├── test_cases/test_cases.csv ├── modules/modules.csv └── test_data/elements.csvThe folder names are only a convention. Optics recognises each file by the column names in its first row, so you can move files around later.
Tell Optics to use a browser#
config.yamlsays what Optics drives. Replace its contents with this:config.yaml driver_sources: - playwright: enabled: true elements_sources: - playwright_find_element: enabled: true - playwright_page_source: enabled: true - playwright_screenshot: enabled: true log_level: WARNINGdriver_sourcespicks the browser driver.elements_sourceslists the ways Optics may look at the page: asking the browser directly, reading the page's HTML, and taking screenshots.log_level: WARNINGkeeps the terminal quiet so you can read the results.Name the things you'll touch#
Before writing steps, give each field and button a name. Open
test_data/elements.csvand add four rows under the header:test_data/elements.csv Element_Name,Element_ID username_field,//input[@id='user-name'] password_field,//input[@id='password'] login_button,//input[@id='login-button'] backpack_add_button,//button[@id='add-to-cart-sauce-labs-backpack']The left column is the name you'll use in steps. The right column tells Optics how to find it. These are XPaths: paths into the page's structure.
//input[@id='user-name']means "the input whose id isuser-name".Write the steps#
Steps live in modules: small, reusable groups of actions. Open
modules/modules.csvand add three modules:modules/modules.csv module_name,module_step,param_1,param_2 Open the store,Launch App,https://www.saucedemo.com Sign in,Enter Text,${username_field},standard_user Sign in,Enter Text,${password_field},secret_sauce Sign in,Press Element,${login_button} Sign in,Assert Presence,Products Add the backpack to the cart,Press Element,${backpack_add_button} Add the backpack to the cart,Assert Presence,RemoveRead each row left to right: the module it belongs to, a keyword (the action), then the values the keyword needs.
${username_field}refers to the name you defined in the previous step.ProductsandRemoveare plain text that should appear on screen: the products page title, and the label the backpack's button changes to once it's in the cart.Turn the modules into a test case#
A test case is a named flow made of modules, run in order. Open
test_cases/test_cases.csv:test_cases/test_cases.csv test_case,test_step Shopper can add a backpack,Open the store Shopper can add a backpack,Sign in Shopper can add a backpack,Add the backpack to the cartCheck the test without running it#
optics dry_runreads every file and checks that each keyword exists and each${name}is defined. It doesn't click anything, so it's a quick way to catch typos:$ optics dry_run first-testTest Suite └── Shopper can add a backpack | 0.00s | PASS ├── Open the store | 0.00s | PASS ├── Sign in | 0.00s | PASS └── Add the backpack to the cart | 0.00s | PASS ╭──────────────────────────────────────────────────────╮ │ Total Test Cases: 1 | Passed: 1 | Failed: 0 │ ╰──────────────────────────────────────────────────────╯Run it#
$ optics execute first-testA browser window opens, signs in and adds the backpack. The terminal prints every step with its time:
Test Suite └── Shopper can add a backpack | 1.88s | PASS ├── Open the store | 0.85s | PASS │ └── Launch App (https://www.saucedemo.com) | 0.85s | PASS ├── Sign in | 0.82s | PASS │ ├── Enter Text (//input[@id='user-name'], standard_user) | 0.17s | PASS │ ├── Enter Text (//input[@id='password'], secret_sauce) | 0.09s | PASS │ ├── Press Element (//input[@id='login-button']) | 0.11s | PASS │ └── Assert Presence (Products) | 0.45s | PASS └── Add the backpack to the cart | 0.21s | PASS ├── Press Element (//button[@id='add-to-cart-sauce-labs... | 0.11s | PASS └── Assert Presence (Remove) | 0.09s | PASS ╭──────────────────────────────────────────────────────╮ │ Total Test Cases: 1 | Passed: 1 | Failed: 0 │ ╰──────────────────────────────────────────────────────╯You've written and run a test. Everything Optics produced is in
first-test/execution_output/: a screenshot taken before each step, andjunit_output.xml, a standard report your CI can read.
Watch a test fail#
A test is only useful if it fails when the app is broken. Pretend the store renamed its button: change the last row of modules.csv to look for Removed instead of Remove, then run the test again.
└── Add the backpack to the cart | 20.74s | FAIL
├── Press Element (//button[@id='add-to-cart-sauce-labs... | 0.12s | PASS
└── Assert Presence (Removed) | 20.51s | FAIL
╭──────────────────────────────────────────────────────╮
│ Total Test Cases: 1 | Passed: 0 | Failed: 1 │
╰──────────────────────────────────────────────────────╯
╭─────────────────── Failure details ──────────────────╮
│ Shopper can add a backpack → step 'Assert Presence │
│ (Removed)' — Keyword 'Assert Presence' failed after │
│ fallback attempts │
╰──────────────────────────────────────────────────────╯The run is slower this time because Assert Presence keeps looking for the text, in case the page is still loading, before it gives up. The tree shows exactly which step failed, and the Failure details box names it. The command also exits with code 1, which is how CI knows to mark the build red. Change Removed back to Remove before you continue.
Make the test survive a redesign#
Locators break when developers rename things. Optics lets an element have more than one value and tries them in order until one works. Add a second row for login_button that uses the button's visible text, and break the first one on purpose:
Element_Name,Element_ID
username_field,//input[@id='user-name']
password_field,//input[@id='password']
login_button,//input[@id='sign-in-button']
login_button,Login
backpack_add_button,//button[@id='add-to-cart-sauce-labs-backpack']Run the test again. It still passes: the XPath finds nothing, so Optics tries the text Login and clicks the button. Put the correct id back afterwards, but keep the text row as a safety net. How Optics finds elements explains every fallback.
What you learned#
- A project is a config (what to drive), elements (names for things on screen), modules (steps) and test cases (flows).
- Steps are keywords with values.
${name}refers to an element. optics dry_runchecks your files.optics executeruns them and writes results toexecution_output/.- An element can have several values, so one broken locator doesn't break the test.