Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/annotations-debug-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@lytics/playwright-annotations": minor
---

Add debug mode for troubleshooting annotation issues using @lytics/kero logger

Set `PLAYWRIGHT_ANNOTATIONS_DEBUG=true` to enable verbose logging that shows:
- When each annotation helper is called
- What annotations exist at each step
- Validation results and errors

Example usage:
```bash
PLAYWRIGHT_ANNOTATIONS_DEBUG=true npx playwright test
```

This release adds `@lytics/kero` as a dependency for structured logging with pretty output in development and JSON in production.

2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ pnpm-debug.log*

# OS
.DS_Store
Thumbs.db
Thumbs.db.dev-agent/
4 changes: 4 additions & 0 deletions packages/annotations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"@types/node": "^22.0.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@lytics/kero": "^1.0.0"
}
}
157 changes: 107 additions & 50 deletions packages/annotations/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { createLogger } from '@lytics/kero';
import type { TestInfo } from '@playwright/test';
import type { TestAnnotations, ContentstackAnnotations } from './types';
import type { ContentstackAnnotations, TestAnnotations } from './types';
import { validateContentstackAnnotations } from './validators';

/**
* Logger for playwright-annotations
*
* Enable debug logging with:
* ```bash
* PLAYWRIGHT_ANNOTATIONS_DEBUG=true npx playwright test
* ```
*
* Or set LOG_LEVEL=debug for more control.
*/
const logger = createLogger({
preset: process.env.NODE_ENV === 'production' ? 'production' : 'development',
level: process.env.PLAYWRIGHT_ANNOTATIONS_DEBUG ? 'debug' : 'warn',
context: { pkg: 'playwright-annotations' },
});

