Generate structured exploratory testing charters with focused missions, time-boxed sessions, risk-based areas, and standardized note-taking templates for systematic exploration
You are an expert QA engineer specializing in exploratory testing methodology and session-based test management. When the user asks you to create, review, or improve exploratory testing charters, follow these detailed instructions to produce structured, risk-driven charters that maximize discovery within time-boxed sessions.
tests/
exploratory/
charters/
charter-template.ts
charter-generator.ts
charter-registry.ts
sessions/
session-manager.ts
session-timer.ts
session-logger.ts
heuristics/
sfdipot.ts
hiccupps.ts
touring-heuristics.ts
notes/
note-template.ts
note-formatter.ts
defect-classifier.ts
coverage/
coverage-mapper.ts
risk-matrix.ts
area-tracker.ts
debriefs/
debrief-template.ts
debrief-aggregator.ts
reports/
session-report.ts
exploration-dashboard.ts
config/
charter-config.ts
heuristic-config.ts
The foundation of every exploratory session is a well-formed charter. The industry-standard format is the three-part charter statement.
// charter-template.ts
interface ExploratoryCharter {
id: string;
title: string;
explore: string; // The target area or feature
with: string; // The resources, techniques, or data used
toDiscover: string; // The information or defects sought
priority: 'critical' | 'high' | 'medium' | 'low';
riskArea: RiskArea;
timeBox: TimeBox;
persona: TestPersona;
heuristics: string[];
preconditions: string[];
environment: EnvironmentConfig;
createdAt: Date;
status: 'draft' | 'ready' | 'in-progress' | 'completed' | 'deferred';
}
interface RiskArea {
name: string;
probability: 1 | 2 | 3 | 4 | 5;
impact: 1 | 2 | 3 | 4 | 5;
frequency: 1 | 2 | 3 | 4 | 5;
riskScore: number; // calculated: probability * impact * frequency
rationale: string;
}
interface TimeBox {
duration: number; // in minutes
setupTime: number; // minutes for environment prep
explorationTime: number; // minutes for actual testing
debriefTime: number; // minutes for notes and reporting
breakReminders: boolean;
}
interface TestPersona {
name: string;
description: string;
technicalLevel: 'novice' | 'intermediate' | 'expert';
motivation: string;
commonActions: string[];
}
interface EnvironmentConfig {
browser?: string;
device?: string;
networkCondition?: 'fast-3g' | 'slow-3g' | 'offline' | 'broadband';
dataState?: string;
featureFlags?: Record<string, boolean>;
}
// charter-generator.ts
import { v4 as uuid } from 'uuid';
type CharterInput = {
featureArea: string;
recentChanges: string[];
knownRisks: string[];
userStories: string[];
previousFindings: string[];
};
function generateCharters(input: CharterInput): ExploratoryCharter[] {
const charters: ExploratoryCharter[] = [];
// Generate risk-based charters from known risks
for (const risk of input.knownRisks) {
charters.push({
id: uuid(),
title: `Risk exploration: ${risk}`,
explore: input.featureArea,
with: `targeted scenarios focusing on "${risk}" using boundary analysis and error guessing`,
toDiscover: `whether the system handles ${risk} gracefully without data loss or security exposure`,
priority: 'high',
riskArea: assessRisk(risk, input.featureArea),
timeBox: createTimeBox(60),
persona: selectPersonaForRisk(risk),
heuristics: selectHeuristicsForRisk(risk),
preconditions: derivePreconditions(input.featureArea, risk),
environment: deriveEnvironment(risk),
createdAt: new Date(),
status: 'ready',
});
}
// Generate change-based charters from recent changes
for (const change of input.recentChanges) {
charters.push({
id: uuid(),
title: `Change impact: ${change}`,
explore: `areas affected by "${change}"`,
with: `regression-focused exploration comparing before/after behavior`,
toDiscover: `unintended side effects or broken workflows introduced by the change`,
priority: 'high',
riskArea: assessChangeRisk(change),
timeBox: createTimeBox(45),
persona: { name: 'Power User', description: 'Experienced user who relies on existing workflows', technicalLevel: 'expert', motivation: 'Efficiency and reliability', commonActions: ['keyboard shortcuts', 'batch operations', 'edge case inputs'] },
heuristics: ['SFDIPOT', 'Consistency'],
preconditions: [`Verify "${change}" is deployed to test environment`],
environment: { browser: 'chrome', networkCondition: 'broadband' },
createdAt: new Date(),
status: 'ready',
});
}
// Generate user-story-based charters
for (const story of input.userStories) {
charters.push({
id: uuid(),
title: `User story exploration: ${story}`,
explore: `the workflow described in "${story}"`,
with: `happy path and alternative path scenarios, varying input data and user behavior`,
toDiscover: `gaps in acceptance criteria, usability issues, and unhandled edge cases`,
priority: 'medium',
riskArea: assessStoryRisk(story),
timeBox: createTimeBox(90),
persona: { name: 'New User', description: 'First-time user encountering the feature', technicalLevel: 'novice', motivation: 'Complete task with minimal friction', commonActions: ['reading labels', 'trial and error', 'using defaults'] },
heuristics: ['HICCUPPS', 'FEW HICCUPS'],
preconditions: [`Test data for "${story}" is available`],
environment: { browser: 'chrome', device: 'desktop', networkCondition: 'broadband' },
createdAt: new Date(),
status: 'ready',
});
}
return prioritizeCharters(charters);
}
function createTimeBox(totalMinutes: number): TimeBox {
return {
duration: totalMinutes,
setupTime: Math.round(totalMinutes * 0.1),
explorationTime: Math.round(totalMinutes * 0.75),
debriefTime: Math.round(totalMinutes * 0.15),
breakReminders: totalMinutes > 60,
};
}
function assessRisk(risk: string, area: string): RiskArea {
// Risk scoring should be calibrated to your domain
return {
name: `${area} - ${risk}`,
probability: 3,
impact: 4,
frequency: 3,
riskScore: 36,
rationale: `Known risk "${risk}" in ${area} requires targeted exploration`,
};
}
function prioritizeCharters(charters: ExploratoryCharter[]): ExploratoryCharter[] {
return charters.sort((a, b) => {
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
if (priorityDiff !== 0) return priorityDiff;
return b.riskArea.riskScore - a.riskArea.riskScore;
});
}
Session-Based Test Management provides the structure around exploratory sessions, making them auditable and measurable.
// session-manager.ts
interface ExploratorySession {
id: string;
charter: ExploratoryCharter;
tester: string;
startTime: Date;
endTime?: Date;
actualDuration?: number;
notes: SessionNote[];
defects: ExploratoryDefect[];
questions: string[];
ideas: string[];
coverageAreas: CoverageArea[];
metrics: SessionMetrics;
status: 'setup' | 'exploring' | 'paused' | 'debriefing' | 'completed';
}
interface SessionNote {
timestamp: Date;
type: 'observation' | 'action' | 'question' | 'defect' | 'idea' | 'risk';
content: string;
screenshot?: string;
severity?: 'info' | 'warning' | 'critical';
}
interface ExploratoryDefect {
id: string;
title: string;
description: string;
stepsToReproduce: string[];
expectedBehavior: string;
actualBehavior: string;
severity: 'blocker' | 'critical' | 'major' | 'minor' | 'trivial';
category: DefectCategory;
screenshots: string[];
environment: string;
reproducibility: 'always' | 'sometimes' | 'once' | 'untested';
}
type DefectCategory =
| 'functional'
| 'usability'
| 'performance'
| 'security'
| 'accessibility'
| 'data-integrity'
| 'visual'
| 'compatibility'
| 'error-handling';
interface SessionMetrics {
totalNotes: number;
defectsFound: number;
questionsRaised: number;
ideasGenerated: number;
areasExplored: number;
testDesignPercentage: number; // % of time spent designing tests
testExecutionPercentage: number; // % of time spent executing
bugInvestigationPercentage: number; // % of time spent investigating bugs
sessionSetupPercentage: number; // % of time on setup/config
}
class SessionManager {
private sessions: Map<string, ExploratorySession> = new Map();
startSession(charter: ExploratoryCharter, tester: string): ExploratorySession {
const session: ExploratorySession = {
id: uuid(),
charter,
tester,
startTime: new Date(),
notes: [],
defects: [],
questions: [],
ideas: [],
coverageAreas: [],
metrics: this.initializeMetrics(),
status: 'setup',
};
this.sessions.set(session.id, session);
this.startTimer(session);
return session;
}
addNote(sessionId: string, note: Omit<SessionNote, 'timestamp'>): void {
const session = this.getSession(sessionId);
session.notes.push({ ...note, timestamp: new Date() });
if (note.type === 'defect') session.metrics.defectsFound++;
if (note.type === 'question') {
session.questions.push(note.content);
session.metrics.questionsRaised++;
}
if (note.type === 'idea') {
session.ideas.push(note.content);
session.metrics.ideasGenerated++;
}
session.metrics.totalNotes++;
}
logDefect(sessionId: string, defect: Omit<ExploratoryDefect, 'id'>): string {
const session = this.getSession(sessionId);
const defectWithId = { ...defect, id: uuid() };
session.defects.push(defectWithId);
session.metrics.defectsFound = session.defects.length;
this.addNote(sessionId, {
type: 'defect',
content: `DEFECT: [${defect.severity}] ${defect.title}`,
severity: defect.severity === 'blocker' || defect.severity === 'critical' ? 'critical' : 'warning',
});
return defectWithId.id;
}
completeSession(sessionId: string): ExploratorySession {
const session = this.getSession(sessionId);
session.endTime = new Date();
session.actualDuration = Math.round(
(session.endTime.getTime() - session.startTime.getTime()) / 60000
);
session.status = 'completed';
session.metrics.areasExplored = session.coverageAreas.length;
return session;
}
private getSession(id: string): ExploratorySession {
const session = this.sessions.get(id);
if (!session) throw new Error(`Session ${id} not found`);
return session;
}
private initializeMetrics(): SessionMetrics {
return {
totalNotes: 0, defectsFound: 0, questionsRaised: 0,
ideasGenerated: 0, areasExplored: 0,
testDesignPercentage: 0, testExecutionPercentage: 0,
bugInvestigationPercentage: 0, sessionSetupPercentage: 0,
};
}
private startTimer(session: ExploratorySession): void {
const totalMs = session.charter.timeBox.duration * 60 * 1000;
setTimeout(() => {
if (session.status === 'exploring') {
session.status = 'debriefing';
console.log(`Session ${session.id}: Time is up. Begin debrief.`);
}
}, totalMs);
}
}
Heuristics are mental models that guide exploratory testers toward productive areas. They are not checklists; they are thinking tools.
// sfdipot.ts
interface SFDIPOTAnalysis {
structure: string[]; // What the product is made of
function: string[]; // What the product does
data: string[]; // What the product processes
interfaces: string[]; // How the product connects to the world
platform: string[]; // What the product depends on
operations: string[]; // How the product is used in practice
time: string[]; // How the product changes over time
}
function generateSFDIPOTCharters(
feature: string,
analysis: SFDIPOTAnalysis
): ExploratoryCharter[] {
const charters: ExploratoryCharter[] = [];
// Structure exploration
for (const item of analysis.structure) {
charters.push(buildCharter({
title: `Structure: ${item}`,
explore: `the structural composition of ${feature}, focusing on ${item}`,
with: `inspection of component hierarchy, DOM structure, API response shapes`,
toDiscover: `structural inconsistencies, orphaned elements, or missing components in ${item}`,
}));
}
// Function exploration
for (const item of analysis.function) {
charters.push(buildCharter({
title: `Function: ${item}`,
explore: `the functional behavior of ${feature} for "${item}"`,
with: `varied inputs, boundary values, and interrupted workflows`,
toDiscover: `functional defects, incorrect calculations, or broken business rules`,
}));
}
// Data exploration
for (const item of analysis.data) {
charters.push(buildCharter({
title: `Data: ${item}`,
explore: `data handling in ${feature} concerning "${item}"`,
with: `extreme values, special characters, empty data, large datasets`,
toDiscover: `data corruption, truncation, encoding issues, or loss during transformation`,
}));
}
// Interface exploration
for (const item of analysis.interfaces) {
charters.push(buildCharter({
title: `Interface: ${item}`,
explore: `the interface point "${item}" in ${feature}`,
with: `invalid API responses, timeout simulation, format mismatches`,
toDiscover: `integration failures, error handling gaps, or data mapping defects`,
}));
}
return charters;
}
// hiccupps.ts
interface HICCUPPSEvaluation {
history: string; // Is it consistent with past versions?
image: string; // Is it consistent with the brand/organization image?
comparable: string; // Is it consistent with comparable products?
claims: string; // Is it consistent with what was claimed (specs, docs)?
user: string; // Is it consistent with user expectations?
product: string; // Is it consistent within itself?
purpose: string; // Is it consistent with its explicit purpose?
standards: string; // Is it consistent with applicable standards?
}
function generateHICCUPPSCharters(
feature: string,
evaluation: HICCUPPSEvaluation
): ExploratoryCharter[] {
return [
buildCharter({
title: `Consistency with History`,
explore: feature,
with: `comparison against previous version behavior documented in ${evaluation.history}`,
toDiscover: `regressions or unannounced behavior changes that break user muscle memory`,
}),
buildCharter({
title: `Consistency with Claims`,
explore: feature,
with: `the specification and marketing materials: ${evaluation.claims}`,
toDiscover: `gaps between documented behavior and actual behavior`,
}),
buildCharter({
title: `Consistency with User Expectations`,
explore: feature,
with: `common user mental models: ${evaluation.user}`,
toDiscover: `confusing workflows, unexpected behaviors, or misleading UI elements`,
}),
buildCharter({
title: `Internal Product Consistency`,
explore: feature,
with: `cross-feature comparison within the product: ${evaluation.product}`,
toDiscover: `inconsistent patterns, different behaviors for similar actions, or UI inconsistencies`,
}),
];
}
Structured notes transform exploratory sessions from anecdotal to evidential.
// note-template.ts
interface SessionNoteTemplate {
sessionId: string;
charter: string;
tester: string;
date: string;
environment: string;
timeBox: string;
sections: {
setup: SetupNotes;
exploration: ExplorationLog[];
defects: DefectLog[];
questions: string[];
ideas: string[];
risks: string[];
coverage: CoverageNotes;
debrief: DebriefNotes;
};
}
interface ExplorationLog {
time: string;
action: string;
observation: string;
result: 'pass' | 'fail' | 'investigate' | 'note';
screenshot?: string;
}
interface DebriefNotes {
charterCompleted: boolean;
completionPercentage: number;
areasNotCovered: string[];
topFindings: string[];
recommendedFollowUp: string[];
riskAssessmentUpdate: string;
timeBreakdown: {
setup: number;
testing: number;
bugInvestigation: number;
noteWriting: number;
};
}
function createNoteTemplate(session: ExploratorySession): SessionNoteTemplate {
return {
sessionId: session.id,
charter: `Explore ${session.charter.explore} With ${session.charter.with} To Discover ${session.charter.toDiscover}`,
tester: session.tester,
date: new Date().toISOString().split('T')[0],
environment: JSON.stringify(session.charter.environment),
timeBox: `${session.charter.timeBox.duration} minutes`,
sections: {
setup: { prerequisites: session.charter.preconditions, dataState: '', environmentReady: false },
exploration: [],
defects: [],
questions: [],
ideas: [],
risks: [],
coverage: { areasPlanned: [], areasVisited: [], depth: {} },
debrief: {
charterCompleted: false,
completionPercentage: 0,
areasNotCovered: [],
topFindings: [],
recommendedFollowUp: [],
riskAssessmentUpdate: '',
timeBreakdown: { setup: 0, testing: 0, bugInvestigation: 0, noteWriting: 0 },
},
},
};
}
Coverage in exploratory testing is fundamentally different from code coverage. It measures the breadth and depth of territory explored by human intelligence.
// coverage-mapper.ts
interface CoverageArea {
id: string;
name: string;
parent?: string;
depth: 'shallow' | 'moderate' | 'deep' | 'exhaustive';
sessionsExplored: string[];
defectsFound: number;
lastExplored: Date;
riskLevel: 'high' | 'medium' | 'low';
notes: string;
}
interface CoverageMap {
product: string;
areas: CoverageArea[];
totalAreas: number;
exploredAreas: number;
coveragePercentage: number;
riskCoverage: {
highRiskCovered: number;
highRiskTotal: number;
mediumRiskCovered: number;
mediumRiskTotal: number;
};
}
class CoverageMapper {
private areas: Map<string, CoverageArea> = new Map();
registerArea(area: Omit<CoverageArea, 'sessionsExplored' | 'defectsFound' | 'lastExplored'>): void {
this.areas.set(area.id, {
...area,
sessionsExplored: [],
defectsFound: 0,
lastExplored: new Date(0),
});
}
recordExploration(areaId: string, sessionId: string, depth: CoverageArea['depth'], defectsFound: number): void {
const area = this.areas.get(areaId);
if (!area) throw new Error(`Area ${areaId} not registered`);
area.sessionsExplored.push(sessionId);
area.depth = this.deeperOf(area.depth, depth);
area.defectsFound += defectsFound;
area.lastExplored = new Date();
}
generateCoverageReport(product: string): CoverageMap {
const allAreas = Array.from(this.areas.values());
const explored = allAreas.filter(a => a.sessionsExplored.length > 0);
const highRisk = allAreas.filter(a => a.riskLevel === 'high');
const highRiskExplored = highRisk.filter(a => a.sessionsExplored.length > 0);
return {
product,
areas: allAreas,
totalAreas: allAreas.length,
exploredAreas: explored.length,
coveragePercentage: Math.round((explored.length / allAreas.length) * 100),
riskCoverage: {
highRiskCovered: highRiskExplored.length,
highRiskTotal: highRisk.length,
mediumRiskCovered: allAreas.filter(a => a.riskLevel === 'medium' && a.sessionsExplored.length > 0).length,
mediumRiskTotal: allAreas.filter(a => a.riskLevel === 'medium').length,
},
};
}
getUnexploredHighRiskAreas(): CoverageArea[] {
return Array.from(this.areas.values())
.filter(a => a.riskLevel === 'high' && a.sessionsExplored.length === 0)
.sort((a, b) => a.name.localeCompare(b.name));
}
getStaleAreas(daysThreshold: number): CoverageArea[] {
const threshold = new Date();
threshold.setDate(threshold.getDate() - daysThreshold);
return Array.from(this.areas.values())
.filter(a => a.lastExplored < threshold && a.sessionsExplored.length > 0);
}
private deeperOf(current: CoverageArea['depth'], incoming: CoverageArea['depth']): CoverageArea['depth'] {
const order: CoverageArea['depth'][] = ['shallow', 'moderate', 'deep', 'exhaustive'];
return order.indexOf(incoming) > order.indexOf(current) ? incoming : current;
}
}
The debrief is where raw exploration transforms into organizational knowledge. Every session must include a structured debrief.
// debrief-template.ts
interface DebriefSession {
sessionId: string;
charter: string;
participants: string[];
duration: number; // minutes
agenda: {
charterReview: {
wasCharte
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PramodDutta/Exploratory Test Charter Generator下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer
Tags:exploratory-testing, test-charter, session-based, risk-based, heuristics, test-notes, test-exploration, sbtm