The Testing Academy · Class Notes Monday, 14 September (IST)
Live class · study guide

A gatekeeper for a framework everyone is now writing with AI

The last morning class of the batch, and the piece that makes everything before it safe to ship. Two halves: ESLint with a type check, the static half a machine can decide, and four quality gates written as skill files that every coding agent reads before it raises a pull request, asking whether the change is real, whether it duplicates what the tooling already does, how many callers the new abstraction has, and whether it still belongs to this framework. Ends with the wrap-up: what is left to finish, and what continues after today.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 2x class, rebuilt from the session recording and the batch repository, which received nine commits between 7:40 and 8:18 am covering ESLint, the four gates, the per-agent rule files and the CI workflow. The Eraser deck was not reachable while this page was written, so the config, the gate rules and the diffs are quoted from the repository. The linter config, the npm scripts, the CI workflow and the gate files were read from the merged main branch; the before and after diffs are the real ones the class watched being applied.

01

Where this sits

The framework is finished: Playwright and TypeScript, Faker, Ajv, JSONPath, CSV, Winston, page objects, the API levels, Cucumber, and last week's AI agent factory. Today adds the thing that protects all of it, and then the course wraps up.

Three pieces of housekeeping came first, and they are the homework for the next month:

  • Finish the locator strategies test. A handful of people had; most had not.
  • Work through QA Battle. Log in with LinkedIn and you get daily battles plus a test automation track. The instruction was to finish the level one and level two challenges over the next month and push the code.
  • Share your framework repository. A thread in the SDET club is being created to collect everyone's advanced framework repo. The bar is not perfection: post it even if AI, Cucumber or the API layer is missing, then add the rest this week.

The stated prerequisite for today: your advanced framework should exist, with the AI, Cucumber and API layers, and ideally it should be running on CI.

02

What a quality gate is, and why now

The question was put to the room before any code: what do you mean by a quality gate? The answers that came back, in order, were quality of the code, standards, no over-engineering, linting and type checks, secure credentials, and a ponytail review. All of them are right, and together they are the definition.

The reason it matters now rather than two years ago is AI. Everyone in the room has GitHub Copilot, Claude Code, Amazon Q, Kiro, Cursor or Windsurf. Code arrives faster than anyone can review it, and it arrives confidently wrong in a few repeatable ways. From the instructor's own consulting work across forty-odd companies, this is the single thing teams are missing right now.

So the proposal, in his words: four gates, one rule engine, three places it fires.

Coding agentwrites the change STATIC: A MACHINE DECIDES npm run typecheck npm run lint types, missing await,unused fixtures, test.only JUDGEMENT: A REVIEWER DECIDES 1 ai-slop 2 ponytail 3 over-engineering 4 framework-patterns Pull requestthen merge a failing gate sends the change back, it does not merge The rules live in the repository, so every agent reads them. The reviewer still decides the four on the right.
Two halves. The left is decided by a machine and belongs in CI. The right is judgement, written down so an agent can apply it consistently.
03

ESLint, and what linting actually is

ESLint reads your code without running it. It parses the file into an abstract syntax tree, walks that tree against a set of rules, and reports what it finds. That is the whole mechanism.

The class called it an "abstract source tree" in passing. The term is abstract syntax tree, the same AST from the JavaScript engine session earlier in the batch. Nothing else about the explanation changes.

The example that makes it matter is the one every Playwright suite hits:

TypeScript
// no await. The promise is created, the test ends, the assertion never runs.
expect(page.locator('[data-test="title"]')).toHaveText('Products');

That test passes. It passes while checking nothing, which is worse than failing. TypeScript will not stop you, because the types are fine. Only a type-aware lint rule can see that a promise was created and dropped.

The analogy from class: ESLint is the older sister. You are slouching, she tells you to sit up. You are watching videos, she tells you to go learn something. Nothing she says is new information; the value is that she says it every single time, before it becomes a habit.

