first go
This commit is contained in:
parent
512cb36c9b
commit
9b333832e0
|
|
@ -50,6 +50,16 @@ Start time of this particular test step.
|
||||||
|
|
||||||
List of steps inside this step.
|
List of steps inside this step.
|
||||||
|
|
||||||
|
## property: TestStep.attachments
|
||||||
|
* since: v1.10
|
||||||
|
- type: <[Array]<[Object]>>
|
||||||
|
- `name` <[string]> Attachment name.
|
||||||
|
- `contentType` <[string]> Content type of this attachment to properly present in the report, for example `'application/json'` or `'image/png'`.
|
||||||
|
- `path` ?<[string]> Optional path on the filesystem to the attached file.
|
||||||
|
- `body` ?<[Buffer]> Optional attachment body used instead of a file.
|
||||||
|
|
||||||
|
The list of files or buffers attached in the step execution through [`property: TestInfo.attachments`].
|
||||||
|
|
||||||
## property: TestStep.title
|
## property: TestStep.title
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: <[string]>
|
- type: <[string]>
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,10 @@ const result: TestResult = {
|
||||||
duration: 10,
|
duration: 10,
|
||||||
location: { file: 'test.spec.ts', line: 82, column: 0 },
|
location: { file: 'test.spec.ts', line: 82, column: 0 },
|
||||||
steps: [],
|
steps: [],
|
||||||
|
attachments: [],
|
||||||
count: 1,
|
count: 1,
|
||||||
}],
|
}],
|
||||||
|
attachments: [],
|
||||||
}],
|
}],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
status: 'passed',
|
status: 'passed',
|
||||||
|
|
@ -139,6 +141,7 @@ const resultWithAttachment: TestResult = {
|
||||||
location: { file: 'test.spec.ts', line: 62, column: 0 },
|
location: { file: 'test.spec.ts', line: 62, column: 0 },
|
||||||
count: 1,
|
count: 1,
|
||||||
steps: [],
|
steps: [],
|
||||||
|
attachments: [1],
|
||||||
}],
|
}],
|
||||||
attachments: [{
|
attachments: [{
|
||||||
name: 'first attachment',
|
name: 'first attachment',
|
||||||
|
|
|
||||||
|
|
@ -108,5 +108,6 @@ export type TestStep = {
|
||||||
snippet?: string;
|
snippet?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
steps: TestStep[];
|
steps: TestStep[];
|
||||||
|
attachments: number[];
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TestInfoImpl } from '../worker/testInfo';
|
import type { TestInfoImpl, TestStepInternal } from '../worker/testInfo';
|
||||||
import type { Suite } from './test';
|
import type { Suite } from './test';
|
||||||
|
|
||||||
let currentTestInfoValue: TestInfoImpl | null = null;
|
let currentTestInfoValue: TestInfoImpl | null = null;
|
||||||
|
|
@ -42,3 +42,13 @@ export function setIsWorkerProcess() {
|
||||||
export function isWorkerProcess() {
|
export function isWorkerProcess() {
|
||||||
return _isWorkerProcess;
|
return _isWorkerProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentStepValue: TestStepInternal | undefined;
|
||||||
|
|
||||||
|
export function setCurrentStep(step: TestStepInternal | undefined) {
|
||||||
|
currentStepValue = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function currentStep(): TestStepInternal | undefined {
|
||||||
|
return currentStepValue;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ export type AttachmentPayload = {
|
||||||
path?: string;
|
path?: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
|
stepId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestInfoErrorImpl = TestInfoError & {
|
export type TestInfoErrorImpl = TestInfoError & {
|
||||||
|
|
|
||||||
|
|
@ -512,6 +512,7 @@ class TeleTestStep implements reporterTypes.TestStep {
|
||||||
parent: reporterTypes.TestStep | undefined;
|
parent: reporterTypes.TestStep | undefined;
|
||||||
duration: number = -1;
|
duration: number = -1;
|
||||||
steps: reporterTypes.TestStep[] = [];
|
steps: reporterTypes.TestStep[] = [];
|
||||||
|
attachments = [];
|
||||||
|
|
||||||
private _startTime: number = 0;
|
private _startTime: number = 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ import {
|
||||||
} from './matchers';
|
} from './matchers';
|
||||||
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
import { toMatchSnapshot, toHaveScreenshot, toHaveScreenshotStepTitle } from './toMatchSnapshot';
|
||||||
import type { Expect, ExpectMatcherState } from '../../types/test';
|
import type { Expect, ExpectMatcherState } from '../../types/test';
|
||||||
import { currentTestInfo } from '../common/globals';
|
import { currentTestInfo, setCurrentStep } from '../common/globals';
|
||||||
import { filteredStackTrace, trimLongString } from '../util';
|
import { filteredStackTrace, trimLongString } from '../util';
|
||||||
import {
|
import {
|
||||||
expect as expectLibrary,
|
expect as expectLibrary,
|
||||||
|
|
@ -322,8 +322,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
};
|
};
|
||||||
|
|
||||||
const step = testInfo._addStep(stepInfo);
|
const step = testInfo._addStep(stepInfo);
|
||||||
|
setCurrentStep(step);
|
||||||
|
|
||||||
const reportStepError = (e: Error | unknown) => {
|
const reportStepError = (e: Error | unknown) => {
|
||||||
|
setCurrentStep(undefined);
|
||||||
const jestError = isJestError(e) ? e : null;
|
const jestError = isJestError(e) ? e : null;
|
||||||
const error = jestError ? new ExpectError(jestError, customMessage, stackFrames) : e;
|
const error = jestError ? new ExpectError(jestError, customMessage, stackFrames) : e;
|
||||||
if (jestError?.matcherResult.suggestedRebaseline) {
|
if (jestError?.matcherResult.suggestedRebaseline) {
|
||||||
|
|
@ -338,6 +340,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalizer = () => {
|
const finalizer = () => {
|
||||||
|
setCurrentStep(undefined);
|
||||||
step.complete({});
|
step.complete({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import type { Locator, Page } from 'playwright-core';
|
import type { Locator, Page } from 'playwright-core';
|
||||||
import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
|
import type { ExpectScreenshotOptions, Page as PageEx } from 'playwright-core/lib/client/page';
|
||||||
import { currentTestInfo } from '../common/globals';
|
import { currentStep, currentTestInfo } from '../common/globals';
|
||||||
import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils';
|
import type { ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils';
|
||||||
import { compareBuffersOrStrings, getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
import { compareBuffersOrStrings, getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||||
import {
|
import {
|
||||||
|
|
@ -29,7 +29,7 @@ import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||||
import type { TestInfoImpl } from '../worker/testInfo';
|
import type { TestInfoImpl, TestStepInternal } from '../worker/testInfo';
|
||||||
import type { ExpectMatcherState } from '../../types/test';
|
import type { ExpectMatcherState } from '../../types/test';
|
||||||
import { matcherHint, type MatcherResult } from './matcherHint';
|
import { matcherHint, type MatcherResult } from './matcherHint';
|
||||||
import type { FullProjectInternal } from '../common/config';
|
import type { FullProjectInternal } from '../common/config';
|
||||||
|
|
@ -75,6 +75,7 @@ const NonConfigProperties: (keyof ToHaveScreenshotOptions)[] = [
|
||||||
|
|
||||||
class SnapshotHelper {
|
class SnapshotHelper {
|
||||||
readonly testInfo: TestInfoImpl;
|
readonly testInfo: TestInfoImpl;
|
||||||
|
readonly step: TestStepInternal;
|
||||||
readonly attachmentBaseName: string;
|
readonly attachmentBaseName: string;
|
||||||
readonly legacyExpectedPath: string;
|
readonly legacyExpectedPath: string;
|
||||||
readonly previousPath: string;
|
readonly previousPath: string;
|
||||||
|
|
@ -91,6 +92,7 @@ class SnapshotHelper {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
testInfo: TestInfoImpl,
|
testInfo: TestInfoImpl,
|
||||||
|
step: TestStepInternal,
|
||||||
matcherName: string,
|
matcherName: string,
|
||||||
locator: Locator | undefined,
|
locator: Locator | undefined,
|
||||||
anonymousSnapshotExtension: string,
|
anonymousSnapshotExtension: string,
|
||||||
|
|
@ -182,6 +184,7 @@ class SnapshotHelper {
|
||||||
this.comparator = getComparator(this.mimeType);
|
this.comparator = getComparator(this.mimeType);
|
||||||
|
|
||||||
this.testInfo = testInfo;
|
this.testInfo = testInfo;
|
||||||
|
this.step = step;
|
||||||
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,9 +227,9 @@ class SnapshotHelper {
|
||||||
const isWriteMissingMode = this.updateSnapshots !== 'none';
|
const isWriteMissingMode = this.updateSnapshots !== 'none';
|
||||||
if (isWriteMissingMode)
|
if (isWriteMissingMode)
|
||||||
writeFileSync(this.expectedPath, actual);
|
writeFileSync(this.expectedPath, actual);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||||
writeFileSync(this.actualPath, actual);
|
writeFileSync(this.actualPath, actual);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||||
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', writing actual.' : '.'}`;
|
||||||
if (this.updateSnapshots === 'all' || this.updateSnapshots === 'changed') {
|
if (this.updateSnapshots === 'all' || this.updateSnapshots === 'changed') {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
@ -254,22 +257,22 @@ class SnapshotHelper {
|
||||||
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
|
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
|
||||||
// so that one can upload `test-results/` directory and have all the data inside.
|
// so that one can upload `test-results/` directory and have all the data inside.
|
||||||
writeFileSync(this.legacyExpectedPath, expected);
|
writeFileSync(this.legacyExpectedPath, expected);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-expected'), contentType: this.mimeType, path: this.expectedPath });
|
||||||
output.push(`\nExpected: ${colors.yellow(this.expectedPath)}`);
|
output.push(`\nExpected: ${colors.yellow(this.expectedPath)}`);
|
||||||
}
|
}
|
||||||
if (previous !== undefined) {
|
if (previous !== undefined) {
|
||||||
writeFileSync(this.previousPath, previous);
|
writeFileSync(this.previousPath, previous);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-previous'), contentType: this.mimeType, path: this.previousPath });
|
||||||
output.push(`Previous: ${colors.yellow(this.previousPath)}`);
|
output.push(`Previous: ${colors.yellow(this.previousPath)}`);
|
||||||
}
|
}
|
||||||
if (actual !== undefined) {
|
if (actual !== undefined) {
|
||||||
writeFileSync(this.actualPath, actual);
|
writeFileSync(this.actualPath, actual);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-actual'), contentType: this.mimeType, path: this.actualPath });
|
||||||
output.push(`Received: ${colors.yellow(this.actualPath)}`);
|
output.push(`Received: ${colors.yellow(this.actualPath)}`);
|
||||||
}
|
}
|
||||||
if (diff !== undefined) {
|
if (diff !== undefined) {
|
||||||
writeFileSync(this.diffPath, diff);
|
writeFileSync(this.diffPath, diff);
|
||||||
this.testInfo.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
this.step.attachments.push({ name: addSuffixToFilePath(this.attachmentBaseName, '-diff'), contentType: this.mimeType, path: this.diffPath });
|
||||||
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
|
output.push(` Diff: ${colors.yellow(this.diffPath)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,7 +296,8 @@ export function toMatchSnapshot(
|
||||||
optOptions: ImageComparatorOptions = {}
|
optOptions: ImageComparatorOptions = {}
|
||||||
): MatcherResult<NameOrSegments | { name?: NameOrSegments }, string> {
|
): MatcherResult<NameOrSegments | { name?: NameOrSegments }, string> {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo)
|
const step = currentStep();
|
||||||
|
if (!testInfo || !step)
|
||||||
throw new Error(`toMatchSnapshot() must be called during the test`);
|
throw new Error(`toMatchSnapshot() must be called during the test`);
|
||||||
if (received instanceof Promise)
|
if (received instanceof Promise)
|
||||||
throw new Error('An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.');
|
throw new Error('An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.');
|
||||||
|
|
@ -303,7 +307,7 @@ export function toMatchSnapshot(
|
||||||
|
|
||||||
const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
|
const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
|
||||||
const helper = new SnapshotHelper(
|
const helper = new SnapshotHelper(
|
||||||
testInfo, 'toMatchSnapshot', undefined, determineFileExtension(received),
|
testInfo, step, 'toMatchSnapshot', undefined, determineFileExtension(received),
|
||||||
configOptions, nameOrOptions, optOptions);
|
configOptions, nameOrOptions, optOptions);
|
||||||
|
|
||||||
if (this.isNot) {
|
if (this.isNot) {
|
||||||
|
|
@ -365,7 +369,8 @@ export async function toHaveScreenshot(
|
||||||
optOptions: ToHaveScreenshotOptions = {}
|
optOptions: ToHaveScreenshotOptions = {}
|
||||||
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
|
): Promise<MatcherResult<NameOrSegments | { name?: NameOrSegments }, string>> {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo)
|
const step = currentStep();
|
||||||
|
if (!testInfo || !step)
|
||||||
throw new Error(`toHaveScreenshot() must be called during the test`);
|
throw new Error(`toHaveScreenshot() must be called during the test`);
|
||||||
|
|
||||||
if (testInfo._projectInternal.ignoreSnapshots)
|
if (testInfo._projectInternal.ignoreSnapshots)
|
||||||
|
|
@ -374,7 +379,7 @@ export async function toHaveScreenshot(
|
||||||
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
||||||
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator];
|
const [page, locator] = pageOrLocator.constructor.name === 'Page' ? [(pageOrLocator as PageEx), undefined] : [(pageOrLocator as Locator).page() as PageEx, pageOrLocator as Locator];
|
||||||
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
|
const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
|
||||||
const helper = new SnapshotHelper(testInfo, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions);
|
const helper = new SnapshotHelper(testInfo, step, 'toHaveScreenshot', locator, 'png', configOptions, nameOrOptions, optOptions);
|
||||||
if (!helper.expectedPath.toLowerCase().endsWith('.png'))
|
if (!helper.expectedPath.toLowerCase().endsWith('.png'))
|
||||||
throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`);
|
throw new Error(`Screenshot name "${path.basename(helper.expectedPath)}" must have '.png' extension`);
|
||||||
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
expectTypes(pageOrLocator, ['Page', 'Locator'], 'toHaveScreenshot');
|
||||||
|
|
|
||||||
|
|
@ -505,7 +505,7 @@ class HtmlBuilder {
|
||||||
duration: result.duration,
|
duration: result.duration,
|
||||||
startTime: result.startTime.toISOString(),
|
startTime: result.startTime.toISOString(),
|
||||||
retry: result.retry,
|
retry: result.retry,
|
||||||
steps: dedupeSteps(result.steps).map(s => this._createTestStep(s)),
|
steps: dedupeSteps(result.steps).map(s => this._createTestStep(s, result)),
|
||||||
errors: formatResultFailure(test, result, '', true).map(error => error.message),
|
errors: formatResultFailure(test, result, '', true).map(error => error.message),
|
||||||
status: result.status,
|
status: result.status,
|
||||||
attachments: this._serializeAttachments([
|
attachments: this._serializeAttachments([
|
||||||
|
|
@ -515,20 +515,26 @@ class HtmlBuilder {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createTestStep(dedupedStep: DedupedStep): TestStep {
|
private _createTestStep(dedupedStep: DedupedStep, result: TestResultPublic): TestStep {
|
||||||
const { step, duration, count } = dedupedStep;
|
const { step, duration, count } = dedupedStep;
|
||||||
const result: TestStep = {
|
const testStep: TestStep = {
|
||||||
title: step.title,
|
title: step.title,
|
||||||
startTime: step.startTime.toISOString(),
|
startTime: step.startTime.toISOString(),
|
||||||
duration,
|
duration,
|
||||||
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s)),
|
steps: dedupeSteps(step.steps).map(s => this._createTestStep(s, result)),
|
||||||
|
attachments: step.attachments.map(s => {
|
||||||
|
const index = result.attachments.indexOf(s);
|
||||||
|
if (index === -1)
|
||||||
|
throw new Error('Unexpected, attachment not found');
|
||||||
|
return index;
|
||||||
|
}),
|
||||||
location: this._relativeLocation(step.location),
|
location: this._relativeLocation(step.location),
|
||||||
error: step.error?.message,
|
error: step.error?.message,
|
||||||
count
|
count
|
||||||
};
|
};
|
||||||
if (step.location)
|
if (step.location)
|
||||||
this._stepsInFile.set(step.location.file, result);
|
this._stepsInFile.set(step.location.file, testStep);
|
||||||
return result;
|
return testStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _relativeLocation(location: Location | undefined): Location | undefined {
|
private _relativeLocation(location: Location | undefined): Location | undefined {
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,7 @@ class JobDispatcher {
|
||||||
startTime: new Date(params.wallTime),
|
startTime: new Date(params.wallTime),
|
||||||
duration: -1,
|
duration: -1,
|
||||||
steps: [],
|
steps: [],
|
||||||
|
attachments: [],
|
||||||
location: params.location,
|
location: params.location,
|
||||||
};
|
};
|
||||||
steps.set(params.stepId, step);
|
steps.set(params.stepId, step);
|
||||||
|
|
@ -361,6 +362,8 @@ class JobDispatcher {
|
||||||
body: params.body !== undefined ? Buffer.from(params.body, 'base64') : undefined
|
body: params.body !== undefined ? Buffer.from(params.body, 'base64') : undefined
|
||||||
};
|
};
|
||||||
data.result.attachments.push(attachment);
|
data.result.attachments.push(attachment);
|
||||||
|
if (params.stepId)
|
||||||
|
data.steps.get(params.stepId)!.attachments.push(attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _failTestWithErrors(test: TestCase, errors: TestError[]) {
|
private _failTestWithErrors(test: TestCase, errors: TestError[]) {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ import type { StackFrame } from '@protocol/channels';
|
||||||
import { testInfoError } from './util';
|
import { testInfoError } from './util';
|
||||||
|
|
||||||
export interface TestStepInternal {
|
export interface TestStepInternal {
|
||||||
complete(result: { error?: Error | unknown, attachments?: Attachment[], suggestedRebaseline?: string }): void;
|
complete(result: { error?: Error | unknown, suggestedRebaseline?: string }): void;
|
||||||
|
attachments: Attachment[];
|
||||||
stepId: string;
|
stepId: string;
|
||||||
title: string;
|
title: string;
|
||||||
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
|
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
|
||||||
|
|
@ -193,7 +194,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
this._attachmentsPush = this.attachments.push.bind(this.attachments);
|
this._attachmentsPush = this.attachments.push.bind(this.attachments);
|
||||||
this.attachments.push = (...attachments: TestInfo['attachments']) => {
|
this.attachments.push = (...attachments: TestInfo['attachments']) => {
|
||||||
for (const a of attachments)
|
for (const a of attachments)
|
||||||
this._attach(a.name, a);
|
this._attach(a, this._parentStep()?.stepId);
|
||||||
return this.attachments.length;
|
return this.attachments.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -238,7 +239,12 @@ export class TestInfoImpl implements TestInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps'>, parentStep?: TestStepInternal): TestStepInternal {
|
_parentStep() {
|
||||||
|
return zones.zoneData<TestStepInternal>('stepZone')
|
||||||
|
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
|
||||||
|
}
|
||||||
|
|
||||||
|
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachments'>, parentStep?: TestStepInternal): TestStepInternal {
|
||||||
const stepId = `${data.category}@${++this._lastStepId}`;
|
const stepId = `${data.category}@${++this._lastStepId}`;
|
||||||
|
|
||||||
if (data.isStage) {
|
if (data.isStage) {
|
||||||
|
|
@ -246,11 +252,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
parentStep = this._findLastStageStep(this._steps);
|
parentStep = this._findLastStageStep(this._steps);
|
||||||
} else {
|
} else {
|
||||||
if (!parentStep)
|
if (!parentStep)
|
||||||
parentStep = zones.zoneData<TestStepInternal>('stepZone');
|
parentStep = this._parentStep();
|
||||||
if (!parentStep) {
|
|
||||||
// If no parent step on stack, assume the current stage as parent.
|
|
||||||
parentStep = this._findLastStageStep(this._steps);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredStack = filteredStackTrace(captureRawStack());
|
const filteredStack = filteredStackTrace(captureRawStack());
|
||||||
|
|
@ -261,10 +263,12 @@ export class TestInfoImpl implements TestInfo {
|
||||||
}
|
}
|
||||||
data.location = data.location || filteredStack[0];
|
data.location = data.location || filteredStack[0];
|
||||||
|
|
||||||
|
const attachments: Attachment[] = [];
|
||||||
const step: TestStepInternal = {
|
const step: TestStepInternal = {
|
||||||
stepId,
|
stepId,
|
||||||
...data,
|
...data,
|
||||||
steps: [],
|
steps: [],
|
||||||
|
attachments,
|
||||||
complete: result => {
|
complete: result => {
|
||||||
if (step.endWallTime)
|
if (step.endWallTime)
|
||||||
return;
|
return;
|
||||||
|
|
@ -292,6 +296,9 @@ export class TestInfoImpl implements TestInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const attachment of attachments ?? [])
|
||||||
|
this._attach(attachment, stepId);
|
||||||
|
|
||||||
const payload: StepEndPayload = {
|
const payload: StepEndPayload = {
|
||||||
testId: this.testId,
|
testId: this.testId,
|
||||||
stepId,
|
stepId,
|
||||||
|
|
@ -301,7 +308,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
};
|
};
|
||||||
this._onStepEnd(payload);
|
this._onStepEnd(payload);
|
||||||
const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined;
|
const errorForTrace = step.error ? { name: '', message: step.error.message || '', stack: step.error.stack } : undefined;
|
||||||
this._tracing.appendAfterActionForStep(stepId, errorForTrace, result.attachments);
|
this._tracing.appendAfterActionForStep(stepId, errorForTrace, attachments);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const parentStepList = parentStep ? parentStep.steps : this._steps;
|
const parentStepList = parentStep ? parentStep.steps : this._steps;
|
||||||
|
|
@ -400,23 +407,24 @@ export class TestInfoImpl implements TestInfo {
|
||||||
// ------------ TestInfo methods ------------
|
// ------------ TestInfo methods ------------
|
||||||
|
|
||||||
async attach(name: string, options: { path?: string, body?: string | Buffer, contentType?: string } = {}) {
|
async attach(name: string, options: { path?: string, body?: string | Buffer, contentType?: string } = {}) {
|
||||||
this._attach(name, await normalizeAndSaveAttachment(this.outputPath(), name, options));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _attach(name: string, attachment: TestInfo['attachments'][0]) {
|
|
||||||
const step = this._addStep({
|
const step = this._addStep({
|
||||||
title: `attach "${name}"`,
|
title: `attach "${name}"`,
|
||||||
category: 'attach',
|
category: 'attach',
|
||||||
});
|
});
|
||||||
|
step.attachments.push(await normalizeAndSaveAttachment(this.outputPath(), name, options));
|
||||||
|
step.complete({});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _attach(attachment: TestInfo['attachments'][0], stepId: string | undefined) {
|
||||||
this._attachmentsPush(attachment);
|
this._attachmentsPush(attachment);
|
||||||
this._onAttach({
|
this._onAttach({
|
||||||
testId: this.testId,
|
testId: this.testId,
|
||||||
name: attachment.name,
|
name: attachment.name,
|
||||||
contentType: attachment.contentType,
|
contentType: attachment.contentType,
|
||||||
path: attachment.path,
|
path: attachment.path,
|
||||||
body: attachment.body?.toString('base64')
|
body: attachment.body?.toString('base64'),
|
||||||
|
stepId,
|
||||||
});
|
});
|
||||||
step.complete({ attachments: [attachment] });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputPath(...pathSegments: string[]){
|
outputPath(...pathSegments: string[]){
|
||||||
|
|
|
||||||
27
packages/playwright/types/testReporter.d.ts
vendored
27
packages/playwright/types/testReporter.d.ts
vendored
|
|
@ -691,6 +691,33 @@ export interface TestStep {
|
||||||
*/
|
*/
|
||||||
titlePath(): Array<string>;
|
titlePath(): Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of files or buffers attached in the step execution through
|
||||||
|
* [testInfo.attachments](https://playwright.dev/docs/api/class-testinfo#test-info-attachments).
|
||||||
|
*/
|
||||||
|
attachments: Array<{
|
||||||
|
/**
|
||||||
|
* Attachment name.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content type of this attachment to properly present in the report, for example `'application/json'` or
|
||||||
|
* `'image/png'`.
|
||||||
|
*/
|
||||||
|
contentType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional path on the filesystem to the attached file.
|
||||||
|
*/
|
||||||
|
path?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional attachment body used instead of a file.
|
||||||
|
*/
|
||||||
|
body?: Buffer;
|
||||||
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step category to differentiate steps with different origin and verbosity. Built-in categories are:
|
* Step category to differentiate steps with different origin and verbosity. Built-in categories are:
|
||||||
* - `hook` for fixtures and hooks initialization and teardown
|
* - `hook` for fixtures and hooks initialization and teardown
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Reporter, TestCase, TestResult, TestStep } from '../../packages/playwright-test/reporter';
|
||||||
import { test, expect } from './playwright-test-fixtures';
|
import { test, expect } from './playwright-test-fixtures';
|
||||||
|
|
||||||
const smallReporterJS = `
|
const smallReporterJS = `
|
||||||
|
|
@ -703,3 +704,33 @@ onEnd
|
||||||
onExit
|
onExit
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step attachments are referentially equal to result attachments', async ({ runInlineTest }) => {
|
||||||
|
class TestReporter implements Reporter {
|
||||||
|
onStepEnd(test: TestCase, result: TestResult, step: TestStep) {
|
||||||
|
console.log('%%%', JSON.stringify({
|
||||||
|
title: step.title,
|
||||||
|
attachments: step.attachments.map(a => result.attachments.indexOf(a)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'reporter.ts': `module.exports = ${TestReporter.toString()}`,
|
||||||
|
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({}, testInfo) => {
|
||||||
|
await test.step('step', async () => {
|
||||||
|
testInfo.attachments.push({ name: 'attachment', body: Buffer.from('content') });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { 'reporter': '', 'workers': 1 });
|
||||||
|
|
||||||
|
const steps = result.outputLines.map(line => JSON.parse(line));
|
||||||
|
expect(steps).toEqual([
|
||||||
|
{ title: 'Before Hooks', attachments: [] },
|
||||||
|
{ title: 'step', attachments: [0] },
|
||||||
|
{ title: 'After Hooks', attachments: [] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue