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.
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.
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:
// 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.
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.
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:
eslint-add -> PR #1 -> main
quality-gate -> PR #2 -> main
What landed for the first half:
npm install --save-dev eslint @eslint/js typescript-eslint eslint-plugin-playwright
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.
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:
"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.
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:
- 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:
- 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:
- 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.
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.
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:
.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
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.
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.
- 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.
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.