THREE TOOLS, THREE DIFFERENT JOBS TypeScriptare the types correct? wrong argument bad import path misspelled property npm run typecheck ESLintis this a good pattern? missing await committed test.only unused page fixture, hard waits npm run lint Prettierdoes it look the same? indentation semicolons braces and line breaks optional, not added today Other languages have the same middle box under other names: SonarLint or SonarQube for Java, Pylint for Python.
They overlap far less than people expect. Types are not patterns, and neither of them is formatting.

Asked in class: TypeScript already checks types, so why add ESLint? Because they answer different questions. TypeScript asks whether the types line up. ESLint asks whether the pattern is a good one: a missing await, a test.only left in, a hard wait, a fixture injected but never used. And a follow-up, is this like SonarQube? Yes, that is a fair way to place it: ESLint is the SonarQube of JavaScript and TypeScript. ES stands for ECMAScript, so it is a JavaScript and TypeScript tool, not a Playwright one; Playwright is just a library it happens to be linting.

04

Adding it, on a branch

The manual route exists (npm init @eslint/config@latest scaffolds a config for you), but nobody does it by hand any more. The class asked the coding agent, and the first instruction was not about linting at all:

Create a branch first. The reasoning, asked of the room and answered by it: ESLint on an existing codebase produces a pile of findings. You want to add it, fix what it finds, run it on a separate CI job, see it green, and only then merge to main. Main never sees the broken middle state.

Two branches were used, one per half, and both were merged at the end:

Text
eslint-add     ->  PR #1  ->  main
quality-gate   ->  PR #2  ->  main

What landed for the first half:

Terminal
npm install --save-dev eslint @eslint/js typescript-eslint eslint-plugin-playwright
Text
eslint       ^10.10.0        typescript-eslint       ^8.70.0
@eslint/js   ^10.0.1         eslint-plugin-playwright ^2.11.0

The config is a flat config, eslint.config.mjs. Two parts matter: what it ignores, and what it insists on.

JavaScript
export default tseslint.config(
    {
        // Generated output and vendored code. Everything here is rebuilt by a run.
        ignores: [
            'node_modules/**', 'test-results/**', 'playwright-report/**',
            'blob-report/**', 'tta-report/**', 'reports/**', 'logs/**',
            'docs/**', 'learnings/**', 'playwright/.cache/**',
            '.claude/**',   // agent skills, not project source
        ],
    },
    js.configs.recommended,
    ...tseslint.configs.recommendedTypeChecked,
    {
        languageOptions: {
            parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
        },
        rules: {
            // A missing await in a Playwright spec is a test that asserts nothing.
            '@typescript-eslint/no-floating-promises': 'error',
            '@typescript-eslint/await-thenable': 'error',
            '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
            '@typescript-eslint/no-explicit-any': 'warn',
            'eqeqeq': ['error', 'always'],
            'prefer-const': 'error',
        },
    },
    {
        files: ['src/tests/**/*.spec.ts'],
        ...playwright.configs['flat/recommended'],
        rules: {
            // A committed test.only silently skips the rest of the file.
            'playwright/no-focused-test': 'error',
            'playwright/no-skipped-test': 'off',   // demo specs skip on an env flag
        },
    },
);

Why the ignore list, asked in class: none of those folders are source. They are output, rebuilt by every run, and linting them is noise. The class's own follow-up was the right instinct too, that docs and learnings are prose rather than code.

Then the scripts, which are how anyone actually runs it:

JSON
"lint":      "eslint .",
"lint:fix":  "eslint . --fix",
"typecheck": "tsc --noEmit -p tsconfig.json",
"verify":    "npm run typecheck && npm run lint && npm test"

Error or warning is a real decision, not a formality. no-floating-promises is an error because a missing await is a broken test. The no-unsafe-* family is a warning, because response.json() and JSONPath() both return any and would otherwise fire on every raw API spec; the real contract check there is the Ajv schema at Level 05. Warnings keep the count visible without blocking a build. The config in the repo says exactly this, in comments, next to each rule.

05

What the linter actually changed

The class watched the fixes land across the framework. The agent applied them, which prompted a fair question from the room: after ESLint runs, do we still review each fix by hand? The answer was yes, ideally. A sample of what changed:

An unused fixture, removed. The one predicted earlier in the session:

TypeScript

- test('logs in with valid credentials @p0', async ({ page }) => {
+ test('logs in with valid credentials @p0', async () => {

page was injected and never used. If you are not using it, do not ask for it.

A cast replaced by a declared type, and a better assertion:

TypeScript

- const allIds = JSONPath({ path: '$[*].bookingid', json: list }) as number[];
- expect(allIds.length).toBe(list.length);
+ const allIds: number[] = JSONPath({ path: '$[*].bookingid', json: list });
+ expect(allIds).toHaveLength(list.length);

A redundant cast, gone:

TypeScript

- return this.context as APIRequestContext;
+ return this.context;

An unused import, gone. createLogger imported into a spec that never logged.

Autofix is not always right, and the repo records one case. lint:fix wanted to strip as number[] from a JSONPath call, which looks redundant but is load-bearing: the source is any, so removing the cast left the callbacks with implicit any and broke the build. That is why no-unnecessary-type-assertion is a warning in this config rather than an autofixed error. Run the fixer, then read the diff.

06

The four gates

ESLint and the type check are the static half. They cannot tell you whether a change invented an API that does not exist, or whether it rebuilt something the framework already has. That is the other half, and it is written as skill files in the repository.

Gate The question it asks
gate-ai-slop Was this generated, skimmed, and shipped?
gate-ponytail Does anything else in the run already record this?
gate-over-engineering How many callers does this abstraction have?
gate-framework-patterns Is this still part of this framework?

1. AI slop. The tells of code that was generated and never read. Invented APIs (grep the symbol in the type definitions; if it is not there, it was imagined). Assertions that cannot fail. as any and @ts-ignore used as a mute button. Comments that restate the code instead of saying why. A helper copy-pasted next to the one that already exists. Dead exports, a symbol exported and never imported: this exact audit found 8 of 17 dead exports in src/ai when it was first run. And claims nobody verified, which includes documentation and commit messages asserting a status code that was never observed.

2. Ponytail. Named for the review style the batch already uses: find what to delete. The gate leans on facts about this repo, that trace: 'on' already records every request and its timing, and the custom reporter already prints per-test status and the flaky diff. So a test.step or a console.log that exists only to surface that information is duplicate machinery. It reports a net line count.

3. Over-engineering. One question, answered with a number produced by a command: how many callers does this abstraction have? One caller is not an abstraction, it is a detour. Zero is dead code. The gate carries the shell one-liner that counts external callers for every new export in the diff.

4. Framework patterns. The longest gate, because it is the repo's conventions written down: import from @fixtures/test-base and never @playwright/test; no locators in a spec, they live in the page object; spec filenames need a dot before spec, since 05_crud_spec.ts with an underscore is silently never collected; a new test directory needs its project decided at the same moment it is created; read env through @config/env; Ajv for schemas, not Zod; no test may pass or fail on model output; and the suite must stay green with no API key.

The rule that makes the whole thing worth doing, quoted from the gate file: a gate that cannot cite a command it ran has not run. "Looks fine" is not a verdict. Each gate reports PASS or FAIL with the grep, the test output or the line numbers that justify it, and the report ends with a verdict line. Never weaken a gate to make a diff pass; if a gate is wrong about the repo, fix the gate in its own commit.

07

Why it lives in dot-folders

Asked in class: do the gates trigger on their own, or do we run them? Both, and the automatic half is the point. The same rules were written into the folder each agent reads on its own:

Text
.claude/skills/quality-gate/SKILL.md     .cursor/rules/quality-gates.mdc
.claude/skills/gate-ai-slop/SKILL.md     .windsurf/rules/quality-gates.md
.claude/skills/gate-ponytail/SKILL.md    .kiro/steering/quality-gates.md
.claude/skills/gate-over-engineering/    .clinerules/quality-gates.md
.claude/skills/gate-framework-patterns/  .opencode/command/quality-gate.md
AGENTS.md                                .github/copilot-instructions.md
WRITE THE RULES ONCE Four gate files ai-slopponytail over-engineeringframework-patterns EVERY AGENT READS ITS OWN FOLDER .claude/skills/ .github/copilot-instructions.md .cursor/rules/ .windsurf/ .kiro/ .clinerules/ The same four verdicts whichever tool your teammatehappens to use A file in the agent's own folder is read automatically, so the gates run before the pull request rather than after the review.
The rules are not in someone's head or in a wiki. They are in the folder the tool already reads, which is why they actually run.

Asked in class: will these rules be shared with whoever approves the merge? Yes. They are in the repository, so everyone has them, including the reviewer and every new joiner. And a second one: do we need a library like ESLint for the AI half? No. These are plain markdown files. The agent reads them; there is nothing to install.

08

The CI half

The mechanical checks also run on every pull request, so the gate does not depend on anyone remembering. The workflow's own comment draws the line: judgement still belongs to the reviewer, and CI only enforces what a machine can check without an opinion. It runs the type check, the lint, a grep for spec filenames with an underscore instead of a dot, a scan for committed key material, the caller audit as warnings, and the full suite with no AI key, because that is CI's normal state.

YAML

- name: framework-patterns - typecheck
  run: ./node_modules/.bin/tsc --noEmit -p tsconfig.json

Call the local binary, never npx, in CI. This gate's own first CI run proved why: on a checkout where typescript was not a declared dependency, npx tsc silently downloaded an unrelated package of the same name from the registry and failed with "This is not the tsc command you are looking for". Two fixes landed together, calling ./node_modules/.bin/tsc, and adding typescript to package.json, where it had never actually been declared.

09

Tasks and announcements

Before anything else

  • Finish the locator strategies test.
  • Work through QA Battle, level one and level two, over the next month, and push what you write.
  • Post your advanced framework repository in the SDET club thread, even if AI, Cucumber or the API layer is still missing. Add the rest this week.
  • Add the quality gate and ESLint to your own framework, then run npm run verify.

Then

  • Watch the classes you are behind on; recordings are permanent.
  • Watch the resume review and GitHub portfolio master classes, and build the portfolio.
  • Start applying. Mock interviews can be scheduled with Deepak after about fifteen days.
  • Submit a testimonial, text or video. Complete the videos and submit it and the certificate is issued automatically.

What continues after today

Morning classes end here. Evening sessions at 8 pm IST continue biweekly through December: Jenkins parts 3 and 4, GitHub Actions, a revisit of Playwright CLI, MCP and the AI agents, Selenium to Playwright parts 1 to 3 with a new part 3, and a PR Bot session built on today's gates, open to earlier Playwright batches too. Three more AI certifications land this month. The new Playwright 4x batch starts 28 September, and the AI Tester Blueprint batch starts in October covering LangChain, Langflow, LangSmith, LangGraph, RAG, MCP, DeepEval and Ragas.

The sign-off, and it is worth keeping: if the 6:30 habit is real now, do not lose it when the class stops. Same hour, your own code.

Repository: AdvancePlaywrightFramework2x received eslint.config.mjs, the lint, lint:fix, typecheck and verify scripts, the four gate skill files plus a quality-gate skill that runs them in order, per-agent rule files for Copilot, Cursor, Windsurf, Kiro, Cline and OpenCode, AGENTS.md, docs/quality-gates.md, a pull request template, and the quality-gate CI workflow, all merged to main through two pull requests.