test: add installation tests for supposed plugin story (#27331)
This commit is contained in:
parent
4fd2b4adef
commit
08e71fec5d
|
|
@ -14,6 +14,7 @@ browser_patches/chromium/output/
|
||||||
output/
|
output/
|
||||||
test-results/
|
test-results/
|
||||||
tests/components/
|
tests/components/
|
||||||
|
tests/installation/fixture-scripts/
|
||||||
examples/
|
examples/
|
||||||
DEPS
|
DEPS
|
||||||
.cache/
|
.cache/
|
||||||
|
|
@ -24,9 +24,9 @@ export function spawnAsync(cmd: string, args: string[], options: SpawnOptions =
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
if (process.stdout)
|
if (process.stdout)
|
||||||
process.stdout.on('data', data => stdout += data);
|
process.stdout.on('data', data => stdout += data.toString());
|
||||||
if (process.stderr)
|
if (process.stderr)
|
||||||
process.stderr.on('data', data => stderr += data);
|
process.stderr.on('data', data => stderr += data.toString());
|
||||||
process.on('close', code => resolve({ stdout, stderr, code }));
|
process.on('close', code => resolve({ stdout, stderr, code }));
|
||||||
process.on('error', error => resolve({ stdout, stderr, code: 0, error }));
|
process.on('error', error => resolve({ stdout, stderr, code: 0, error }));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { test as test1 } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import { test as test2 } from 'playwright-test-plugin';
|
||||||
|
|
||||||
|
const test = (test1 as any)._extendTest(test2);
|
||||||
|
|
||||||
|
test('sample test', async ({ page, plugin }) => {
|
||||||
|
type IsPage = (typeof page) extends Page ? true : never;
|
||||||
|
const isPage: IsPage = true;
|
||||||
|
|
||||||
|
type IsString = (typeof plugin) extends string ? true : never;
|
||||||
|
const isString: IsString = true;
|
||||||
|
});
|
||||||
12
tests/installation/fixture-scripts/plugin.spec.ts
Normal file
12
tests/installation/fixture-scripts/plugin.spec.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { test as test1, expect } from '@playwright/test';
|
||||||
|
import { test as test2 } from 'playwright-test-plugin';
|
||||||
|
|
||||||
|
const test = (test1 as any)._extendTest(test2);
|
||||||
|
|
||||||
|
test('sample test', async ({ page, plugin }) => {
|
||||||
|
await page.setContent(`<div>hello</div><span>world</span>`);
|
||||||
|
expect(await page.textContent('span')).toBe('world');
|
||||||
|
|
||||||
|
console.log(`plugin value: ${plugin}`);
|
||||||
|
expect(plugin).toBe('hello from plugin');
|
||||||
|
});
|
||||||
|
|
@ -65,6 +65,22 @@ async function globalSetup() {
|
||||||
build('playwright-browser-webkit', '@playwright/browser-webkit'),
|
build('playwright-browser-webkit', '@playwright/browser-webkit'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const buildPlaywrightTestPlugin = async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, `playwright-test-plugin`));
|
||||||
|
const tscResult = await spawnAsync('npx', ['tsc', '-p', 'tsconfig.json'], { cwd, shell: process.platform === 'win32' });
|
||||||
|
if (tscResult.code)
|
||||||
|
throw new Error(`Failed to build playwright-test-plugin:\n${tscResult.stderr}\n${tscResult.stdout}`);
|
||||||
|
const packResult = await spawnAsync('npm', ['pack'], { cwd, shell: process.platform === 'win32' });
|
||||||
|
if (packResult.code)
|
||||||
|
throw new Error(`Failed to build playwright-test-plugin:\n${packResult.stderr}\n${packResult.stdout}`);
|
||||||
|
const tgzName = packResult.stdout.trim();
|
||||||
|
const outPath = path.resolve(path.join(outputDir, `playwright-test-plugin.tgz`));
|
||||||
|
await fs.promises.rename(path.join(cwd, tgzName), outPath);
|
||||||
|
console.log('Built playwright-test-plugin');
|
||||||
|
return ['playwright-test-plugin', outPath];
|
||||||
|
};
|
||||||
|
builds.push(await buildPlaywrightTestPlugin());
|
||||||
|
|
||||||
await fs.promises.writeFile(path.join(__dirname, '.registry.json'), JSON.stringify(Object.fromEntries(builds)));
|
await fs.promises.writeFile(path.join(__dirname, '.registry.json'), JSON.stringify(Object.fromEntries(builds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,11 @@ import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import debugLogger from 'debug';
|
import debugLogger from 'debug';
|
||||||
import { Registry } from './registry';
|
import { Registry } from './registry';
|
||||||
import { spawnAsync } from './spawnAsync';
|
|
||||||
import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures';
|
import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures';
|
||||||
import { commonFixtures } from '../config/commonFixtures';
|
import { commonFixtures } from '../config/commonFixtures';
|
||||||
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
|
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
|
||||||
|
import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync';
|
||||||
|
import type { SpawnOptions } from 'child_process';
|
||||||
|
|
||||||
export const TMP_WORKSPACES = path.join(os.platform() === 'darwin' ? '/tmp' : os.tmpdir(), 'pwt', 'workspaces');
|
export const TMP_WORKSPACES = path.join(os.platform() === 'darwin' ? '/tmp' : os.tmpdir(), 'pwt', 'workspaces');
|
||||||
|
|
||||||
|
|
@ -56,7 +57,7 @@ const expect = _expect.extend({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type ExecOptions = { cwd?: string, env?: Record<string, string>, message?: string, expectToExitWithError?: boolean };
|
type ExecOptions = SpawnOptions & { message?: string, expectToExitWithError?: boolean };
|
||||||
type ArgsOrOptions = [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions];
|
type ArgsOrOptions = [] | [...string[]] | [...string[], ExecOptions] | [ExecOptions];
|
||||||
|
|
||||||
type NPMTestOptions = {
|
type NPMTestOptions = {
|
||||||
|
|
@ -71,7 +72,7 @@ type NPMTestFixtures = {
|
||||||
writeConfig: (allowGlobal: boolean) => Promise<void>;
|
writeConfig: (allowGlobal: boolean) => Promise<void>;
|
||||||
writeFiles: (nameToContents: Record<string, string>) => Promise<void>;
|
writeFiles: (nameToContents: Record<string, string>) => Promise<void>;
|
||||||
exec: (cmd: string, ...argsAndOrOptions: ArgsOrOptions) => Promise<string>;
|
exec: (cmd: string, ...argsAndOrOptions: ArgsOrOptions) => Promise<string>;
|
||||||
tsc: (...argsAndOrOptions: ArgsOrOptions) => Promise<string>;
|
tsc: (args: string) => Promise<string>;
|
||||||
registry: Registry;
|
registry: Registry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -153,7 +154,7 @@ export const test = _test
|
||||||
|
|
||||||
args = argsAndOrOptions as string[];
|
args = argsAndOrOptions as string[];
|
||||||
|
|
||||||
let result!: Awaited<ReturnType<typeof spawnAsync>>;
|
let result!: {stdout: string, stderr: string, code: number | null, error?: Error};
|
||||||
await test.step(`exec: ${[cmd, ...args].join(' ')}`, async () => {
|
await test.step(`exec: ${[cmd, ...args].join(' ')}`, async () => {
|
||||||
result = await spawnAsync(cmd, args, {
|
result = await spawnAsync(cmd, args, {
|
||||||
shell: true,
|
shell: true,
|
||||||
|
|
@ -197,8 +198,8 @@ export const test = _test
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
tsc: async ({ exec }, use) => {
|
tsc: async ({ exec }, use) => {
|
||||||
await exec('npm i --foreground-scripts typescript@3.8 @types/node@14');
|
await exec('npm i --foreground-scripts typescript@5.2.2 @types/node@16');
|
||||||
await use((...args: ArgsOrOptions) => exec('npx', '-p', 'typescript@4.1.6', 'tsc', ...args));
|
await use((args: string) => exec('npx', 'tsc', args, { shell: process.platform === 'win32' }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
75
tests/installation/playwright-test-plugin.spec.ts
Executable file
75
tests/installation/playwright-test-plugin.spec.ts
Executable file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './npmTest';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
function patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace: string) {
|
||||||
|
// It is not currently possible to declare plugin's peerDependency to match
|
||||||
|
// various pre-release versions, e.g. "1.38.0-next" and "1.39.1-alpha".
|
||||||
|
// See https://github.com/npm/rfcs/pull/397 and https://github.com/npm/node-semver#prerelease-tags.
|
||||||
|
//
|
||||||
|
// Workaround per https://stackoverflow.com/questions/71479750/npm-install-pre-release-versions-for-peer-dependency.
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(path.resolve(tmpWorkspace, 'package.json'), 'utf-8'));
|
||||||
|
if (pkg.dependencies['@playwright/test'].match(/\d+\.\d+-\w+/)) {
|
||||||
|
console.log(`Setting overrides in package.json to make pre-release version of peer dependency work.`);
|
||||||
|
pkg.overrides = { '@playwright/test': '$@playwright/test' };
|
||||||
|
fs.writeFileSync(path.resolve(tmpWorkspace, 'package.json'), JSON.stringify(pkg, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('npm: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) => {
|
||||||
|
await exec('npm i --foreground-scripts @playwright/test');
|
||||||
|
patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace);
|
||||||
|
await exec('npm i --foreground-scripts playwright-test-plugin');
|
||||||
|
await exec('npx playwright install chromium');
|
||||||
|
|
||||||
|
const output = await exec('npx playwright test -c . --browser=chromium --reporter=line plugin.spec.ts');
|
||||||
|
expect(output).toContain('plugin value: hello from plugin');
|
||||||
|
expect(output).toContain('1 passed');
|
||||||
|
|
||||||
|
await exec('npm i --foreground-scripts typescript@5.2.2 @types/node@16');
|
||||||
|
await exec('npx tsc playwright-test-plugin-types.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pnpm: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) => {
|
||||||
|
await exec('pnpm add @playwright/test');
|
||||||
|
patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace);
|
||||||
|
await exec('pnpm add playwright-test-plugin');
|
||||||
|
await exec('pnpm exec playwright install chromium');
|
||||||
|
|
||||||
|
const output = await exec('pnpm exec playwright test -c . --browser=chromium --reporter=line plugin.spec.ts');
|
||||||
|
expect(output).toContain('plugin value: hello from plugin');
|
||||||
|
expect(output).toContain('1 passed');
|
||||||
|
|
||||||
|
await exec('pnpm add typescript@5.2.2 @types/node@16');
|
||||||
|
await exec('pnpm exec tsc playwright-test-plugin-types.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('yarn: @playwright/test plugin should work', async ({ exec, tmpWorkspace }) => {
|
||||||
|
await exec('yarn add @playwright/test');
|
||||||
|
patchPackageJsonForPreReleaseIfNeeded(tmpWorkspace);
|
||||||
|
await exec('yarn add playwright-test-plugin');
|
||||||
|
await exec('yarn playwright install chromium');
|
||||||
|
|
||||||
|
const output = await exec('yarn playwright test -c . --browser=chromium --reporter=line plugin.spec.ts');
|
||||||
|
expect(output).toContain('plugin value: hello from plugin');
|
||||||
|
expect(output).toContain('1 passed');
|
||||||
|
|
||||||
|
await exec('yarn add typescript@5.2.2 @types/node@16');
|
||||||
|
await exec('yarn tsc playwright-test-plugin-types.ts');
|
||||||
|
});
|
||||||
2
tests/installation/playwright-test-plugin/.gitignore
vendored
Normal file
2
tests/installation/playwright-test-plugin/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
**/*.js
|
||||||
|
**/*.d.ts
|
||||||
2
tests/installation/playwright-test-plugin/.npmignore
Normal file
2
tests/installation/playwright-test-plugin/.npmignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
**/*.ts
|
||||||
|
!**/*.d.ts
|
||||||
23
tests/installation/playwright-test-plugin/index.ts
Normal file
23
tests/installation/playwright-test-plugin/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
|
||||||
|
export const test = base.extend<{ plugin: string }>({
|
||||||
|
plugin: async ({}, use) => {
|
||||||
|
await use('hello from plugin');
|
||||||
|
},
|
||||||
|
});
|
||||||
15
tests/installation/playwright-test-plugin/package.json
Normal file
15
tests/installation/playwright-test-plugin/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "playwright-test-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@playwright/test": "1.x"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
14
tests/installation/playwright-test-plugin/tsconfig.json
Normal file
14
tests/installation/playwright-test-plugin/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["esnext", "dom", "DOM.Iterable"],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"allowJs": false,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"useUnknownInCatchVariables": false,
|
||||||
|
"declaration": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ import path from 'path';
|
||||||
|
|
||||||
test('npm: @playwright/test should work', async ({ exec, tmpWorkspace }) => {
|
test('npm: @playwright/test should work', async ({ exec, tmpWorkspace }) => {
|
||||||
await exec('npm i --foreground-scripts @playwright/test');
|
await exec('npm i --foreground-scripts @playwright/test');
|
||||||
await exec('npx playwright test -c .', { expectToExitWithError: true, message: 'should not be able to run tests without installing browsers' });
|
await exec('npx playwright test -c . sample.spec.js', { expectToExitWithError: true, message: 'should not be able to run tests without installing browsers' });
|
||||||
|
|
||||||
await exec('npx playwright install');
|
await exec('npx playwright install');
|
||||||
await exec('npx playwright test -c . --browser=all --reporter=list,json sample.spec.js', { env: { PLAYWRIGHT_JSON_OUTPUT_NAME: 'report.json' } });
|
await exec('npx playwright test -c . --browser=all --reporter=list,json sample.spec.js', { env: { PLAYWRIGHT_JSON_OUTPUT_NAME: 'report.json' } });
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import type { Server } from 'http';
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { spawnAsync } from './spawnAsync';
|
import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync';
|
||||||
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
|
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
|
||||||
|
|
||||||
const kPublicNpmRegistry = 'https://registry.npmjs.org';
|
const kPublicNpmRegistry = 'https://registry.npmjs.org';
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
import type { SpawnOptions } from 'child_process';
|
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import debugLogger from 'debug';
|
|
||||||
|
|
||||||
const debugExec = debugLogger('itest:exec');
|
|
||||||
const debugExecStdout = debugLogger('itest:exec:stdout');
|
|
||||||
const debugExecStderr = debugLogger('itest:exec:stderr');
|
|
||||||
|
|
||||||
export function spawnAsync(cmd: string, args: string[], options: SpawnOptions = {}): Promise<{stdout: string, stderr: string, code: number | null, error?: Error}> {
|
|
||||||
// debugExec(`CWD: ${options.cwd || process.cwd()}`);
|
|
||||||
// debugExec(`ENV: ${Object.entries(options.env || {}).map(([key, value]) => `${key}=${value}`).join(' ')}`);
|
|
||||||
debugExec([cmd, ...args].join(' '));
|
|
||||||
const p = spawn(cmd, args, Object.assign({ windowsHide: true }, options));
|
|
||||||
|
|
||||||
return new Promise(resolve => {
|
|
||||||
let stdout = '';
|
|
||||||
let stderr = '';
|
|
||||||
if (p.stdout) {
|
|
||||||
p.stdout.on('data', data => {
|
|
||||||
debugExecStdout(data.toString());
|
|
||||||
stdout += data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (p.stderr) {
|
|
||||||
p.stderr.on('data', data => {
|
|
||||||
debugExecStderr(data.toString());
|
|
||||||
stderr += data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
p.on('close', code => resolve({ stdout, stderr, code }));
|
|
||||||
p.on('error', error => resolve({ stdout, stderr, code: 0, error }));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -37,8 +37,6 @@ test('typescript types should work', async ({ exec, tsc, writeFiles }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('typescript types should work with module: NodeNext', async ({ exec, tsc, writeFiles }) => {
|
test('typescript types should work with module: NodeNext', async ({ exec, tsc, writeFiles }) => {
|
||||||
// module: NodeNext got added in TypeScript 4.7
|
|
||||||
await exec('npm i --foreground-scripts typescript@4.7 @types/node@18');
|
|
||||||
const libraryPackages = [
|
const libraryPackages = [
|
||||||
'playwright',
|
'playwright',
|
||||||
'playwright-core',
|
'playwright-core',
|
||||||
|
|
@ -53,8 +51,8 @@ test('typescript types should work with module: NodeNext', async ({ exec, tsc, w
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
[filename]: `import { Page } from '${libraryPackage}';`,
|
[filename]: `import { Page } from '${libraryPackage}';`,
|
||||||
});
|
});
|
||||||
await exec('npx', '-p', 'typescript@4.7', 'tsc', '--module nodenext', filename);
|
await tsc(`--module nodenext ${filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await exec('npx', '-p', 'typescript@4.7', 'tsc', '--module nodenext', 'playwright-test-types.ts');
|
await tsc('--module nodenext playwright-test-types.ts');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,5 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"include": ["**/*.spec.js", "**/*.ts", "index.d.ts"],
|
"include": ["**/*.spec.js", "**/*.ts", "index.d.ts"],
|
||||||
"exclude": ["components/"]
|
"exclude": ["components/", "installation/fixture-scripts/"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue