mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 11:46:42 +03:00
chore(test runner): allow TestInfoImpl without a TestCase (#29534)
This will be useful to run `beforeAll`/`afterAll` hooks with a separate `TestInfo` instance, as well as run use helpers like `_runAndFailOnError()` during scope teardown.
This commit is contained in:
parent
dbf0b25146
commit
269a293ba1
@ -17,7 +17,7 @@
|
|||||||
import { formatLocation, debugTest, filterStackFile } from '../util';
|
import { formatLocation, debugTest, filterStackFile } from '../util';
|
||||||
import { ManualPromise } from 'playwright-core/lib/utils';
|
import { ManualPromise } from 'playwright-core/lib/utils';
|
||||||
import type { TestInfoImpl, TestStepInternal } from './testInfo';
|
import type { TestInfoImpl, TestStepInternal } from './testInfo';
|
||||||
import type { FixtureDescription, TimeoutManager } from './timeoutManager';
|
import type { FixtureDescription } from './timeoutManager';
|
||||||
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
|
import { fixtureParameterNames, type FixturePool, type FixtureRegistration, type FixtureScope } from '../common/fixtures';
|
||||||
import type { WorkerInfo } from '../../types/test';
|
import type { WorkerInfo } from '../../types/test';
|
||||||
import type { Location } from '../../types/testReporter';
|
import type { Location } from '../../types/testReporter';
|
||||||
@ -136,21 +136,21 @@ class Fixture {
|
|||||||
testInfo._timeoutManager.setCurrentFixture(undefined);
|
testInfo._timeoutManager.setCurrentFixture(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
async teardown(timeoutManager: TimeoutManager) {
|
async teardown(testInfo: TestInfoImpl) {
|
||||||
if (this._teardownWithDepsComplete) {
|
if (this._teardownWithDepsComplete) {
|
||||||
// When we are waiting for the teardown for the second time,
|
// When we are waiting for the teardown for the second time,
|
||||||
// most likely after the first time did timeout, annotate current fixture
|
// most likely after the first time did timeout, annotate current fixture
|
||||||
// for better error messages.
|
// for better error messages.
|
||||||
this._setTeardownDescription(timeoutManager);
|
this._setTeardownDescription(testInfo);
|
||||||
await this._teardownWithDepsComplete;
|
await this._teardownWithDepsComplete;
|
||||||
timeoutManager.setCurrentFixture(undefined);
|
testInfo._timeoutManager.setCurrentFixture(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._teardownWithDepsComplete = this._teardownInternal(timeoutManager);
|
this._teardownWithDepsComplete = this._teardownInternal(testInfo);
|
||||||
await this._teardownWithDepsComplete;
|
await this._teardownWithDepsComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _teardownInternal(timeoutManager: TimeoutManager) {
|
private async _teardownInternal(testInfo: TestInfoImpl) {
|
||||||
if (typeof this.registration.fn !== 'function')
|
if (typeof this.registration.fn !== 'function')
|
||||||
return;
|
return;
|
||||||
try {
|
try {
|
||||||
@ -161,10 +161,10 @@ class Fixture {
|
|||||||
}
|
}
|
||||||
if (this._useFuncFinished) {
|
if (this._useFuncFinished) {
|
||||||
debugTest(`teardown ${this.registration.name}`);
|
debugTest(`teardown ${this.registration.name}`);
|
||||||
this._setTeardownDescription(timeoutManager);
|
this._setTeardownDescription(testInfo);
|
||||||
this._useFuncFinished.resolve();
|
this._useFuncFinished.resolve();
|
||||||
await this._selfTeardownComplete;
|
await this._selfTeardownComplete;
|
||||||
timeoutManager.setCurrentFixture(undefined);
|
testInfo._timeoutManager.setCurrentFixture(undefined);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
for (const dep of this._deps)
|
for (const dep of this._deps)
|
||||||
@ -173,9 +173,9 @@ class Fixture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setTeardownDescription(timeoutManager: TimeoutManager) {
|
private _setTeardownDescription(testInfo: TestInfoImpl) {
|
||||||
this._runnableDescription.phase = 'teardown';
|
this._runnableDescription.phase = 'teardown';
|
||||||
timeoutManager.setCurrentFixture(this._runnableDescription);
|
testInfo._timeoutManager.setCurrentFixture(this._runnableDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectFixturesInTeardownOrder(scope: FixtureScope, collector: Set<Fixture>) {
|
_collectFixturesInTeardownOrder(scope: FixtureScope, collector: Set<Fixture>) {
|
||||||
@ -206,14 +206,14 @@ export class FixtureRunner {
|
|||||||
this.pool = pool;
|
this.pool = pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
async teardownScope(scope: FixtureScope, timeoutManager: TimeoutManager, onFixtureError: (error: Error) => void) {
|
async teardownScope(scope: FixtureScope, testInfo: TestInfoImpl, onFixtureError: (error: Error) => void) {
|
||||||
// Teardown fixtures in the reverse order.
|
// Teardown fixtures in the reverse order.
|
||||||
const fixtures = Array.from(this.instanceForId.values()).reverse();
|
const fixtures = Array.from(this.instanceForId.values()).reverse();
|
||||||
const collector = new Set<Fixture>();
|
const collector = new Set<Fixture>();
|
||||||
for (const fixture of fixtures)
|
for (const fixture of fixtures)
|
||||||
fixture._collectFixturesInTeardownOrder(scope, collector);
|
fixture._collectFixturesInTeardownOrder(scope, collector);
|
||||||
for (const fixture of collector)
|
for (const fixture of collector)
|
||||||
await fixture.teardown(timeoutManager).catch(onFixtureError);
|
await fixture.teardown(testInfo).catch(onFixtureError);
|
||||||
if (scope === 'test')
|
if (scope === 'test')
|
||||||
this.testScopeClean = true;
|
this.testScopeClean = true;
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,6 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
private _onStepBegin: (payload: StepBeginPayload) => void;
|
private _onStepBegin: (payload: StepBeginPayload) => void;
|
||||||
private _onStepEnd: (payload: StepEndPayload) => void;
|
private _onStepEnd: (payload: StepEndPayload) => void;
|
||||||
private _onAttach: (payload: AttachmentPayload) => void;
|
private _onAttach: (payload: AttachmentPayload) => void;
|
||||||
readonly _test: TestCase;
|
|
||||||
readonly _timeoutManager: TimeoutManager;
|
readonly _timeoutManager: TimeoutManager;
|
||||||
readonly _startTime: number;
|
readonly _startTime: number;
|
||||||
readonly _startWallTime: number;
|
readonly _startWallTime: number;
|
||||||
@ -62,6 +61,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
_didTimeout = false;
|
_didTimeout = false;
|
||||||
_wasInterrupted = false;
|
_wasInterrupted = false;
|
||||||
_lastStepId = 0;
|
_lastStepId = 0;
|
||||||
|
private readonly _requireFile: string;
|
||||||
readonly _projectInternal: FullProjectInternal;
|
readonly _projectInternal: FullProjectInternal;
|
||||||
readonly _configInternal: FullConfigInternal;
|
readonly _configInternal: FullConfigInternal;
|
||||||
readonly _steps: TestStepInternal[] = [];
|
readonly _steps: TestStepInternal[] = [];
|
||||||
@ -129,19 +129,19 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
configInternal: FullConfigInternal,
|
configInternal: FullConfigInternal,
|
||||||
projectInternal: FullProjectInternal,
|
projectInternal: FullProjectInternal,
|
||||||
workerParams: WorkerInitParams,
|
workerParams: WorkerInitParams,
|
||||||
test: TestCase,
|
test: TestCase | undefined,
|
||||||
retry: number,
|
retry: number,
|
||||||
onStepBegin: (payload: StepBeginPayload) => void,
|
onStepBegin: (payload: StepBeginPayload) => void,
|
||||||
onStepEnd: (payload: StepEndPayload) => void,
|
onStepEnd: (payload: StepEndPayload) => void,
|
||||||
onAttach: (payload: AttachmentPayload) => void,
|
onAttach: (payload: AttachmentPayload) => void,
|
||||||
) {
|
) {
|
||||||
this._test = test;
|
this.testId = test?.id ?? '';
|
||||||
this.testId = test.id;
|
|
||||||
this._onStepBegin = onStepBegin;
|
this._onStepBegin = onStepBegin;
|
||||||
this._onStepEnd = onStepEnd;
|
this._onStepEnd = onStepEnd;
|
||||||
this._onAttach = onAttach;
|
this._onAttach = onAttach;
|
||||||
this._startTime = monotonicTime();
|
this._startTime = monotonicTime();
|
||||||
this._startWallTime = Date.now();
|
this._startWallTime = Date.now();
|
||||||
|
this._requireFile = test?._requireFile ?? '';
|
||||||
|
|
||||||
this.repeatEachIndex = workerParams.repeatEachIndex;
|
this.repeatEachIndex = workerParams.repeatEachIndex;
|
||||||
this.retry = retry;
|
this.retry = retry;
|
||||||
@ -151,20 +151,20 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
this.project = projectInternal.project;
|
this.project = projectInternal.project;
|
||||||
this._configInternal = configInternal;
|
this._configInternal = configInternal;
|
||||||
this.config = configInternal.config;
|
this.config = configInternal.config;
|
||||||
this.title = test.title;
|
this.title = test?.title ?? '';
|
||||||
this.titlePath = test.titlePath();
|
this.titlePath = test?.titlePath() ?? [];
|
||||||
this.file = test.location.file;
|
this.file = test?.location.file ?? '';
|
||||||
this.line = test.location.line;
|
this.line = test?.location.line ?? 0;
|
||||||
this.column = test.location.column;
|
this.column = test?.location.column ?? 0;
|
||||||
this.fn = test.fn;
|
this.fn = test?.fn ?? (() => {});
|
||||||
this.expectedStatus = test.expectedStatus;
|
this.expectedStatus = test?.expectedStatus ?? 'skipped';
|
||||||
|
|
||||||
this._timeoutManager = new TimeoutManager(this.project.timeout);
|
this._timeoutManager = new TimeoutManager(this.project.timeout);
|
||||||
|
|
||||||
this.outputDir = (() => {
|
this.outputDir = (() => {
|
||||||
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, ''));
|
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, ''));
|
||||||
const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-');
|
const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-');
|
||||||
const fullTitleWithoutSpec = test.titlePath().slice(1).join(' ');
|
const fullTitleWithoutSpec = this.titlePath.slice(1).join(' ');
|
||||||
|
|
||||||
let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec));
|
let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec));
|
||||||
if (projectInternal.id)
|
if (projectInternal.id)
|
||||||
@ -177,7 +177,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
this.snapshotDir = (() => {
|
this.snapshotDir = (() => {
|
||||||
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile);
|
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
||||||
return path.join(this.project.snapshotDir, relativeTestFilePath + '-snapshots');
|
return path.join(this.project.snapshotDir, relativeTestFilePath + '-snapshots');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -328,7 +328,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload: StepEndPayload = {
|
const payload: StepEndPayload = {
|
||||||
testId: this._test.id,
|
testId: this.testId,
|
||||||
stepId,
|
stepId,
|
||||||
wallTime: step.endWallTime,
|
wallTime: step.endWallTime,
|
||||||
error: step.error,
|
error: step.error,
|
||||||
@ -344,7 +344,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
const parentStepList = parentStep ? parentStep.steps : this._steps;
|
const parentStepList = parentStep ? parentStep.steps : this._steps;
|
||||||
parentStepList.push(step);
|
parentStepList.push(step);
|
||||||
const payload: StepBeginPayload = {
|
const payload: StepBeginPayload = {
|
||||||
testId: this._test.id,
|
testId: this.testId,
|
||||||
stepId,
|
stepId,
|
||||||
parentStepId: parentStep ? parentStep.stepId : undefined,
|
parentStepId: parentStep ? parentStep.stepId : undefined,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
@ -434,7 +434,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
});
|
});
|
||||||
this._attachmentsPush(attachment);
|
this._attachmentsPush(attachment);
|
||||||
this._onAttach({
|
this._onAttach({
|
||||||
testId: this._test.id,
|
testId: this.testId,
|
||||||
name: attachment.name,
|
name: attachment.name,
|
||||||
contentType: attachment.contentType,
|
contentType: attachment.contentType,
|
||||||
path: attachment.path,
|
path: attachment.path,
|
||||||
@ -465,7 +465,7 @@ export class TestInfoImpl implements TestInfo {
|
|||||||
snapshotPath(...pathSegments: string[]) {
|
snapshotPath(...pathSegments: string[]) {
|
||||||
const subPath = path.join(...pathSegments);
|
const subPath = path.join(...pathSegments);
|
||||||
const parsedSubPath = path.parse(subPath);
|
const parsedSubPath = path.parse(subPath);
|
||||||
const relativeTestFilePath = path.relative(this.project.testDir, this._test._requireFile);
|
const relativeTestFilePath = path.relative(this.project.testDir, this._requireFile);
|
||||||
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
const parsedRelativeTestFilePath = path.parse(relativeTestFilePath);
|
||||||
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
const projectNamePathSegment = sanitizeForFilePath(this.project.name);
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ import type { Annotation, FullConfigInternal, FullProjectInternal } from '../com
|
|||||||
import { FixtureRunner } from './fixtureRunner';
|
import { FixtureRunner } from './fixtureRunner';
|
||||||
import { ManualPromise, gracefullyCloseAll, removeFolders } from 'playwright-core/lib/utils';
|
import { ManualPromise, gracefullyCloseAll, removeFolders } from 'playwright-core/lib/utils';
|
||||||
import { TestInfoImpl } from './testInfo';
|
import { TestInfoImpl } from './testInfo';
|
||||||
import { TimeoutManager } from './timeoutManager';
|
|
||||||
import { ProcessRunner } from '../common/process';
|
import { ProcessRunner } from '../common/process';
|
||||||
import { loadTestFile } from '../common/testLoader';
|
import { loadTestFile } from '../common/testLoader';
|
||||||
import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
|
import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils';
|
||||||
@ -53,7 +52,7 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
// This promise resolves once the single "run test group" call finishes.
|
// This promise resolves once the single "run test group" call finishes.
|
||||||
private _runFinished = new ManualPromise<void>();
|
private _runFinished = new ManualPromise<void>();
|
||||||
private _currentTest: TestInfoImpl | null = null;
|
private _currentTest: TestInfoImpl | null = null;
|
||||||
private _lastRunningTests: TestInfoImpl[] = [];
|
private _lastRunningTests: TestCase[] = [];
|
||||||
private _totalRunningTests = 0;
|
private _totalRunningTests = 0;
|
||||||
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
|
// Suites that had their beforeAll hooks, but not afterAll hooks executed.
|
||||||
// These suites still need afterAll hooks to be executed for the proper cleanup.
|
// These suites still need afterAll hooks to be executed for the proper cleanup.
|
||||||
@ -129,7 +128,7 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
'',
|
'',
|
||||||
'',
|
'',
|
||||||
colors.red(`Failed worker ran ${count}${lastMessage}:`),
|
colors.red(`Failed worker ran ${count}${lastMessage}:`),
|
||||||
...this._lastRunningTests.map(testInfo => formatTestTitle(testInfo._test, testInfo.project.name)),
|
...this._lastRunningTests.map(test => formatTestTitle(test, this._project.project.name)),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
if (error.stack) {
|
if (error.stack) {
|
||||||
@ -153,7 +152,7 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
|
|
||||||
private async _teardownScopeAndReturnFirstError(scope: FixtureScope, testInfo: TestInfoImpl): Promise<Error | undefined> {
|
private async _teardownScopeAndReturnFirstError(scope: FixtureScope, testInfo: TestInfoImpl): Promise<Error | undefined> {
|
||||||
let error: Error | undefined;
|
let error: Error | undefined;
|
||||||
await this._fixtureRunner.teardownScope(scope, testInfo._timeoutManager, e => {
|
await this._fixtureRunner.teardownScope(scope, testInfo, e => {
|
||||||
testInfo._failWithError(e, true, false);
|
testInfo._failWithError(e, true, false);
|
||||||
if (error === undefined)
|
if (error === undefined)
|
||||||
error = e;
|
error = e;
|
||||||
@ -163,15 +162,14 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
|
|
||||||
private async _teardownScopes() {
|
private async _teardownScopes() {
|
||||||
// TODO: separate timeout for teardown?
|
// TODO: separate timeout for teardown?
|
||||||
const timeoutManager = new TimeoutManager(this._project.project.timeout);
|
const fakeTestInfo = new TestInfoImpl(this._config, this._project, this._params, undefined, 0, () => {}, () => {}, () => {});
|
||||||
await timeoutManager.withRunnable({ type: 'teardown' }, async () => {
|
await fakeTestInfo._timeoutManager.withRunnable({ type: 'teardown' }, async () => {
|
||||||
const timeoutError = await timeoutManager.runWithTimeout(async () => {
|
await fakeTestInfo._runWithTimeout(async () => {
|
||||||
await this._fixtureRunner.teardownScope('test', timeoutManager, e => this._fatalErrors.push(serializeError(e)));
|
await this._teardownScopeAndReturnFirstError('test', fakeTestInfo);
|
||||||
await this._fixtureRunner.teardownScope('worker', timeoutManager, e => this._fatalErrors.push(serializeError(e)));
|
await this._teardownScopeAndReturnFirstError('worker', fakeTestInfo);
|
||||||
});
|
});
|
||||||
if (timeoutError)
|
|
||||||
this._fatalErrors.push(serializeError(timeoutError));
|
|
||||||
});
|
});
|
||||||
|
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
unhandledError(error: Error | any) {
|
unhandledError(error: Error | any) {
|
||||||
@ -322,7 +320,7 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._totalRunningTests++;
|
this._totalRunningTests++;
|
||||||
this._lastRunningTests.push(testInfo);
|
this._lastRunningTests.push(test);
|
||||||
if (this._lastRunningTests.length > 10)
|
if (this._lastRunningTests.length > 10)
|
||||||
this._lastRunningTests.shift();
|
this._lastRunningTests.shift();
|
||||||
let didFailBeforeAllForSuite: Suite | undefined;
|
let didFailBeforeAllForSuite: Suite | undefined;
|
||||||
@ -603,14 +601,14 @@ export class WorkerMain extends ProcessRunner {
|
|||||||
|
|
||||||
function buildTestBeginPayload(testInfo: TestInfoImpl): TestBeginPayload {
|
function buildTestBeginPayload(testInfo: TestInfoImpl): TestBeginPayload {
|
||||||
return {
|
return {
|
||||||
testId: testInfo._test.id,
|
testId: testInfo.testId,
|
||||||
startWallTime: testInfo._startWallTime,
|
startWallTime: testInfo._startWallTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTestEndPayload(testInfo: TestInfoImpl): TestEndPayload {
|
function buildTestEndPayload(testInfo: TestInfoImpl): TestEndPayload {
|
||||||
return {
|
return {
|
||||||
testId: testInfo._test.id,
|
testId: testInfo.testId,
|
||||||
duration: testInfo.duration,
|
duration: testInfo.duration,
|
||||||
status: testInfo.status!,
|
status: testInfo.status!,
|
||||||
errors: testInfo.errors,
|
errors: testInfo.errors,
|
||||||
|
@ -42,13 +42,10 @@ test('should have correct tags', async ({ runInlineTest }) => {
|
|||||||
'stdio.spec.js': `
|
'stdio.spec.js': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('no-tags', () => {
|
test('no-tags', () => {
|
||||||
expect(test.info()._test.tags).toEqual([]);
|
|
||||||
});
|
});
|
||||||
test('foo-tag @inline', { tag: '@foo' }, () => {
|
test('foo-tag @inline', { tag: '@foo' }, () => {
|
||||||
expect(test.info()._test.tags).toEqual(['@inline', '@foo']);
|
|
||||||
});
|
});
|
||||||
test('foo-bar-tags', { tag: ['@foo', '@bar'] }, () => {
|
test('foo-bar-tags', { tag: ['@foo', '@bar'] }, () => {
|
||||||
expect(test.info()._test.tags).toEqual(['@foo', '@bar']);
|
|
||||||
});
|
});
|
||||||
test.skip('skip-foo-tag', { tag: '@foo' }, () => {
|
test.skip('skip-foo-tag', { tag: '@foo' }, () => {
|
||||||
});
|
});
|
||||||
@ -59,11 +56,9 @@ test('should have correct tags', async ({ runInlineTest }) => {
|
|||||||
});
|
});
|
||||||
test.describe('suite @inline', { tag: '@foo' }, () => {
|
test.describe('suite @inline', { tag: '@foo' }, () => {
|
||||||
test('foo-suite', () => {
|
test('foo-suite', () => {
|
||||||
expect(test.info()._test.tags).toEqual(['@inline', '@foo']);
|
|
||||||
});
|
});
|
||||||
test.describe('inner', { tag: '@bar' }, () => {
|
test.describe('inner', { tag: '@bar' }, () => {
|
||||||
test('foo-bar-suite', () => {
|
test('foo-bar-suite', () => {
|
||||||
expect(test.info()._test.tags).toEqual(['@inline', '@foo', '@bar']);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user