
How I Built an AIDLC E2E Test Case Generator Using .kiro Specs — From Feature Requirements to Passing...
How I Built an AIDLC E2E Test Case Generator Using .kiro Specs — From Feature Requirements to Passing Playwright Tests
Testing web applications end-to-end is often tedious. You write tests, they break, you rewrite them. But what if you could follow a structured, AI-driven development lifecycle (AIDLC) that generates feature requirements from your actual deployed app, then derives test cases systematically from those requirements?
In this article, I’ll walk you through how I built a complete E2E testing pipeline for a Social Media Event Attendee Banner Generator app using the .kiro spec-driven approach — going from analyzing the live DOM to generating Feature Requirements (FRs), Test Specs (TSs), and finally passing Playwright tests.
The app is a simple but functional web tool that lets users:
Deployed at: https://event-attendee-banner-generator.vercel.app/
Here’s the complete flow from app analysis to passing tests:

Instead of writing tests directly, we first establish a spec-driven structure:
.kiro/
├── docs/
│ └── product-context.md # App overview & tech stack
├── specs/
│ ├── feature-requirements/ # FR-1 through FR-7
│ │ ├── FR-1-page-load-and-layout.md
│ │ ├── FR-2-image-upload.md
│ │ ├── FR-3-image-cropping.md
│ │ ├── FR-4-attendee-info-form.md
│ │ ├── FR-5-banner-generation.md
│ │ ├── FR-6-canvas-banner-display.md
│ │ └── FR-7-usage-instructions.md
│ └── test-specs/ # TS-1 through TS-7
│ ├── TS-1-page-load-and-layout.md
│ ├── TS-2-image-upload.md
│ ├── TS-3-image-cropping.md
│ ├── TS-4-attendee-info-form.md
│ ├── TS-5-banner-generation.md
│ ├── TS-6-canvas-banner-display.md
│ └── TS-7-usage-instructions.md
├── standards/
│ └── spec-standards.md # Format rules for FRs & TSs
└── rules/
└── project-rules.md # Project conventions
e2e/
├── playwright.config.js
├── package.json
└── tests/ # Playwright implementations
├── FR-1-page-load-and-layout.spec.js
├── FR-2-image-upload.spec.js
└── ... (7 spec files)
The key insight: specs are pure markdown, tests are derived from them, not the other way around.
First, we document what the app is:
# Social Media Event Attendee Banner Generator
## Overview
A web-based tool that allows users to create custom social media
banners for Tech Events. Users upload a headshot, enter their name,
designation, and company, and generate a professional banner optimized
for LinkedIn and Instagram sharing.
## Tech Stack
- HTML5, CSS3, Vanilla JavaScript
- jQuery 3.6.0
- Cropper.js 1.5.12 (image cropping)
- Bootstrap 5.3.3 (layout/styling)
- HTML5 Canvas API (banner rendering)
## Deployed URL
https://event-attendee-banner-generator.vercel.app/
## Key Files
- `index.html` — Main page with form and canvas
- `script.js` — App logic (upload, crop, generate, download)
- `style.css` — Custom styles
Each feature gets its own markdown file with a consistent structure. Here’s an example:
# FR-2: Image Upload
## Description
User can upload a headshot image by clicking the preview area,
which triggers a hidden file input.
## Acceptance Criteria
1. A default user icon (img/user-icon.png) is displayed as placeholder
2. Clicking the preview image triggers the hidden file input
via onclick="imageUpload.click()"
3. File input (#imageUpload) has accept="image/*" and is hidden
with class d-none
4. After selecting a file, a FileReader reads it as DataURL
5. The image-to-crop element (#imageToCrop) src is updated
6. The cropper container (#cropperContainer) becomes visible
## DOM Elements
- `#imagePreview` — Preview wrapper div
- `#previewImage` — img element showing placeholder/cropped result
- `#imageUpload` — Hidden file input (type="file", accept="image/*")
- `#cropperContainer` — Container shown after upload
## Priority
High
# FR-5: Banner Generation
## Description
User clicks "Generate Banner" to render a composite banner on
the canvas with their headshot and details.
## Acceptance Criteria
1. "Generate Banner" button is visible with classes
btn btn-warning w-100 mt-3
2. Button triggers generateBanner() via onclick attribute
3. If no cropped image exists, function returns early
4. Background image is drawn on canvas at full size
5. Cropped headshot is drawn as a circular clip at position x=60
6. User name is drawn in bold 50px white Arial text
7. Designation is drawn in normal 30px white Arial text
8. Company is drawn in normal 25px white Arial text
9. If inputs are empty, defaults are used
10. Banner auto-downloads as PNG
## DOM Elements
- `button[onclick="generateBanner()"]` — Generate Banner button
- `#downloadCanvas` — Canvas element for rendering
## Priority
High
Each FR maps to a Test Spec with concrete, atomic test cases:
# Test Spec: FR-2 — Image Upload
## Source
[FR-2-image-upload.md](../feature-requirements/FR-2-image-upload.md)
## Test Cases
### TC-2.1: Default user icon displayed
- **Action**: Check `#previewImage` src attribute
- **Expected**: src contains "user-icon.png"
### TC-2.2: Clicking preview triggers file input
- **Action**: Check `#previewImage` onclick attribute
- **Expected**: onclick contains "imageUpload.click()"
### TC-2.3: File input is hidden and accepts images
- **Action**: Check `#imageUpload` attributes
- **Expected**: Has class d-none, attribute accept="image/*"
We define how specs should be authored — this ensures consistency:
## Feature Requirements (FR) Format
- File naming: `FR-{number}-{slug}.md`
- Required sections in order:
1. `# FR-{n}: {Title}`
2. `## Description`
3. `## Acceptance Criteria` — Numbered list
4. `## DOM Elements` — Relevant selectors
5. `## Priority` — High | Medium | Low
## Test Specs (TS) Format
- File naming: `TS-{number}-{slug}.md` matching its FR
- Each test case must have:
- `- **Action**:` — What to do
- `- **Expected**:` — What to assert
## Coverage
- Every acceptance criterion must have at least one test case
## General
- All spec artifacts live under `.kiro/specs/`
- No code-based generators — specs are pure markdown
- Deployed app URL is the source of truth for DOM verification
## Test Specs
- Test cases must be atomic — one assertion per test case
- Use semantic DOM selectors (#id preferred, then .class, then tag)
Now we translate the markdown test specs into executable Playwright tests:
const { test, expect } = require('@playwright/test');
test.describe('FR-1: Page Load & Layout', () => {
test('TC-1.1: Page title is correct', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(
'Social Media Sharing | Event Banner Generator | Marketing Events'
);
});
test('TC-1.2: Main heading is displayed', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toContainText(
'Event Attending Poster Generator'
);
await expect(page.locator('h1 span.alert')).toContainText('#opensource');
});
test('TC-1.3: Two-column layout renders', async ({ page }) => {
await page.goto('/');
await expect(page.locator('.col-md-4')).toBeVisible();
await expect(page.locator('.col-md-8')).toBeVisible();
});
test('TC-1.4: Bootstrap CSS is loaded', async ({ page }) => {
await page.goto('/');
await expect(
page.locator('link[href*="bootstrap"]').first()
).toHaveAttribute('href', /bootstrap/);
});
test('TC-1.5: jQuery is loaded', async ({ page }) => {
await page.goto('/');
await expect(
page.locator('script[src*="jquery"]')
).toHaveAttribute('src', /jquery/);
});
test('TC-1.6: Cropper.js is loaded', async ({ page }) => {
await page.goto('/');
await expect(
page.locator('link[href*="cropperjs"]')
).toHaveAttribute('href', /cropper/);
await expect(
page.locator('script[src*="cropper.min.js"]')
).toHaveAttribute('src', /cropper/);
});
});
const { test, expect } = require('@playwright/test');
test.describe('FR-4: Attendee Information Form', () => {
test('TC-4.1: Name input visible with placeholder', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#nameInput')).toBeVisible();
await expect(page.locator('#nameInput'))
.toHaveAttribute('placeholder', 'Full Name');
});
test('TC-4.2: Designation input visible', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#designationInput')).toBeVisible();
await expect(page.locator('#designationInput'))
.toHaveAttribute('placeholder', 'Designation');
});
test('TC-4.3: Company input visible', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#companyInput')).toBeVisible();
await expect(page.locator('#companyInput'))
.toHaveAttribute('placeholder', 'Company');
});
test('TC-4.4: Inputs accept text entry', async ({ page }) => {
await page.goto('/');
await page.fill('#nameInput', 'John Doe');
await page.fill('#designationInput', 'Engineer');
await page.fill('#companyInput', 'Acme');
await expect(page.locator('#nameInput')).toHaveValue('John Doe');
await expect(page.locator('#designationInput')).toHaveValue('Engineer');
await expect(page.locator('#companyInput')).toHaveValue('Acme');
});
});
The config serves the static app locally via http-server:
const { defineConfig } = require('@playwright/test');
const path = require('path');
module.exports = defineConfig({
testDir: './tests',
timeout: 30000,
use: {
baseURL: 'http://localhost:8080',
headless: true,
},
webServer: {
command: `npx http-server "${path.resolve(__dirname, '..')}" -p 8080 -s`,
port: 8080,
reuseExistingServer: true,
},
});
This means:
cd e2e
npm install
npx playwright test
Running 22 tests using 4 workers
✓ FR-1: Page Load & Layout › TC-1.1: Page title is correct
✓ FR-1: Page Load & Layout › TC-1.2: Main heading is displayed
✓ FR-1: Page Load & Layout › TC-1.3: Two-column layout renders
✓ FR-1: Page Load & Layout › TC-1.4: Bootstrap CSS is loaded
✓ FR-1: Page Load & Layout › TC-1.5: jQuery is loaded
✓ FR-1: Page Load & Layout › TC-1.6: Cropper.js is loaded
✓ FR-2: Image Upload › TC-2.1: Default user icon displayed
✓ FR-2: Image Upload › TC-2.2: Clicking preview triggers file input
✓ FR-2: Image Upload › TC-2.3: File input is hidden and accepts images
✓ FR-3: Image Cropping › TC-3.1: Crop button exists
✓ FR-3: Image Cropping › TC-3.2: Cropper container initially hidden
✓ FR-4: Attendee Info Form › TC-4.1: Name input
✓ FR-4: Attendee Info Form › TC-4.2: Designation input
✓ FR-4: Attendee Info Form › TC-4.3: Company input
✓ FR-4: Attendee Info Form › TC-4.4: Inputs accept text
✓ FR-5: Banner Generation › TC-5.1: Generate button visible
✓ FR-6: Canvas Banner › TC-6.1: Canvas element exists
✓ FR-6: Canvas Banner › TC-6.2: Canvas dimensions 1200x630
✓ FR-7: Usage Instructions › TC-7.1: Info area visible
✓ FR-7: Usage Instructions › TC-7.2: Heading correct
✓ FR-7: Usage Instructions › TC-7.3: Instructions listed
✓ FR-7: Usage Instructions › TC-7.4: Hashtags displayed
22 passed (5.5s)
npx playwright test --headed --reporter=html
npx playwright show-report
Here’s how everything connects:


Requirements-First, Not Test-First By writing Feature Requirements before tests, you ensure you’re testing what matters — not just what’s easy to test.
Full Traceability Every test maps back to an acceptance criterion, which maps to a feature. When a test fails, you know exactly which requirement is broken.
Living Documentation
The .kiro/specs/ folder serves as both documentation and test source. New team members can read the FRs to understand the app without running it.
Spec-Driven, Not Code-Driven The markdown specs are the source of truth. Playwright code is just the implementation of those specs. If you switch test frameworks, the specs remain valid.
AI-Friendly The structured format makes it trivial for AI agents (like Kiro) to:
.kiro/docs/product-context.md — Document your app’s purpose and stack — headed mode for visual verificationi want to develop an automated aidlc e2e test case generator for the
current project by visiting the deployed url of the app and the codebase
aidlc docs in a way that it first visits the app url and verify the
input elements and DOM elements and page ui code and everything and
reads the aidl-docs of the app and finally generates the feature
requirements files in separate folder by naming FR-1-image preview
like this, then generates the test cases spec files from FR files.
Kiro analyzed the codebase, visited the deployed URL, and initially built a JavaScript-based generator. But I wanted it the .kiro way…
it should always use .kiro way not using javascript
This single prompt made Kiro pivot entirely — removing the JS generator and creating pure markdown Feature Requirements and Test Specs following the .kiro spec-driven approach.
yes use .kiro way only with md files and everything to make the context
Kiro then generated all 7 FR files, 7 TS files, and the product context document — all as structured markdown.
also create standard and rule files for the patterns we are following in this
This produced the spec-standards.md and project-rules.md — codifying the naming conventions, format rules, and coverage requirements.
now run e2e-aidlc and see test cases pass
Kiro translated the markdown test specs into Playwright implementations, set up the test infrastructure, and ran them — 22 tests, all passing.
The AIDLC approach transforms testing from an afterthought into a structured, traceable process. By leveraging .kiro specs as the single source of truth, you get:
The complete source code is available on GitHub.
https://github.com/muhammadawaisshaikh/social-media-event-attendee-banner-generator/pull/1
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...
Workflows from the Neura Market marketplace related to this DeepSeek resource