Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@babel/core": "^7.27.4",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@playwright/test": "^1.59.1",
"@repo/eslint-config": "workspace:*",
Comment on lines +55 to 56

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@playwright/test is required at runtime by the published reporter (import ... from '@playwright/test/reporter'), but it’s added under devDependencies, which won’t be installed for consumers. Move it to peerDependencies (optionally peerDependenciesMeta.optional) or dependencies so the reporter can be required in downstream projects.

Copilot uses AI. Check for mistakes.
"@repo/typescript-config": "workspace:*",
"@rollup/plugin-babel": "^6.0.4",
Expand Down
92 changes: 91 additions & 1 deletion apps/npm/src/helper/context.ts
Original file line number Diff line number Diff line change
@@ -1 +1,91 @@
// Detects current test framework runtime
type Framework = 'playwright' | 'jest' | 'vitest' | 'unknown';

function detectFramework(): Framework {
const env = process.env;
if (env['TEST_WORKER_INDEX'] !== undefined) return 'playwright';
if (env['JEST_WORKER_ID'] !== undefined) return 'jest';
if (env['VITEST'] !== undefined) return 'vitest';
return 'unknown';
}

interface ContextAdapter {
set(type: string, value: string): void;
}

/**
* Pushes metadata into test.info().annotations.
* The reporter reads these back via test.annotations in onTestEnd.
*/
class PlaywrightContext implements ContextAdapter {
set(type: string, value: string): void {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { test } = require('@playwright/test');
const info = test.info();
if (info) {
info.annotations.push({ type, description: value });
}
} catch {
// Not inside a Playwright worker — silently ignore.
}
}
}

/**
* For runners without per-test annotation APIs, we use a module-level Map.
* Key = current test name (from expect.getState()), value = annotations.
*/
const globalStore = new Map<
string,
Array<{ type: string; description: string }>
>();

class GlobalStoreContext implements ContextAdapter {
set(type: string, value: string): void {
const testName = this.currentTestName() ?? '__default__';
let entries = globalStore.get(testName);
if (!entries) {
entries = [];
globalStore.set(testName, entries);
}
entries.push({ type, description: value });
}

private currentTestName(): string | undefined {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { expect } = require('expect');
const state = expect.getState() as {
currentTestName?: string;
};
Comment on lines +56 to +60

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GlobalStoreContext relies on require('expect'), but this package doesn’t declare expect as a dependency/peerDependency. In Vitest projects in particular, expect may be available only as a global, so this require can fail and all metadata falls back to the __default__ bucket (mixing tests). Consider using globalThis.expect when present, and/or documenting/declaring expect as a (peer) dependency for Jest/Vitest support.

Suggested change
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { expect } = require('expect');
const state = expect.getState() as {
currentTestName?: string;
};
type ExpectLike = {
getState(): {
currentTestName?: string;
};
};
const globalExpect = (
globalThis as typeof globalThis & { expect?: ExpectLike }
).expect;
const expectInstance =
globalExpect ??
(() => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { expect } = require('expect');
return expect as ExpectLike | undefined;
})();
if (!expectInstance || typeof expectInstance.getState !== 'function') {
return undefined;
}
const state = expectInstance.getState();

Copilot uses AI. Check for mistakes.
return state.currentTestName ?? undefined;
} catch {
return undefined;
}
}
}

/** Drain and clear stored annotations for a Jest/Vitest test name. */
export function drainGlobalStore(
testName: string,
): Array<{ type: string; description: string }> {
const key = globalStore.has(testName) ? testName : '__default__';
const entries = globalStore.get(key) ?? [];
globalStore.delete(key);
return entries;
}

let cachedContext: ContextAdapter | null = null;

export function getContext(): ContextAdapter {
if (!cachedContext) {
const fw = detectFramework();
cachedContext =
fw === 'playwright'
? new PlaywrightContext()
: new GlobalStoreContext();
}
return cachedContext;
}


63 changes: 62 additions & 1 deletion apps/npm/src/helper/flush.ts
Original file line number Diff line number Diff line change
@@ -1 +1,62 @@
// Exposes collected metadata to reporters
import type { AssertiveMetadata } from './types';
import { drainGlobalStore } from './context';

Comment on lines +1 to +3

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

./types is imported here but no apps/npm/src/helper/types.ts (or similar) exists in the repository, so this new helper will not compile. Add the missing types module or correct the import path.

Suggested change
import type { AssertiveMetadata } from './types';
import { drainGlobalStore } from './context';
import { drainGlobalStore } from './context';
export type AssertiveMetadata = {
testId: string | undefined;
tags: string[];
owner: string | undefined;
priority: string | undefined;
testType: string | undefined;
customFields: Record<string, string>;
attachments: Record<string, string>;
};

Copilot uses AI. Check for mistakes.
/**
* Parse an array of { type, description } annotations
* (from Playwright's test.annotations or the global store)
* into a structured AssertiveMetadata object.
*/
export function parseAnnotations(
annotations: ReadonlyArray<{ type: string; description?: string }>,
): AssertiveMetadata {
const meta: AssertiveMetadata = {
testId: undefined,
tags: [],
owner: undefined,
priority: undefined,
testType: undefined,
customFields: {},
attachments: {},
};

for (const ann of annotations) {
const val = ann.description ?? '';

switch (ann.type) {
case 'assertive_id':
meta.testId = val;
break;
case 'assertive_tag':
meta.tags.push(val);
break;
case 'assertive_owner':
meta.owner = val;
break;
case 'assertive_priority':
meta.priority = val;
break;
case 'assertive_type':
meta.testType = val;
break;
default:
if (ann.type.startsWith('assertive_field_')) {
meta.customFields[ann.type.slice('assertive_field_'.length)] =
val;
} else if (ann.type.startsWith('assertive_attach_')) {
meta.attachments[ann.type.slice('assertive_attach_'.length)] =
val;
}
break;
}
}

return meta;
}

/**
* For Jest / Vitest: drain the global store for a given test name
* and return parsed metadata.
*/
export function flushGlobalStore(testName: string): AssertiveMetadata {
return parseAnnotations(drainGlobalStore(testName));
}
60 changes: 59 additions & 1 deletion apps/npm/src/helper/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,59 @@
// assertive.id(), .tags(), .owner()
import type { Priority, TestType } from './types';
import { getContext } from './context';
Comment on lines +1 to +2

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file imports ./types, but there is no apps/npm/src/helper/types.ts (or equivalent) in the repo, so tsc/bundling will fail with “Cannot find module './types'”. Add the missing types module (and export Priority, TestType, etc.) or update the import to the correct existing path.

Copilot uses AI. Check for mistakes.

/**
* Universal helper client — import this in test files.
*
* import { assertive } from 'getassertive/helper';
*
* test('User can checkout', async ({ page }) => {
* assertive.id('TST-123');
* assertive.tags('checkout', 'smoke');
* assertive.owner('aayush');
* assertive.priority('high');
* // … test code …
* });
*/
class AssertiveHelper {
/** Link this test to a unique assertive ID (e.g. "TST-123"). */
id(testId: string): void {
getContext().set('assertive_id', testId);
}

/** Attach one or more tags to the current test. */
tags(...tags: string[]): void {
for (const tag of tags) {
getContext().set('assertive_tag', tag);
}
}

/** Set the owner / assignee for the current test. */
owner(name: string): void {
getContext().set('assertive_owner', name);
}

/** Set the priority level — full autocomplete for the four levels. */
priority(level: Priority): void {
getContext().set('assertive_priority', level);
}

/** Set the test type. */
type(testType: TestType): void {
getContext().set('assertive_type', testType);
}

/** Attach an arbitrary custom field. */
field(key: string, value: string): void {
getContext().set(`assertive_field_${key}`, value);
}

/** Attach contextual data (e.g. a cart total, a screenshot path). */
attach(key: string, data: string): void {
getContext().set(`assertive_attach_${key}`, data);
}
}

/** Singleton — import this in test files. */
export const assertive = new AssertiveHelper();

export type { Priority, TestType };
Loading