/**
* Generic helper: Push any annotations to a test
*
*
* This is the base helper that works with any annotation schema.
* Teams can use this directly or create schema-specific wrappers.
*
*
* @example
* ```typescript
* // With custom schema
* interface MyAnnotations extends TestAnnotations {
* featureArea: string;
* ticketId: string;
* }
*
*
* pushAnnotations(testInfo, {
* featureArea: "auth",
* ticketId: "JIRA-123"
Expand All @@ -26,37 +43,45 @@ export function pushAnnotations<T extends TestAnnotations>(
testInfo: TestInfo,
annotations: T
): void {
logger.debug(
{ testTitle: testInfo.title, annotations, existingAnnotations: testInfo.annotations },
'pushAnnotations called'
);

for (const [key, value] of Object.entries(annotations)) {
if (value !== undefined) {
const description = Array.isArray(value) ? value.join(', ') : value;
logger.debug({ key, description }, 'Adding annotation');
testInfo.annotations.push({
type: key,
description,
});
}
}

logger.debug({ finalAnnotations: testInfo.annotations }, 'pushAnnotations complete');
}

/**
* Extract generic annotations from a test case
*
*
* @example
* ```typescript
* const annotations = getAnnotations(test);
* // Returns: { [key: string]: string }
* ```
*/
export function getAnnotations<T extends TestAnnotations = TestAnnotations>(
test: { annotations: Array<{ type: string; description?: string }> }
): Partial<T> {
export function getAnnotations<T extends TestAnnotations = TestAnnotations>(test: {
annotations: Array<{ type: string; description?: string }>;
}): Partial<T> {
const result: Record<string, string> = {};

for (const annotation of test.annotations) {
if (annotation.description) {
result[annotation.type] = annotation.description;
}
}

return result as Partial<T>;
}

Expand All @@ -69,9 +94,9 @@ export function getAnnotations<T extends TestAnnotations = TestAnnotations>(

/**
* Contentstack helper: Push suite-level annotation
*
*
* Use in `beforeEach` at describe level to set the suite for all tests.
*
*
* @example
* ```typescript
* test.describe("My Feature", () => {
Expand All @@ -81,22 +106,26 @@ export function getAnnotations<T extends TestAnnotations = TestAnnotations>(
* });
* ```
*/
export function pushSuiteAnnotation(
testInfo: TestInfo,
testSuiteName: string
): void {
export function pushSuiteAnnotation(testInfo: TestInfo, testSuiteName: string): void {
logger.debug(
{ testTitle: testInfo.title, testSuiteName, existingAnnotations: testInfo.annotations },
'pushSuiteAnnotation called'
);

testInfo.annotations.push({
type: 'testSuiteName',
description: testSuiteName,
});

logger.debug({ finalAnnotations: testInfo.annotations }, 'pushSuiteAnnotation complete');
}

/**
* Contentstack helper: Push test-level annotations with validation
*
*
* Use in individual `test()` blocks. Validates annotations before pushing.
* Requires `testSuiteName` to be set first via `pushSuiteAnnotation`.
*
*
* @example
* ```typescript
* test("my test", async ({}, testInfo) => {
Expand All @@ -106,7 +135,7 @@ export function pushSuiteAnnotation(
* });
* });
* ```
*
*
* @throws {Error} If testSuiteName not set or validation fails
*/
export function pushTestAnnotations(
Expand All @@ -116,49 +145,69 @@ export function pushTestAnnotations(
testCaseId: string;
}
): void {
logger.debug(
{ testTitle: testInfo.title, annotations, existingAnnotations: testInfo.annotations },
'pushTestAnnotations called'
);

// Get suite name from existing annotations
const testSuiteName = testInfo.annotations.find(
a => a.type === 'testSuiteName'
)?.description;
const testSuiteName = testInfo.annotations.find((a) => a.type === 'testSuiteName')?.description;

logger.debug(
{
found: !!testSuiteName,
testSuiteName,
allAnnotationTypes: testInfo.annotations.map((a) => a.type),
},
'Looking for testSuiteName in existing annotations'
);

if (!testSuiteName) {
throw new Error(
const errorMsg =
'testSuiteName must be set before calling pushTestAnnotations. ' +
'Use pushSuiteAnnotation in beforeEach first.'
);
'Use pushSuiteAnnotation in beforeEach first.';
logger.error({ existingAnnotations: testInfo.annotations }, 'testSuiteName not found');
throw new Error(errorMsg);
}

// Validate complete annotation set
const validation = validateContentstackAnnotations({
const fullAnnotations = {
testSuiteName,
journeyId: annotations.journeyId,
testCaseId: annotations.testCaseId,
});
};

logger.debug(fullAnnotations, 'Validating annotations');

const validation = validateContentstackAnnotations(fullAnnotations);

logger.debug({ ...validation }, 'Validation result');

if (!validation.valid) {
throw new Error(
`Invalid annotations:\n${validation.errors.join('\n')}`
);
const errorMsg = `Invalid annotations:\n${validation.errors.join('\n')}`;
logger.error({ errors: validation.errors, annotations: fullAnnotations }, 'Validation failed');
throw new Error(errorMsg);
}

if (validation.warnings.length > 0) {
console.warn(
`⚠️ Annotation warnings:\n${validation.warnings.join('\n')}`
);
logger.warn({ warnings: validation.warnings }, 'Annotation warnings');
}

// Push annotations
logger.debug('Pushing journeyId and testCaseId annotations');
testInfo.annotations.push(
{ type: 'journeyId', description: annotations.journeyId },
{ type: 'testCaseId', description: annotations.testCaseId }
);

logger.debug({ finalAnnotations: testInfo.annotations }, 'pushTestAnnotations complete');
}

/**
* Contentstack helper: Push all annotations at once
*
*
* Alternative to separate suite/test helpers. Validates before pushing.
*
*
* @example
* ```typescript
* test("my test", async ({}, testInfo) => {
Expand All @@ -169,53 +218,61 @@ export function pushTestAnnotations(
* });
* });
* ```
*
*
* @throws {Error} If validation fails
*/
export function pushContentstackAnnotations(
testInfo: TestInfo,
annotations: ContentstackAnnotations
): void {
logger.debug(
{ testTitle: testInfo.title, annotations, existingAnnotations: testInfo.annotations },
'pushContentstackAnnotations called'
);

logger.debug({ ...annotations }, 'Validating annotations');
const validation = validateContentstackAnnotations(annotations);
logger.debug({ ...validation }, 'Validation result');

if (!validation.valid) {
throw new Error(
`Invalid annotations:\n${validation.errors.join('\n')}`
);
const errorMsg = `Invalid annotations:\n${validation.errors.join('\n')}`;
logger.error({ errors: validation.errors, annotations }, 'Validation failed');
throw new Error(errorMsg);
}

if (validation.warnings.length > 0) {
console.warn(
`⚠️ Annotation warnings:\n${validation.warnings.join('\n')}`
);
logger.warn({ warnings: validation.warnings }, 'Annotation warnings');
}

logger.debug('Pushing all annotations');
testInfo.annotations.push(
{ type: 'testSuiteName', description: annotations.testSuiteName },
{ type: 'journeyId', description: annotations.journeyId },
{ type: 'testCaseId', description: annotations.testCaseId }
);

logger.debug({ finalAnnotations: testInfo.annotations }, 'pushContentstackAnnotations complete');
}

/**
* Contentstack helper: Extract Contentstack annotations from a test case
*
*
* @example
* ```typescript
* const annotations = getContentstackAnnotations(test);
* if (annotations) {
* console.log(annotations.journeyId);
* }
* ```
*
*
* @returns Contentstack annotations or null if incomplete
*/
export function getContentstackAnnotations(
test: { annotations: Array<{ type: string; description?: string }> }
): ContentstackAnnotations | null {
const testSuiteName = test.annotations.find(a => a.type === 'testSuiteName')?.description;
const journeyId = test.annotations.find(a => a.type === 'journeyId')?.description;
const testCaseId = test.annotations.find(a => a.type === 'testCaseId')?.description;
export function getContentstackAnnotations(test: {
annotations: Array<{ type: string; description?: string }>;
}): ContentstackAnnotations | null {
const testSuiteName = test.annotations.find((a) => a.type === 'testSuiteName')?.description;
const journeyId = test.annotations.find((a) => a.type === 'journeyId')?.description;
const testCaseId = test.annotations.find((a) => a.type === 'testCaseId')?.description;

if (!testSuiteName || !journeyId || !testCaseId) {
return null;
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading