← hunter-sreeni / projects TC-002 · case study

Nithyakarma Quality Lab

Where DSA actually shows up in a real test suite

I wrote the same 39 end-to-end test cases twice, in Playwright (TypeScript) and Selenium (Java + TestNG), against my live Nithyakarma app. Then I went back through every file and asked a plain question: which data structures and algorithms are really in here, and what did each one fix? This page is the answer, with the code, the impact, and the places where the honest answer is "none".

39test cases, written in both frameworks
~57%less wait per save check (533 ms to 228 ms)
4dependency-ordered stages in a DAG
12DSA ideas found in the real code

Scope

What this is, and what it is not

A test suite is not a LeetCode set, so not every topic belongs in it. Forcing a heap or a DP table into a login test would make the suite worse. What I wanted to show is that the topics that did appear were picked because they solved a real problem: test ordering, flaky waits, stale caches, merging two result formats.

Not used anywhere in this suite: nothing here needed them, so I am not claiming them.
  • sorting
  • binary search
  • heap
  • explicit stack / queue
  • linked list
  • trie
  • two pointers
  • sliding window
  • dynamic programming
  • backtracking
  • greedy
  • union-find
  • LRU cache

Contents

  1. Test ordering as a DAG
  2. Event-driven wait vs polling
  3. Hash map join of two result formats
  4. Frequency counting for the dashboard
  5. Recursive DFS over the report tree
  6. The DOM is a tree: leaves and parents
  7. Memoization and cache invalidation
  8. Ordered maps (and the LRU connection)
  9. Data-driven tables and boundary values
  10. Set equality
  11. Linear search with early exit
  12. Inverse operations keep live data clean
  13. All 39 test cases mapped to problems

The 12 ideas

1. Test ordering as a DAG (topological order)

Graph
Problem
All tests share one live account. If logout runs early it can revoke the session every other test is using. If the settings tests run alongside the read-only checks, they change the data those checks read.
Idea
Each stage is a node and each "must run after" rule is an edge. Playwright runs projects in topological order of that graph. CI does the same thing one level up with job needs:.
Impact
Login happens once, and logout is guaranteed to run last. Selenium copies the same order with TestNG <test> blocks, so both suites see the account in the same state.
Cost
Topological sort is O(V + E). Here V = 5 projects, so the value is correctness, not speed.

playwright-ts/playwright.config.ts · .github/workflows/tests.yml

setup ──► app ──► settings ──► logout        (Playwright projects)
playwright ──► selenium ──► dashboard        (CI jobs)

{ name: 'settings', dependencies: ['app'] },
{ name: 'logout',   dependencies: ['settings'] },

2. Event-driven wait instead of polling

Concurrency
Problem
The app updates the screen first and saves in the background. Reloading before the save request goes out cancels it, and the test then sees the old value. Selenium's default WebDriverWait polls every 500 ms, so it kept noticing the save late.
Idea
Stop asking "is it done yet?" on a timer. Subscribe to the network over WebDriver BiDi and let the PATCH response itself release a CountDownLatch.
Impact
Each save check dropped from about 533 ms to about 228 ms, close to Playwright's built-in waitForResponse. The reload that verifies each step only runs after the save has landed.
Cost
Polling has worst-case lag equal to the poll interval. An event has none, so you wait only as long as the server takes.

selenium-java/.../pages/ProfilePage.java · clickAndWaitForSave()

CountDownLatch saved = new CountDownLatch(1);
network.onResponseCompleted(response -> {
    if ("PATCH".equals(response.getRequest().getMethod())
            && response.getRequest().getUrl().contains("/rest/v1/profiles")
            && response.getResponseData().getStatus() / 100 == 2) {
        saved.countDown();
    }
});
click.run();
if (!saved.await(10, TimeUnit.SECONDS)) throw new TimeoutException(...);

3. Hash map join of two result formats

Hash map
Problem
Playwright writes JSON and Selenium writes Surefire XML. Each test is named differently in each one. The results page needs one row per test case showing both outcomes.
Idea
Index each report once into a Map keyed by test name, then walk the 39 test cases and look each one up. This is the Two Sum trick: trade a little memory for constant-time lookups.
Impact
The dashboard can say "same result in both frameworks" and highlight any test where they disagree.
Cost
O(n + m) to build and join, instead of O(n × m) for a nested loop.

dashboard/build.mjs

const pw = new Map();                       // title -> { status, ms }
pw.set(spec.title, { status, ms: last?.duration ?? 0 });

const rows = cases.map(c => ({ ...c, pw: pw.get(c.playwright) ?? null, se: findSe(c.selenium) }));

4. Frequency counting for the summary cards

Counting
Problem
Each framework card shows passed, failed, skipped, flaky and "not run" totals.
Idea
One pass over the rows, incrementing a counter per status. It is the same pattern as character frequency or Python's Counter. A missing result counts as its own bucket instead of crashing the build.
Impact
One function serves both frameworks, and a test that silently did not run shows up as "missing" instead of being dropped.
Cost
O(n) time, O(k) space for k statuses.
const tally = side => {
    const t = { passed: 0, failed: 0, skipped: 0, flaky: 0, missing: 0 };
    for (const r of rows) t[r[side]?.status ?? 'missing']++;
    return t;
};

5. Recursive DFS over the report tree

Tree
Problem
Playwright's JSON report is a tree: files contain describe blocks, which contain more describe blocks and then specs. The depth is not fixed.
Idea
A depth-first walk that handles the specs at the current node and then recurses into each child suite, the same shape as Maximum Depth of a Binary Tree, but for an n-ary tree.
Impact
Nested test.describe blocks just work. Adding a new level of grouping to the tests does not break the dashboard.
Cost
O(nodes). Recursion depth equals nesting depth, which is about 3 here.
const walk = suite => {
    for (const spec of suite.specs ?? []) { /* record each test */ }
    (suite.suites ?? []).forEach(walk);
};
report.suites.forEach(walk);

6. The DOM is a tree: leaves and parents

Tree
Problem
Several parts of the app have no roles or labels to target: history dates, kalam times, stat tiles.
Idea
Use tree vocabulary directly. div[not(*)] means "leaf nodes only", .. is the parent pointer, and h1 + * is the next sibling.
Impact
Matching the date regex against leaves only avoids false hits on container divs, whose text includes all their children.
Cost
One scan of the subtree under main.

selenium-java/.../pages/HistoryPage.java

private final By leafDivs = By.xpath("//main//div[not(*)]");   // leaves of the DOM tree
driver.findElements(leafDivs).stream().map(BasePage::text).filter(t -> t.matches(ENTRY_DATE)).toList();

7. Memoization and cache invalidation

Caching
Problem
Logging in through the UI is blocked by a Cloudflare captcha, and minting a session for every test is slow. Separately, the app paints a cached profile before the server copy arrives, so a check can pass on stale data.
Idea
Compute the session once and reuse it. Playwright saves it to a storageState file in a setup project. Selenium keeps it in a lazily filled, synchronized static field. For the stale profile, invalidate the cache key, reload, and wait until the app rewrites it from the server.
Impact
One admin magic-link call per run instead of one per test, and no false passes from stale cached values.
Not an LRU
It is a cache with exactly one entry, no eviction policy, and the token's own expiry as a TTL. An LRU cache needs a fixed capacity and evicts the least recently used entry, and nothing here does that.
// Selenium: compute once, reuse (memoized singleton)
public static synchronized String sharedSessionJson() {
    if (shared == null) shared = createSessionJson();
    return shared;
}

// Playwright: invalidate, reload, wait for the fresh write
await page.evaluate(key => localStorage.removeItem(key), ProfilePage.CACHE_KEY);
await page.reload();
await page.waitForFunction(key => localStorage.getItem(key) !== null, ProfilePage.CACHE_KEY);

8. Ordered maps (and the LRU connection)

Hash map + linked list
Problem
The navigation test visits all six tabs, and the failure report should list them in the same order as on screen.
Idea
Java's LinkedHashMap is a hash map with a doubly linked list running through its entries, so iteration follows insertion order. TypeScript objects with string keys keep insertion order too.
Impact
The tab tests run in a stable order every time, so a failure always means the same thing.
Interview link
This is the exact structure behind LRU Cache (#146). Construct it with accessOrder = true and override removeEldestEntry, and you have an LRU. The suite uses insertion order only.
public static final Map<String, String> TABS = new LinkedHashMap<>();
static { TABS.put("Today", "/"); TABS.put("Learning", "/learning"); /* ... */ }

9. Data-driven tables and boundary values

2D array
Problem
Seven login validation checks would mean seven nearly identical test bodies.
Idea
One table of rows (an Object[][] TestNG DataProvider and a TypeScript array), one test body. The cases sit exactly at the boundary: a 7-character password must fail and an 8-character password must pass.
Impact
Adding a case is one line. The 7 vs 8 pair is the off-by-one check that catches a < written where <= was meant.
{ "password 7 chars",  "password", "1234567",  "tooShort" },
{ "8 char password",   "password", "12345678", "valid"    },

10. Set equality

Set
Problem
"Shows three kalams" is not enough. A fourth unexpected kalam, or the right count with a wrong name, should both fail.
Idea
Build a map of name to time from the page and assert its key set equals the expected set exactly. The Learning test does the same with a count plus membership for all 14 practices.
Impact
Catches extras and missing items in one assertion, with a readable diff.
assertThat(kalams).containsOnlyKeys("Rahu Kalam", "Yamagandam", "Gulika Kalam");

12. Inverse operations keep live data clean

State
Problem
These tests run against production with a real account. Any test that changes data has to leave it exactly as it found it.
Idea
Every write is paired with its inverse: switch a setting and switch it back, add a child and remove it, Next and then Previous. The start state is read first, and the two targets are chosen with a swap so the test works whichever option is currently on.
Impact
The suite can run on every push without slowly corrupting the account. The two writes with no inverse in the app yet are opt-in only and stay skipped in CI.
const [from, to] = await profile.isChipOn(a) ? [a, b] : [b, a];
for (const target of [to, from]) { /* click, wait for the save, verify after a fresh reload */ }

Traceability

All 39 test cases mapped to a problem pattern

For each test case: the technique the code actually uses, and the classic interview problem with the same core idea. Direct means the same data structure or algorithm. Analogy means the same way of thinking. None means it is a plain UI check, and I would rather say so than stretch it.

TCTest caseTechnique in the codeProblem patternFit
TC-01Login page renders all elementsTable of expected elements, anyMatch scan per itemTwo Sum (#1): existence lookup. A set would make it O(1), but at about 10 items a scan is fineAnalogy
TC-02 to TC-08Login validation (7 cases)Data-driven table (Object[][]), boundary values 7 vs 8"Write the test cases for this function" (boundary values, equivalence classes); off-by-one thinking from First and Last Position (#34)Analogy
TC-09Captcha blocks an automated loginWait, check, retry the click once, then assertRetry with backoffAnalogy
TC-10API rejects login without a captcha tokenAssert status and a JSON fieldValidate a JSON response against an expected shapeAnalogy
TC-11Header: logo, tabs, streak, logoutExpected map of tab to href compared with the DOMDeep-compare / diff two objectsDirect
TC-12Every tab navigates to its pageLinkedHashMap iterated in insertion orderLRU Cache (#146): same hash map + doubly linked list structureDirect
TC-13Greeting shows today in ISTTimezone-aware date formatting-None
TC-14Panchangam box and three kalamsBuild map of name to time, containsOnlyKeysValid Anagram (#242): exact equality of two collectionsDirect
TC-15Family switcher shows Me and Add childVisibility checks-None
TC-16Add child opens the family sectionURL match after click-None
TC-17Streak card: streak, best, punyaSeveral regex patterns over one block of textParse a log file and extract fieldsAnalogy
TC-18Anushtanams list: slots and Mark DoneFilter cards by a child element's text-None
TC-19Mark one sandhya slot (opt-in)Regex extract the count, skip if already done-None
TC-20Picker disables tracked practicesVerifies the app refuses duplicatesContains Duplicate (#217): set membershipDirect
TC-21Search filters the pickerSubstring filter, count equals 1- (a linear scan here; at scale this becomes a Trie)None
TC-22Add an untracked anushtanam (opt-in)filter then findFirst, then membership re-checkFirst Unique Character (#387): first item matching a condition, early exitAnalogy
TC-23Lists every read-along practiceCount equals 14, plus each name presentIntersection of Two Arrays (#349) / set equalityDirect
TC-24Hanuman Chalisa reader, language switchOne-hot toggle state-None
TC-25History: family switcher and dated entriesdiv[not(*)]: leaf nodes of the DOM treeMaximum Depth of Binary Tree (#104): tree traversal, leaf = no childrenDirect
TC-26Sabha defaults to this weekText and class checks-None
TC-27Week / Month / Kids switch the boardState switches and returns to start-None
TC-28Calendar day view details and kalamsSoft assertions in a loop-None
TC-29Next and Previous move a day and backInverse operations return to the start stateValid Parentheses (#20): every open matched by a close returns to balanceAnalogy
TC-30Week and Month viewsExclusive selection-None
TC-31Profile: name, tier progress, statsRegex on text (8-char hex invite code)-None
TC-32Save enabled only after the name changesDirty check: current value compared to originalDeep-compare / diff two objectsAnalogy
TC-33Delete account disabled without the emailGuard condition check-None
TC-34Footer linksMap of link text to href compared with the DOMDeep-compare / diff two objectsDirect
TC-35Add a child and remove itUnique name from a timestamp, paired add/remove-None
TC-36, TC-37Tradition / marital status round-tripsSwap by destructuring, event wait, poll until a fresh reload agreesRetry with backoff (bounded polling)Analogy
TC-38Leaderboard opt-in round-tripSame pattern for a booleanRetry with backoff (bounded polling)Analogy
TC-39Logout returns to the login pageRuns last, as the final node of the project DAGCourse Schedule (#207): topological sortDirect

Outside the test cases, the dashboard build adds three more direct matches: the result join is Two Sum (#1), the status totals are frequency counting (Top K Frequent Elements, #347, without the heap), and the report walk is tree DFS (#104).

Takeaway

What I would say in an interview

The useful DSA in test automation is mostly about time and state: running things in the right order, waiting on events instead of guessing with timers, not trusting cached data, and leaving shared data exactly as you found it. The classic structures (hash maps, sets, trees, graphs) show up as the tools for those jobs. I can point to the line where each one is used, and I can also point to the places where a plain check was the right call.