Automated Testing at SeaLogs and Vessel Vanguard
Automated testing using Playwright and Cypress for web applications via GitHub Actions CI/CD pipelines.
In maritime operations, software reliability is not an academic exercise. When a vessel operates off the coast or navigates international waters, crew members rely on digital logbooks and maintenance systems to track engine hours, safety equipment, fuel bunkering, and regulatory compliance. At SeaLogs and Vessel Vanguard, our platforms manage these critical operational workflows across both desktop web browsers and mobile tablets packaged via Capacitor.
A software crash, silent regression, or failed synchronization routine in this environment does not just create user friction—it can delay port clearances, violate maritime safety conventions, or leave a vessel operator unable to log an emergency maintenance check. To protect against these failures across dozens of frequent releases, we engineered a rigorous automated testing pipeline combining fast unit test suites, role-driven Playwright end-to-end integration tests, deterministic test data seeding, and parallel GitHub Actions CI workflows.
Here is how our testing architecture evolved, how we eliminated flakiness, and the core strategies we used to guarantee reliability across every deployment.
The Stakes: Testing Software Offline and at Sea
Maritime software presents unique architectural challenges. Fleet operations demand:
- Strict Regulatory Compliance: Every log entry—from bilge pump inspections to liferaft certifications—carries legal significance during maritime authority audits.
- Intermittent Connectivity: Vessels regularly experience satellite dropouts. Applications must operate offline-first with local caching and reconcile changes gracefully once a shore-side link is re-established.
- Heterogeneous Devices: Crew members complete checklists on ruggedized mobile tablets or phones on deck, while fleet managers oversee hundreds of vessels from desktop command dashboards.
Because bugs in compliance calculations or synchronization protocols could corrupt audit trails, manual QA alone could never provide sufficient coverage or confidence. We needed automated verification that executed continuously on every pull request, validating not only that components rendered, but that core end-to-end operational workflows remained unbroken.
The Test Suite Architecture
To maintain high confidence without grinding engineering velocity to a halt, we organized our testing pyramid into three clear tiers:
/ \
/ E2E \ Playwright / Cypress
/-------\ (Critical journeys, auth, multi-role)
/ Component\ Contract & Form validation
/------------\ (Interaction states, edge cases)
/ Unit Tests \ Bun / Vitest / TypeScript
/----------------\ (Domain logic, math, UTC conversions)
1. Fast Domain Unit Tests
At the base of the pyramid, unit tests run against pure TypeScript business logic, math calculations, and state reducers using fast modern test runners. These tests execute in sub-second timeframes and cover:
- Engine hour accumulation and threshold warning triggers.
- Preventive maintenance schedule intervals (such as servicing required every 250 running hours or 6 calendar months, whichever comes first).
- Strict UTC timestamp formatting and relative duration calculations to prevent client-server timezone skew.
- Offline mutation queue serialization and idempotent conflict resolution routines.
2. Component Contract Tests
At the component boundary, tests verify that UI controls react predictably to invalid inputs, disabled states, and network timeouts. Rather than mounting full browser instances for every visual variation, component tests assert on field validation errors (such as entering negative fuel consumption rates or future inspection dates) before network requests are ever triggered.
3. End-to-End (E2E) Integration Tests
The pinnacle of our testing suite is end-to-end automation. While earlier initiatives utilized Cypress, we transitioned primarily to Playwright due to its first-class multi-tab support, native WebSocket and network interception primitives, resilient auto-waiting locators, and lightweight worker isolation. Playwright exercises complete user journeys against real browser engines (Chromium, WebKit, and Firefox).
Playwright End-to-End Automation
Our end-to-end test suite centers around multi-role workflows and real-world operational scenarios.
Multi-Role Permission and Workflow Testing
In fleet management, access control is paramount. A vessel operator logging a checklist has vastly different capabilities and permissions than a shore-based fleet administrator reviewing maintenance audits across forty ships.
Playwright allows us to simulate these personas cleanly using isolated browser contexts. We use Playwright’s storageState fixture to persist authentication tokens and cookies after a single programmatic login, avoiding redundant UI logins before each test case:
// tests/e2e/fixtures/auth.fixture.ts
import { test as base, type Page } from "@playwright/test";
type RoleFixtures = {
operatorPage: Page;
adminPage: Page;
};
export const test = base.extend<RoleFixtures>({
operatorPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: "playwright/.auth/operator.json",
viewport: { width: 768, height: 1024 }, // Tablet viewport
});
const page = await context.newPage();
await use(page);
await context.close();
},
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: "playwright/.auth/admin.json",
viewport: { width: 1440, height: 900 }, // Desktop viewport
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
Using these fixtures, we test cross-role collaboration directly. For example, when an operator flags a critical generator fault from a tablet view, the test verifies that an alert immediately appears on the fleet administrator’s dashboard:
// tests/e2e/incidents/fault-escalation.spec.ts
import { expect } from "@playwright/test";
import { test } from "../fixtures/auth.fixture";
test.describe("Critical Fault Escalation", () => {
test("submitting critical equipment fault alerts fleet admin", async ({
operatorPage,
adminPage,
}) => {
// 1. Operator logs critical fault on vessel engine
await operatorPage.goto("/vessels/v-102/logbook");
await operatorPage.getByRole("button", { name: /new entry/i }).click();
await operatorPage.getByLabel(/log type/i).selectOption("DEFECT");
await operatorPage.getByLabel(/severity/i).selectOption("CRITICAL");
await operatorPage.getByLabel(/description/i).fill("Port generator cooling pump failure.");
await operatorPage.getByRole("button", { name: /sign and submit/i }).click();
await expect(operatorPage.getByText(/entry logged successfully/i)).toBeVisible();
// 2. Admin verifies alert in fleet overview
await adminPage.goto("/fleet/alerts");
const alertRow = adminPage.getByRole("row", { name: /port generator cooling pump failure/i });
await expect(alertRow).toBeVisible();
await expect(alertRow.getByText(/critical/i)).toBeVisible();
});
});
Deterministic Seed Data & Eliminating Flakiness
Test flakiness destroys team trust in CI. If tests fail intermittently, developers learn to re-run pipelines without investigating or ignore failures altogether. In our domain, the two biggest causes of flakiness were mutable shared state and dynamic clock time.
Ephemeral Database Seeding
Early iterations suffered from test interference when concurrent test runners wrote to the same shared test database. We eliminated this by assigning an isolated test tenant ID and database schema prefix per Playwright worker process:
- Each worker generates an isolated tenant namespace (
test_worker_1,test_worker_2). - A lightweight seed script injects known baseline entities (vessel record, equipment checklist, assigned crew).
- After worker teardown, an automated hook drops the ephemeral worker schema.
Deterministic Clock and UTC Mocking
Because maritime log entries are strictly sequenced by UTC timestamps, daylight savings transitions or midnight date rollovers caused non-deterministic failures in test suites running across different developer machines or GitHub Actions runners.
We tackled this by leveraging Playwright’s clock mocking capabilities:
// Fast-forward or freeze time deterministically
await page.clock.setFixedTime(new Date("2026-09-05T14:30:00Z"));
By freezing time to an explicit UTC moment during date-sensitive assertions, our timeline calculations, overdue maintenance warnings, and relative time badges evaluate identically regardless of where or when the CI runner executes.
The GitHub Actions CI/CD Pipeline
To ensure that pull requests can be reviewed and merged promptly, our CI pipeline in GitHub Actions runs automated checks in parallel stages:
- Lint & Static Analysis: Runs
eslint,prettier --check, andtsc --noEmitconcurrently in under 60 seconds. - Unit & Contract Suite: Executes fast unit tests with high concurrency.
- E2E Matrix Execution: Shards Playwright tests across multiple parallel runner containers, running headless Chromium and WebKit browsers.
- Artifact Retention on Failure: When a failure occurs, the runner captures a full Playwright trace file, video recording, and DOM snapshot, uploading them as workflow artifacts.
# Sample GitHub Actions E2E Job Definition
name: End-to-End Tests
on:
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bunx playwright install --with-deps chromium
- name: Run Sharded Playwright Tests
run: bunx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-traces-${{ matrix.shardIndex }}
path: test-results/
retention-days: 7
With full trace archives available directly from GitHub Actions summary pages, debugging an intermittent failure takes minutes rather than hours. Developers inspect the exact network waterfall, console logs, and visual frames captured at the moment of failure.
Key Takeaways for Scaling Quality on Lean Teams
Building a resilient automated testing practice on lean teams does not require writing tests for every trivial function. Instead, it requires ruthless focus on high-impact areas:
- Prioritize User Journeys Over Code Coverage: A 95% unit test code coverage metric provides false confidence if the primary checkout or log submission path breaks in the browser. Invest in end-to-end tests for the top 5–10 critical business paths first.
- Treat Flakiness as a Blocker: Never tolerate intermittent test failures. If a test is flaky, isolate it immediately, identify the root cause (usually unawaited asynchronous operations or unseeded shared state), and fix it. A green CI build must mean the software is working.
- Keep CI Fast Through Sharding: As test suites grow, execution times naturally creep up. Shard your integration suites across parallel CI runners and maintain fast unit tests to keep PR feedback loops under 5 minutes.
- Mock External Boundaries, Not Internal Architecture: Keep database models and domain logic real in tests, but mock third-party SMS providers, satellite telemetry uplinks, and payment gateways.
By investing in clean fixtures, deterministic UTC time mocks, and robust CI gating, our team transformed automated testing from a chore into our greatest competitive advantage—allowing us to ship updates with speed, stability, and total confidence.