How I Automate Web Tasks Without Building Fragile Scripts
I wrote the original version of this article around a Firefox extension based on Selenium IDE. The central idea still holds: when I repeat the same browser task, I ask whether a small automation can remove the repetition.
The old tool list no longer helps. The better question is whether the browser should be involved at all.
I Look for an API First
Clicks are tied to a page. An API is tied to a contract. If a service lets me export a report, create a record, or update data through an API, I normally use it.
I automate the browser when I need to test the path a person actually follows or when no suitable API exists. I also check the service terms before writing the script. Automation does not make fake accounts, unsolicited messages, or limit avoidance acceptable.
This distinction matters because a browser script has more ways to fail. Text changes, a modal appears, authentication expires, or a button moves behind a different state. I accept that cost only when the browser provides evidence or access that I cannot get another way.
Playwright and Selenium Solve Different Starting Problems
For a new JavaScript or TypeScript project, I often begin with Playwright. Its locators can target roles, labels, and visible text. It also checks whether an element is ready before acting on it.
Selenium WebDriver remains a sound choice for an existing Selenium suite, teams working across several programming languages, or infrastructure already built around Selenium Grid. Selenium IDE can still record a first scenario, but I treat the recording as a draft. The final automation belongs in readable, reviewed code.
A Small Playwright Example
This test exports a report from a test environment:
1import { test, expect } from "@playwright/test";
2
3test("exports a report", async ({ page }) => {
4 await page.goto(process.env.APP_URL!);
5
6 await page.getByLabel("Email address").fill(process.env.TEST_EMAIL!);
7 await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!);
8 await page.getByRole("button", { name: "Sign in" }).click();
9
10 await expect(page.getByRole("heading", { name: "Reports" })).toBeVisible();
11
12 const download = page.waitForEvent("download");
13 await page.getByRole("button", { name: "Export" }).click();
14 await download;
15});
I do not target “the third button” or copy a long CSS path from the browser inspector. I target the control as a user understands it. The Playwright locator guide recommends the same preference for user-facing attributes and explicit contracts.
Credentials stay in a secret store. The test account receives only the permissions required for this scenario.
Scheduling Is the Easy Part
A test can run on every pull request. A business task may run on a schedule. In both cases, I want the code to be safe when it runs twice.
GitHub Actions workflows can run on repository events, manually, or on a schedule. For an important job, I keep a manual trigger as a recovery option and record enough context to understand every run:
- when it started and finished;
- which input it processed;
- what output it produced;
- whether a retry occurred;
- why it failed.
I also define an owner. A scheduled script without an owner becomes a hidden service that everyone depends on and nobody maintains.
My Release Checklist for an Automation
Before letting a task run unattended, I verify that:
- a retry will not create an unwanted duplicate;
- a changed page causes a clear failure rather than a wrong action;
- permissions and secrets are limited;
- logs explain the result without exposing sensitive data;
- someone can stop, inspect, and resume the job;
- the automation saves more time than it consumes in maintenance.
The first recording rarely creates the real value. The value comes from a small script that another engineer can read, test, and repair without guessing what I intended.
Pierre-Henry Soria
#Automation #Playwright #Selenium #Software Testing #Github Actions #Web Development