Commit 36f9de66 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Playwright tests refactoring, pt.1 (#1744)

* abort all requests for external resources and single mock for text ad

* render and mockApiResponse fixtures

* move fixtures into separate files

* rewrite Blocks test

* refactor mockApiResponse signature

* test with auth

* rewrite test with other API domains

* fix test

* separate script for installing deps in pw docker
parent 50a96d5d
...@@ -17,6 +17,7 @@ NEXT_PUBLIC_IS_TESTNET=true ...@@ -17,6 +17,7 @@ NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
# api configuration # api configuration
NEXT_PUBLIC_API_PROTOCOL=http
NEXT_PUBLIC_API_HOST=localhost NEXT_PUBLIC_API_HOST=localhost
NEXT_PUBLIC_API_PORT=3003 NEXT_PUBLIC_API_PORT=3003
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
...@@ -47,8 +48,8 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true ...@@ -47,8 +48,8 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3100 NEXT_PUBLIC_AUTH_URL=http://localhost:3100
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004 NEXT_PUBLIC_STATS_API_HOST=http://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005 NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
\ No newline at end of file
...@@ -2,7 +2,7 @@ export const duck = { ...@@ -2,7 +2,7 @@ export const duck = {
ad: { ad: {
name: 'Hello utia!', name: 'Hello utia!',
description_short: 'Utia is the best! Go with utia! Utia is the best! Go with utia!', description_short: 'Utia is the best! Go with utia! Utia is the best! Go with utia!',
thumbnail: 'https://utia.utia', thumbnail: 'http://localhost:3100/utia.jpg',
url: 'https://test.url', url: 'https://test.url',
cta_button: 'Click me!', cta_button: 'Click me!',
}, },
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"test:pw": "./tools/scripts/pw.sh", "test:pw": "./tools/scripts/pw.sh",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw", "test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.sh", "test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.sh",
"test:pw:docker:deps": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-focal ./tools/scripts/pw.docker.deps.sh",
"test:pw:ci": "yarn test:pw --project=$PW_PROJECT", "test:pw:ci": "yarn test:pw --project=$PW_PROJECT",
"test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js", "test:pw:detect-affected": "node ./deploy/tools/affected-tests/index.js",
"test:jest": "jest", "test:jest": "jest",
......
...@@ -4,6 +4,8 @@ import react from '@vitejs/plugin-react'; ...@@ -4,6 +4,8 @@ import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr'; import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import appConfig from 'configs/app';
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
...@@ -36,6 +38,7 @@ const config: PlaywrightTestConfig = defineConfig({ ...@@ -36,6 +38,7 @@ const config: PlaywrightTestConfig = defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
baseURL: appConfig.app.baseUrl,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
......
...@@ -12,7 +12,7 @@ import wagmiConfig from 'lib/web3/wagmiConfig'; ...@@ -12,7 +12,7 @@ import wagmiConfig from 'lib/web3/wagmiConfig';
import * as app from 'playwright/utils/app'; import * as app from 'playwright/utils/app';
import theme from 'theme'; import theme from 'theme';
type Props = { export type Props = {
children: React.ReactNode; children: React.ReactNode;
withSocket?: boolean; withSocket?: boolean;
appContext?: { appContext?: {
......
import type { TestFixture, Page } from '@playwright/test';
import buildUrl from 'lib/api/buildUrl';
import type { ResourceName, ResourcePayload } from 'lib/api/resources';
interface Options<R extends ResourceName> {
pathParams?: Parameters<typeof buildUrl<R>>[1];
queryParams?: Parameters<typeof buildUrl<R>>[2];
}
export type MockApiResponseFixture = <R extends ResourceName>(resourceName: R, responseMock: ResourcePayload<R>, options?: Options<R>) => Promise<string>;
const fixture: TestFixture<MockApiResponseFixture, { page: Page }> = async({ page }, use) => {
await use(async(resourceName, responseMock, options) => {
const apiUrl = buildUrl(resourceName, options?.pathParams, options?.queryParams);
await page.route(apiUrl, (route) => route.fulfill({
status: 200,
body: JSON.stringify(responseMock),
}));
return apiUrl;
});
};
export default fixture;
import type { TestFixture, Page } from '@playwright/test';
export type MockAssetResponseFixture = (url: string, path: string) => Promise<void>;
const fixture: TestFixture<MockAssetResponseFixture, { page: Page }> = async({ page }, use) => {
await use(async(url, path) => {
await page.route(url, (route) => route.fulfill({
status: 200,
path,
}));
});
};
export default fixture;
import type { MountOptions } from '@playwright/experimental-ct-react';
import type { Locator, TestFixture } from '@playwright/test';
import type router from 'next/router';
import React from 'react';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { Props as TestAppProps } from 'playwright/TestApp';
import TestApp from 'playwright/TestApp';
interface MountResult extends Locator {
unmount(): Promise<void>;
update(component: JSX.Element): Promise<void>;
}
type Mount = <HooksConfig extends JsonObject>(component: JSX.Element, options?: MountOptions<HooksConfig>) => Promise<MountResult>;
interface Options extends JsonObject {
hooksConfig?: {
router: Partial<Pick<typeof router, 'query' | 'isReady' | 'asPath' | 'pathname'>>;
};
}
export type RenderFixture = (component: JSX.Element, options?: Options, props?: Omit<TestAppProps, 'children'>) => Promise<MountResult>
const fixture: TestFixture<RenderFixture, { mount: Mount }> = async({ mount }, use) => {
await use((component, options, props) => {
return mount(
<TestApp { ...props }>{ component }</TestApp>,
options,
);
});
};
export default fixture;
...@@ -10,16 +10,16 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -10,16 +10,16 @@ import type { Transaction } from 'types/api/transaction';
import * as app from 'playwright/utils/app'; import * as app from 'playwright/utils/app';
type ReturnType = () => Promise<WebSocket>; export type CreateSocketFixture = () => Promise<WebSocket>;
type Channel = [string, string, string]; type Channel = [string, string, string];
export interface SocketServerFixture { export interface SocketServerFixture {
createSocket: ReturnType; createSocket: CreateSocketFixture;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export const createSocket: TestFixture<ReturnType, { page: Page}> = async({ page }, use) => { export const createSocket: TestFixture<CreateSocketFixture, { page: Page}> = async({ page }, use) => {
const socketServer = new WebSocketServer({ port: app.socketPort }); const socketServer = new WebSocketServer({ port: app.socketPort });
const connectionPromise = new Promise<WebSocket>((resolve) => { const connectionPromise = new Promise<WebSocket>((resolve) => {
......
/* eslint-disable no-console */
import { test as base } from '@playwright/experimental-ct-react';
import * as textAdMock from 'mocks/ad/textAd';
import type { MockApiResponseFixture } from './fixtures/mockApiResponse';
import mockApiResponseFixture from './fixtures/mockApiResponse';
import type { MockAssetResponseFixture } from './fixtures/mockAssetResponse';
import mockAssetResponseFixture from './fixtures/mockAssetResponse';
import type { RenderFixture } from './fixtures/render';
import renderFixture from './fixtures/render';
import type { CreateSocketFixture } from './fixtures/socketServer';
import { createSocket as createSocketFixture } from './fixtures/socketServer';
interface Fixtures {
render: RenderFixture;
mockApiResponse: MockApiResponseFixture;
mockAssetResponse: MockAssetResponseFixture;
createSocket: CreateSocketFixture;
}
const test = base.extend<Fixtures>({
render: renderFixture,
mockApiResponse: mockApiResponseFixture,
mockAssetResponse: mockAssetResponseFixture,
createSocket: createSocketFixture,
});
test.beforeEach(async({ page }) => {
// debug
const isDebug = process.env.PWDEBUG === '1';
if (isDebug) {
page.on('console', msg => console.log(msg.text()));
page.on('request', request => console.info('\x1b[34m%s\x1b[0m', '>>', request.method(), request.url()));
page.on('response', response => console.info('\x1b[35m%s\x1b[0m', '<<', String(response.status()), response.url()));
}
// Abort all other requests to external resources
await page.route('**', (route) => {
if (!route.request().url().startsWith('http://localhost')) {
isDebug && console.info('Aborting request to', route.request().url());
route.abort();
} else {
route.continue();
}
});
// with few exceptions:
// 1. mock text AD requests
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
});
export * from '@playwright/experimental-ct-react';
export { test };
import { compile } from 'path-to-regexp'; import { compile } from 'path-to-regexp';
import config from 'configs/app';
import type { ResourceName, ResourcePathParams } from 'lib/api/resources'; import type { ResourceName, ResourcePathParams } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources';
// DEPRECATED
/**
* @deprecated please use fixture mockApiResponse from playwright/lib.tsx for rendering test suite
*
* @export
* @template R
* @param {R} resourceName
* @param {ResourcePathParams<R>} [pathParams]
* @return {*} string
*/
export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) { export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) {
const resource = RESOURCES[resourceName]; const resource = RESOURCES[resourceName];
const defaultApi = 'https://' + process.env.NEXT_PUBLIC_API_HOST + ':' + process.env.NEXT_PUBLIC_API_PORT; const origin = 'endpoint' in resource && resource.endpoint ? resource.endpoint + (resource.basePath ?? '') : config.api.endpoint;
const origin = 'endpoint' in resource && resource.endpoint ? resource.endpoint + (resource.basePath ?? '') : defaultApi;
return origin + compile(resource.path)(pathParams); return origin + compile(resource.path)(pathParams);
} }
#!/bin/bash
yarn install --modules-folder node_modules_linux
#!/bin/bash #!/bin/bash
yarn install --modules-folder node_modules_linux
export NODE_PATH=$(pwd)/node_modules_linux export NODE_PATH=$(pwd)/node_modules_linux
yarn test:pw "$@" yarn test:pw "$@"
...@@ -69,7 +69,7 @@ export type Transactions = Array<Transaction> ...@@ -69,7 +69,7 @@ export type Transactions = Array<Transaction>
export interface UserInfo { export interface UserInfo {
name?: string; name?: string;
nickname?: string; nickname?: string;
email: string; email: string | null;
avatar?: string; avatar?: string;
} }
......
import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import type { BrowserContext } from '@playwright/test';
import React from 'react'; import React from 'react';
import * as textAdMock from 'mocks/ad/textAd';
import * as blockMock from 'mocks/blocks/block'; import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import { test, expect, devices } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
import Blocks from './Blocks'; import Blocks from './Blocks';
const BLOCKS_API_URL = buildApiUrl('blocks') + '?type=block';
const STATS_API_URL = buildApiUrl('stats');
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { tab: 'blocks' }, query: { tab: 'blocks' },
...@@ -21,70 +17,28 @@ const hooksConfig = { ...@@ -21,70 +17,28 @@ const hooksConfig = {
}, },
}; };
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME // FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port // test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
test.beforeEach(async({ page }) => { test('base view +@dark-mode', async({ render, mockApiResponse }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
status: 200, await mockApiResponse('stats', statsMock.base);
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
});
test('base view +@dark-mode', async({ mount, page }) => { const component = await render(<Blocks/>, { hooksConfig });
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(BLOCKS_API_URL);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
const hiddenFieldsTest = test.extend({ const hiddenFieldsTest = test.extend<{ context: BrowserContext }>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields),
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any,
}); });
hiddenFieldsTest('hidden fields', async({ mount, page }) => { hiddenFieldsTest('hidden fields', async({ render, mockApiResponse }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({ await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
status: 200, await mockApiResponse('stats', statsMock.base);
body: JSON.stringify(blockMock.baseListResponse),
})); const component = await render(<Blocks/>, { hooksConfig });
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(BLOCKS_API_URL);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -92,66 +46,33 @@ hiddenFieldsTest('hidden fields', async({ mount, page }) => { ...@@ -92,66 +46,33 @@ hiddenFieldsTest('hidden fields', async({ mount, page }) => {
test.describe('mobile', () => { test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test(' base view', async({ mount, page }) => { test(' base view', async({ render, mockApiResponse }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({ await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
status: 200, await mockApiResponse('stats', statsMock.base);
body: JSON.stringify(blockMock.baseListResponse),
})); const component = await render(<Blocks/>, { hooksConfig });
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(BLOCKS_API_URL);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
const hiddenFieldsTest = test.extend({ const hiddenFieldsTest = test.extend<{ context: BrowserContext }>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields),
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any,
}); });
hiddenFieldsTest('hidden fields', async({ mount, page }) => { hiddenFieldsTest('hidden fields', async({ render, mockApiResponse }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({ await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
status: 200, await mockApiResponse('stats', statsMock.base);
body: JSON.stringify(blockMock.baseListResponse),
})); const component = await render(<Blocks/>, { hooksConfig });
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(BLOCKS_API_URL);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
test('new item from socket', async({ mount, page, createSocket }) => { test('new item from socket', async({ render, mockApiResponse, createSocket }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({ await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount( const component = await render(<Blocks/>, { hooksConfig }, { withSocket: true });
<TestApp withSocket>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket(); const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block'); const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
...@@ -167,18 +88,10 @@ test('new item from socket', async({ mount, page, createSocket }) => { ...@@ -167,18 +88,10 @@ test('new item from socket', async({ mount, page, createSocket }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('socket error', async({ mount, page, createSocket }) => { test('socket error', async({ render, mockApiResponse, createSocket }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({ await mockApiResponse('blocks', blockMock.baseListResponse, { queryParams: { type: 'block' } });
status: 200,
body: JSON.stringify(blockMock.baseListResponse), const component = await render(<Blocks/>, { hooksConfig }, { withSocket: true });
}));
const component = await mount(
<TestApp withSocket>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket(); const socket = await createSocket();
await socketServer.joinChannel(socket, 'blocks:new_block'); await socketServer.joinChannel(socket, 'blocks:new_block');
......
...@@ -44,7 +44,7 @@ const MyProfile = () => { ...@@ -44,7 +44,7 @@ const MyProfile = () => {
<Input <Input
required required
disabled disabled
value={ data.email } value={ data.email || '' }
/> />
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
</FormControl> </FormControl>
......
import { test as base, expect } from '@playwright/experimental-ct-react'; import type { BrowserContext } from '@playwright/test';
import React from 'react'; import React from 'react';
import * as textAdMock from 'mocks/ad/textAd';
import * as validatorsMock from 'mocks/validators/index'; import * as validatorsMock from 'mocks/validators/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import { test as base, expect } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
import Validators from './Validators'; import Validators from './Validators';
const VALIDATORS_API_URL = buildApiUrl('validators', { chainType: 'stability' }); const test = base.extend<{ context: BrowserContext }>({
const VALIDATORS_COUNTERS_API_URL = buildApiUrl('validators_counters', { chainType: 'stability' }); context: contextWithEnvs(configs.featureEnvs.validators),
const test = base.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.featureEnvs.validators) as any,
});
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
}); });
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ render, mockApiResponse }) => {
await page.route(VALIDATORS_API_URL, (route) => route.fulfill({ await mockApiResponse('validators', validatorsMock.validatorsResponse, { pathParams: { chainType: 'stability' } });
status: 200, await mockApiResponse('validators_counters', validatorsMock.validatorsCountersResponse, { pathParams: { chainType: 'stability' } });
body: JSON.stringify(validatorsMock.validatorsResponse),
}));
await page.route(VALIDATORS_COUNTERS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(validatorsMock.validatorsCountersResponse),
}));
const component = await mount( const component = await render(<Validators/>);
<TestApp>
<Validators/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as mocks from 'mocks/account/verifiedAddresses'; import * as mocks from 'mocks/account/verifiedAddresses';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import { test as base, expect } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import VerifiedAddresses from './VerifiedAddresses'; import VerifiedAddresses from './VerifiedAddresses';
const VERIFIED_ADDRESS_URL = buildApiUrl('verified_addresses', { chainId: '1' });
const TOKEN_INFO_APPLICATIONS_URL = buildApiUrl('token_info_applications', { chainId: '1', id: undefined });
const USER_INFO_URL = buildApiUrl('user_info');
const test = base.extend({ const test = base.extend({
context: ({ context }, use) => { context: ({ context }, use) => {
authFixture(context); authFixture(context);
...@@ -20,90 +14,37 @@ const test = base.extend({ ...@@ -20,90 +14,37 @@ const test = base.extend({
}, },
}); });
test.beforeEach(async({ context }) => { test.beforeEach(async({ mockAssetResponse }) => {
await context.route(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, (route) => { await mockAssetResponse(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, './playwright/mocks/image_s.jpg');
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
}); });
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ render, mockApiResponse }) => {
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({ await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } });
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT), await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT, { pathParams: { chainId: '1', id: undefined } });
})); await mockApiResponse('user_info', profileMock.base);
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT),
}));
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
const component = await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
);
const component = await render(<VerifiedAddresses/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('user without email', async({ mount, page }) => { test('user without email', async({ render, mockApiResponse }) => {
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({ await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } });
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT), await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT, { pathParams: { chainId: '1', id: undefined } });
})); await mockApiResponse('user_info', profileMock.withoutEmail);
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({ const component = await render(<VerifiedAddresses/>);
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT),
}));
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.withoutEmail),
}));
const component = await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('address verification flow', async({ mount, page }) => { test('address verification flow', async({ render, mockApiResponse, page }) => {
const CHECK_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '1', type: ':prepare' }); await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } });
const VERIFY_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '1', type: ':verify' }); await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT, { pathParams: { chainId: '1', id: undefined } });
await mockApiResponse('address_verification', mocks.ADDRESS_CHECK_RESPONSE.SUCCESS as never, { pathParams: { chainId: '1', type: ':prepare' } });
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({ await mockApiResponse('address_verification', mocks.ADDRESS_VERIFY_RESPONSE.SUCCESS as never, { pathParams: { chainId: '1', type: ':verify' } });
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT), await mockApiResponse('user_info', profileMock.base);
}));
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT),
}));
await page.route(CHECK_ADDRESS_URL, (route) => route.fulfill({ await render(<VerifiedAddresses/>);
body: JSON.stringify(mocks.ADDRESS_CHECK_RESPONSE.SUCCESS),
}));
await page.route(VERIFY_ADDRESS_URL, (route) => {
return route.fulfill({
body: JSON.stringify(mocks.ADDRESS_VERIFY_RESPONSE.SUCCESS),
});
});
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
);
// open modal // open modal
await page.getByRole('button', { name: /add address/i }).click(); await page.getByRole('button', { name: /add address/i }).click();
...@@ -125,37 +66,20 @@ test('address verification flow', async({ mount, page }) => { ...@@ -125,37 +66,20 @@ test('address verification flow', async({ mount, page }) => {
await expect(page).toHaveScreenshot(); await expect(page).toHaveScreenshot();
}); });
test('application update flow', async({ mount, page }) => { test('application update flow', async({ render, mockApiResponse, page }) => {
const TOKEN_INFO_APPLICATION_URL = buildApiUrl('token_info_applications', { chainId: '1', id: mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM.id }); await mockApiResponse('verified_addresses', mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT, { pathParams: { chainId: '1' } });
const FORM_CONFIG_URL = buildApiUrl('token_info_applications_config', { chainId: '1' }); await mockApiResponse('token_info_applications', mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.FOR_UPDATE, { pathParams: { chainId: '1', id: undefined } });
await mockApiResponse('user_info', profileMock.base);
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({ await mockApiResponse('token_info_applications_config', mocks.TOKEN_INFO_FORM_CONFIG, { pathParams: { chainId: '1' } });
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT),
}));
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({ await mockApiResponse(
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.FOR_UPDATE), 'token_info_applications',
})); mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM as never, // this mock is for PUT request
{ pathParams: { chainId: '1', id: mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM.id } },
await page.route(FORM_CONFIG_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_FORM_CONFIG),
}));
await page.route(USER_INFO_URL, (route) => route.fulfill({
body: JSON.stringify(profileMock.base),
}));
// PUT request
await page.route(TOKEN_INFO_APPLICATION_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM),
}));
await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
); );
await render(<VerifiedAddresses/>);
// open form // open form
await page.locator('tr').filter({ hasText: 'waiting for update' }).locator('button[aria-label="edit"]').click(); await page.locator('tr').filter({ hasText: 'waiting for update' }).locator('button[aria-label="edit"]').click();
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
import * as app from 'playwright/utils/app'; import * as app from 'playwright/utils/app';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ProfileMenuDesktop from './ProfileMenuDesktop'; import ProfileMenuDesktop from './ProfileMenuDesktop';
test('no auth', async({ mount, page }) => { test('no auth', async({ render, page }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
asPath: '/', asPath: '/',
pathname: '/', pathname: '/',
}, },
}; };
const component = await mount( const component = await render(<ProfileMenuDesktop/>, { hooksConfig });
<TestApp>
<ProfileMenuDesktop/>
</TestApp>,
{ hooksConfig },
);
await component.locator('a').click(); await component.locator('a').click();
expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`); expect(page.url()).toBe(`${ app.url }/auth/auth0?path=%2F`);
}); });
test.describe('auth', () => { const authTest = test.extend({
const extendedTest = test.extend({ context: ({ context }, use) => {
context: ({ context }, use) => { authFixture(context);
authFixture(context); use(context);
use(context); },
}, });
}); authTest('auth +@dark-mode', async({ render, page, mockApiResponse, mockAssetResponse }) => {
await mockApiResponse('user_info', profileMock.base);
extendedTest('+@dark-mode', async({ mount, page }) => { await mockAssetResponse(profileMock.base.avatar, './playwright/mocks/image_s.jpg');
await page.route(buildApiUrl('user_info'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(profileMock.base),
}));
await page.route(profileMock.base.avatar, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
const component = await mount( const component = await render(<ProfileMenuDesktop/>);
<TestApp> await component.getByAltText(/Profile picture/i).click();
<ProfileMenuDesktop/>
</TestApp>,
);
await component.getByAltText(/Profile picture/i).click(); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 600 } });
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 600 } });
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment