feat(reporter): expose more apis (#9603)
This commit is contained in:
parent
6d727401bf
commit
6d554a5e30
|
|
@ -29,6 +29,16 @@ Returns the list of all test cases in this suite and its descendants, as opposit
|
||||||
|
|
||||||
Location in the source where the suite is defined. Missing for root and project suites.
|
Location in the source where the suite is defined. Missing for root and project suites.
|
||||||
|
|
||||||
|
## property: Suite.parent
|
||||||
|
- type: <[void]|[Suite]>
|
||||||
|
|
||||||
|
Parent suite or [void] for the root suite.
|
||||||
|
|
||||||
|
## property: Suite.project
|
||||||
|
- type: <[void]|[TestProject]>
|
||||||
|
|
||||||
|
Configuration of the project this suite belongs to, or [void] for the root suite.
|
||||||
|
|
||||||
## property: Suite.suites
|
## property: Suite.suites
|
||||||
- type: <[Array]<[Suite]>>
|
- type: <[Array]<[Suite]>>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,11 @@ The maximum number of retries given to this test in the configuration.
|
||||||
|
|
||||||
Learn more about [test retries](./test-retries.md#retries).
|
Learn more about [test retries](./test-retries.md#retries).
|
||||||
|
|
||||||
|
## property: TestCase.suite
|
||||||
|
- type: <[Suite]>
|
||||||
|
|
||||||
|
Suite this test case belongs to.
|
||||||
|
|
||||||
## property: TestCase.timeout
|
## property: TestCase.timeout
|
||||||
- type: <[float]>
|
- type: <[float]>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -230,7 +230,7 @@ export class Dispatcher {
|
||||||
retryCandidates.add(failedTestId);
|
retryCandidates.add(failedTestId);
|
||||||
|
|
||||||
let outermostSerialSuite: Suite | undefined;
|
let outermostSerialSuite: Suite | undefined;
|
||||||
for (let parent = this._testById.get(failedTestId)!.test.parent; parent; parent = parent.parent) {
|
for (let parent: Suite | undefined = this._testById.get(failedTestId)!.test.parent; parent; parent = parent.parent) {
|
||||||
if (parent._parallelMode === 'serial')
|
if (parent._parallelMode === 'serial')
|
||||||
outermostSerialSuite = parent;
|
outermostSerialSuite = parent;
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +241,7 @@ export class Dispatcher {
|
||||||
// We have failed tests that belong to a serial suite.
|
// We have failed tests that belong to a serial suite.
|
||||||
// We should skip all future tests from the same serial suite.
|
// We should skip all future tests from the same serial suite.
|
||||||
remaining = remaining.filter(test => {
|
remaining = remaining.filter(test => {
|
||||||
let parent = test.parent;
|
let parent: Suite | undefined = test.parent;
|
||||||
while (parent && !serialSuitesWithFailures.has(parent))
|
while (parent && !serialSuitesWithFailures.has(parent))
|
||||||
parent = parent.parent;
|
parent = parent.parent;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export class Loader {
|
||||||
constructor(defaultConfig: Config, configOverrides: Config) {
|
constructor(defaultConfig: Config, configOverrides: Config) {
|
||||||
this._defaultConfig = defaultConfig;
|
this._defaultConfig = defaultConfig;
|
||||||
this._configOverrides = configOverrides;
|
this._configOverrides = configOverrides;
|
||||||
this._fullConfig = baseFullConfig;
|
this._fullConfig = { ...baseFullConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deserialize(data: SerializedLoaderData): Promise<Loader> {
|
static async deserialize(data: SerializedLoaderData): Promise<Loader> {
|
||||||
|
|
@ -426,6 +426,7 @@ const baseFullConfig: FullConfig = {
|
||||||
quiet: false,
|
quiet: false,
|
||||||
shard: null,
|
shard: null,
|
||||||
updateSnapshots: 'missing',
|
updateSnapshots: 'missing',
|
||||||
|
version: require('../package.json').version,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
webServer: null,
|
webServer: null,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export class ProjectImpl {
|
||||||
let pool = this.buildTestTypePool(test._testType);
|
let pool = this.buildTestTypePool(test._testType);
|
||||||
|
|
||||||
const parents: Suite[] = [];
|
const parents: Suite[] = [];
|
||||||
for (let parent = test.parent; parent; parent = parent.parent)
|
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
|
||||||
parents.push(parent);
|
parents.push(parent);
|
||||||
parents.reverse();
|
parents.reverse();
|
||||||
|
|
||||||
|
|
@ -82,7 +82,6 @@ export class ProjectImpl {
|
||||||
private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
|
private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
|
||||||
for (const hook of from._allHooks) {
|
for (const hook of from._allHooks) {
|
||||||
const clone = hook._clone();
|
const clone = hook._clone();
|
||||||
clone.projectName = this.config.name;
|
|
||||||
clone._pool = this.buildPool(hook);
|
clone._pool = this.buildPool(hook);
|
||||||
clone._projectIndex = this.index;
|
clone._projectIndex = this.index;
|
||||||
to._addAllHook(clone);
|
to._addAllHook(clone);
|
||||||
|
|
@ -98,7 +97,6 @@ export class ProjectImpl {
|
||||||
} else {
|
} else {
|
||||||
const pool = this.buildPool(entry);
|
const pool = this.buildPool(entry);
|
||||||
const test = entry._clone();
|
const test = entry._clone();
|
||||||
test.projectName = this.config.name;
|
|
||||||
test.retries = this.config.retries;
|
test.retries = this.config.retries;
|
||||||
test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`;
|
test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`;
|
||||||
test._id = `${entry._ordinalInFile}@${entry._requireFile}#run${this.index}-repeat${repeatEachIndex}`;
|
test._id = `${entry._ordinalInFile}@${entry._requireFile}#run${this.index}-repeat${repeatEachIndex}`;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { FullProject } from '../types';
|
|
||||||
import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter';
|
import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter';
|
||||||
import { assert, calculateSha1 } from 'playwright-core/src/utils/utils';
|
import { assert, calculateSha1 } from 'playwright-core/src/utils/utils';
|
||||||
import { sanitizeForFilePath } from '../util';
|
import { sanitizeForFilePath } from '../util';
|
||||||
|
|
@ -109,7 +108,7 @@ class RawReporter {
|
||||||
async onEnd() {
|
async onEnd() {
|
||||||
const projectSuites = this.suite.suites;
|
const projectSuites = this.suite.suites;
|
||||||
for (const suite of projectSuites) {
|
for (const suite of projectSuites) {
|
||||||
const project = (suite as any)._projectConfig as FullProject;
|
const project = suite.project();
|
||||||
assert(project, 'Internal Error: Invalid project structure');
|
assert(project, 'Internal Error: Invalid project structure');
|
||||||
const reportFolder = path.join(project.outputDir, 'report');
|
const reportFolder = path.join(project.outputDir, 'report');
|
||||||
fs.mkdirSync(reportFolder, { recursive: true });
|
fs.mkdirSync(reportFolder, { recursive: true });
|
||||||
|
|
@ -132,7 +131,7 @@ class RawReporter {
|
||||||
|
|
||||||
generateProjectReport(config: FullConfig, suite: Suite): JsonReport {
|
generateProjectReport(config: FullConfig, suite: Suite): JsonReport {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
const project = (suite as any)._projectConfig as FullProject;
|
const project = suite.project();
|
||||||
assert(project, 'Internal Error: Invalid project structure');
|
assert(project, 'Internal Error: Invalid project structure');
|
||||||
const report: JsonReport = {
|
const report: JsonReport = {
|
||||||
config,
|
config,
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
let insideParallel = false;
|
let insideParallel = false;
|
||||||
for (let parent = test.parent; parent; parent = parent.parent)
|
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
|
||||||
insideParallel = insideParallel || parent._parallelMode === 'parallel';
|
insideParallel = insideParallel || parent._parallelMode === 'parallel';
|
||||||
|
|
||||||
if (insideParallel) {
|
if (insideParallel) {
|
||||||
|
|
|
||||||
|
|
@ -22,20 +22,12 @@ import { FullProject } from './types';
|
||||||
|
|
||||||
class Base {
|
class Base {
|
||||||
title: string;
|
title: string;
|
||||||
parent?: Suite;
|
|
||||||
|
|
||||||
_only = false;
|
_only = false;
|
||||||
_requireFile: string = '';
|
_requireFile: string = '';
|
||||||
|
|
||||||
constructor(title: string) {
|
constructor(title: string) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
titlePath(): string[] {
|
|
||||||
const titlePath = this.parent ? this.parent.titlePath() : [];
|
|
||||||
titlePath.push(this.title);
|
|
||||||
return titlePath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Modifier = {
|
export type Modifier = {
|
||||||
|
|
@ -49,6 +41,7 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||||
suites: Suite[] = [];
|
suites: Suite[] = [];
|
||||||
tests: TestCase[] = [];
|
tests: TestCase[] = [];
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
parent?: Suite;
|
||||||
_use: FixturesWithLocation[] = [];
|
_use: FixturesWithLocation[] = [];
|
||||||
_isDescribe = false;
|
_isDescribe = false;
|
||||||
_entries: (Suite | TestCase)[] = [];
|
_entries: (Suite | TestCase)[] = [];
|
||||||
|
|
@ -91,6 +84,12 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
titlePath(): string[] {
|
||||||
|
const titlePath = this.parent ? this.parent.titlePath() : [];
|
||||||
|
titlePath.push(this.title);
|
||||||
|
return titlePath;
|
||||||
|
}
|
||||||
|
|
||||||
_getOnlyItems(): (TestCase | Suite)[] {
|
_getOnlyItems(): (TestCase | Suite)[] {
|
||||||
const items: (TestCase | Suite)[] = [];
|
const items: (TestCase | Suite)[] = [];
|
||||||
if (this._only)
|
if (this._only)
|
||||||
|
|
@ -113,19 +112,24 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||||
suite._modifiers = this._modifiers.slice();
|
suite._modifiers = this._modifiers.slice();
|
||||||
suite._isDescribe = this._isDescribe;
|
suite._isDescribe = this._isDescribe;
|
||||||
suite._parallelMode = this._parallelMode;
|
suite._parallelMode = this._parallelMode;
|
||||||
|
suite._projectConfig = this._projectConfig;
|
||||||
return suite;
|
return suite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
project(): FullProject | undefined {
|
||||||
|
return this._projectConfig || this.parent?.project();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TestCase extends Base implements reporterTypes.TestCase {
|
export class TestCase extends Base implements reporterTypes.TestCase {
|
||||||
fn: Function;
|
fn: Function;
|
||||||
results: reporterTypes.TestResult[] = [];
|
results: reporterTypes.TestResult[] = [];
|
||||||
location: Location;
|
location: Location;
|
||||||
|
parent!: Suite;
|
||||||
|
|
||||||
expectedStatus: reporterTypes.TestStatus = 'passed';
|
expectedStatus: reporterTypes.TestStatus = 'passed';
|
||||||
timeout = 0;
|
timeout = 0;
|
||||||
annotations: Annotations = [];
|
annotations: Annotations = [];
|
||||||
projectName = '';
|
|
||||||
retries = 0;
|
retries = 0;
|
||||||
|
|
||||||
_type: 'beforeAll' | 'afterAll' | 'test';
|
_type: 'beforeAll' | 'afterAll' | 'test';
|
||||||
|
|
@ -146,6 +150,12 @@ export class TestCase extends Base implements reporterTypes.TestCase {
|
||||||
this.location = location;
|
this.location = location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
titlePath(): string[] {
|
||||||
|
const titlePath = this.parent ? this.parent.titlePath() : [];
|
||||||
|
titlePath.push(this.title);
|
||||||
|
return titlePath;
|
||||||
|
}
|
||||||
|
|
||||||
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
|
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
|
||||||
const nonSkipped = this.results.filter(result => result.status !== 'skipped');
|
const nonSkipped = this.results.filter(result => result.status !== 'skipped');
|
||||||
if (!nonSkipped.length)
|
if (!nonSkipped.length)
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inherit test.setTimeout() from parent suites.
|
// Inherit test.setTimeout() from parent suites.
|
||||||
for (let suite = test.parent; suite; suite = suite.parent) {
|
for (let suite: Suite | undefined = test.parent; suite; suite = suite.parent) {
|
||||||
if (suite._timeout !== undefined) {
|
if (suite._timeout !== undefined) {
|
||||||
testInfo.setTimeout(suite._timeout);
|
testInfo.setTimeout(suite._timeout);
|
||||||
break;
|
break;
|
||||||
|
|
@ -420,7 +420,7 @@ export class WorkerRunner extends EventEmitter {
|
||||||
private async _runBeforeHooks(test: TestCase, testInfo: TestInfoImpl) {
|
private async _runBeforeHooks(test: TestCase, testInfo: TestInfoImpl) {
|
||||||
try {
|
try {
|
||||||
const beforeEachModifiers: Modifier[] = [];
|
const beforeEachModifiers: Modifier[] = [];
|
||||||
for (let s = test.parent; s; s = s.parent) {
|
for (let s: Suite | undefined = test.parent; s; s = s.parent) {
|
||||||
const modifiers = s._modifiers.filter(modifier => !this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location));
|
const modifiers = s._modifiers.filter(modifier => !this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location));
|
||||||
beforeEachModifiers.push(...modifiers.reverse());
|
beforeEachModifiers.push(...modifiers.reverse());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
packages/playwright-test/types/test.d.ts
vendored
1
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -698,6 +698,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* Also available in the [command line](https://playwright.dev/docs/test-cli) with the `--max-failures` and `-x` options.
|
* Also available in the [command line](https://playwright.dev/docs/test-cli) with the `--max-failures` and `-x` options.
|
||||||
*/
|
*/
|
||||||
maxFailures: number;
|
maxFailures: number;
|
||||||
|
version: string;
|
||||||
/**
|
/**
|
||||||
* Whether to preserve test output in the
|
* Whether to preserve test output in the
|
||||||
* [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to `'always'`.
|
* [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to `'always'`.
|
||||||
|
|
|
||||||
11
packages/playwright-test/types/testReporter.d.ts
vendored
11
packages/playwright-test/types/testReporter.d.ts
vendored
|
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FullConfig, TestStatus, TestError } from './test';
|
import type { FullConfig, FullProject, TestStatus, TestError } from './test';
|
||||||
export type { FullConfig, TestStatus, TestError } from './test';
|
export type { FullConfig, TestStatus, TestError } from './test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,6 +57,10 @@ export interface Location {
|
||||||
* [reporter.onBegin(config, suite)](https://playwright.dev/docs/api/class-reporter#reporter-on-begin) method.
|
* [reporter.onBegin(config, suite)](https://playwright.dev/docs/api/class-reporter#reporter-on-begin) method.
|
||||||
*/
|
*/
|
||||||
export interface Suite {
|
export interface Suite {
|
||||||
|
/**
|
||||||
|
* Parent suite or [void] for the root suite.
|
||||||
|
*/
|
||||||
|
parent?: Suite;
|
||||||
/**
|
/**
|
||||||
* Suite title.
|
* Suite title.
|
||||||
* - Empty for root suite.
|
* - Empty for root suite.
|
||||||
|
|
@ -89,6 +93,10 @@ export interface Suite {
|
||||||
* [suite.tests](https://playwright.dev/docs/api/class-suite#suite-tests).
|
* [suite.tests](https://playwright.dev/docs/api/class-suite#suite-tests).
|
||||||
*/
|
*/
|
||||||
allTests(): TestCase[];
|
allTests(): TestCase[];
|
||||||
|
/**
|
||||||
|
* Configuration of the project this suite belongs to, or [void] for the root suite.
|
||||||
|
*/
|
||||||
|
project(): FullProject | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -98,6 +106,7 @@ export interface Suite {
|
||||||
* or repeated multiple times, it will have multiple `TestCase` objects in corresponding projects' suites.
|
* or repeated multiple times, it will have multiple `TestCase` objects in corresponding projects' suites.
|
||||||
*/
|
*/
|
||||||
export interface TestCase {
|
export interface TestCase {
|
||||||
|
parent: Suite;
|
||||||
/**
|
/**
|
||||||
* Test title as passed to the [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
|
* Test title as passed to the [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
|
||||||
* call.
|
* call.
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,16 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
|
||||||
}
|
}
|
||||||
onBegin(config, suite) {
|
onBegin(config, suite) {
|
||||||
console.log('\\n%%reporter-begin-' + this.options.begin + '%%');
|
console.log('\\n%%reporter-begin-' + this.options.begin + '%%');
|
||||||
|
console.log('\\n%%version-' + config.version);
|
||||||
}
|
}
|
||||||
onTestBegin(test) {
|
onTestBegin(test) {
|
||||||
console.log('\\n%%reporter-testbegin-' + test.title + '-' + test.titlePath()[1] + '%%');
|
const projectName = test.titlePath()[1];
|
||||||
|
console.log('\\n%%reporter-testbegin-' + test.title + '-' + projectName + '%%');
|
||||||
|
const suite = test.parent;
|
||||||
|
if (!suite.tests.includes(test))
|
||||||
|
console.log('\\n%%error-inconsistent-parent');
|
||||||
|
if (test.parent.project().name !== projectName)
|
||||||
|
console.log('\\n%%error-inconsistent-project-name');
|
||||||
}
|
}
|
||||||
onStdOut() {
|
onStdOut() {
|
||||||
console.log('\\n%%reporter-stdout%%');
|
console.log('\\n%%reporter-stdout%%');
|
||||||
|
|
@ -126,6 +133,7 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
|
expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
|
||||||
'%%reporter-begin-begin%%',
|
'%%reporter-begin-begin%%',
|
||||||
|
'%%version-' + require('../../packages/playwright-test/package.json').version,
|
||||||
'%%reporter-testbegin-is run-foo%%',
|
'%%reporter-testbegin-is run-foo%%',
|
||||||
'%%reporter-stdout%%',
|
'%%reporter-stdout%%',
|
||||||
'%%reporter-stderr%%',
|
'%%reporter-stderr%%',
|
||||||
|
|
|
||||||
1
utils/generate_types/overrides-test.d.ts
vendored
1
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -142,6 +142,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
grep: RegExp | RegExp[];
|
grep: RegExp | RegExp[];
|
||||||
grepInvert: RegExp | RegExp[] | null;
|
grepInvert: RegExp | RegExp[] | null;
|
||||||
maxFailures: number;
|
maxFailures: number;
|
||||||
|
version: string;
|
||||||
preserveOutput: PreserveOutput;
|
preserveOutput: PreserveOutput;
|
||||||
projects: FullProject<TestArgs, WorkerArgs>[];
|
projects: FullProject<TestArgs, WorkerArgs>[];
|
||||||
reporter: ReporterDescription[];
|
reporter: ReporterDescription[];
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FullConfig, TestStatus, TestError } from './test';
|
import type { FullConfig, FullProject, TestStatus, TestError } from './test';
|
||||||
export type { FullConfig, TestStatus, TestError } from './test';
|
export type { FullConfig, TestStatus, TestError } from './test';
|
||||||
|
|
||||||
export interface Location {
|
export interface Location {
|
||||||
|
|
@ -24,15 +24,18 @@ export interface Location {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Suite {
|
export interface Suite {
|
||||||
|
parent?: Suite;
|
||||||
title: string;
|
title: string;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
suites: Suite[];
|
suites: Suite[];
|
||||||
tests: TestCase[];
|
tests: TestCase[];
|
||||||
titlePath(): string[];
|
titlePath(): string[];
|
||||||
allTests(): TestCase[];
|
allTests(): TestCase[];
|
||||||
|
project(): FullProject | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestCase {
|
export interface TestCase {
|
||||||
|
parent: Suite;
|
||||||
title: string;
|
title: string;
|
||||||
location: Location;
|
location: Location;
|
||||||
titlePath(): string[];
|
titlePath(): string[];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue