Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/complex e2e tests #2422

Merged
merged 6 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Snackbar } from '@epam/uui';
import { Modals, PortalRoot, useDocumentDir } from '@epam/uui-components';
import { DragGhost } from '@epam/uui-core';
import { getCurrentTheme } from './helpers';
import { DocExamplePage } from './docExample/docExamplePage';

function App() {
return (
Expand All @@ -20,6 +21,7 @@ function App() {
<Route path="/demo" Component={ DemoPage } />
<Route path="/sandbox" Component={ SandboxPage } />
<Route path="/preview" Component={ PreviewPage } />
<Route path="/docExample" Component={ DocExamplePage } />
</Route>
</Routes>
);
Expand Down
16 changes: 15 additions & 1 deletion app/src/common/docs/DocExample.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@
max-width: 100%;
}

.container-footer-wrapper {
display: flex;
background-color: var(--uui-surface-main);

> .container-footer {
flex-grow: 1;
}

> a {
display: flex;
align-items: center;
padding: 12px;
}
}

.container-footer {
justify-content: space-between;
background-color: var(--uui-surface-main);
Expand All @@ -16,5 +31,4 @@
border: 1px solid var(--uui-divider);
width: 960px;
max-width: 100%;

}
45 changes: 36 additions & 9 deletions app/src/common/docs/DocExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import css from './DocExample.module.scss';
import { CodesandboxLink } from './CodesandboxLink';
import { Code } from './Code';
import cx from 'classnames';
import { docExampleLoader } from './docExampleLoader';
import { TTheme } from '../../data';
import { LinkButton } from '@epam/uui';
import { ReactComponent as PreviewIcon } from '@epam/assets/icons/common/media-fullscreen-12.svg';
import { getCurrentTheme } from '../../helpers';

interface DocExampleProps {
path: string;
Expand All @@ -23,18 +28,17 @@ interface DocExampleState {
raw?: string;
stylesheets?: FilesRecord;
}
const EXAMPLES_PATH_PREFIX = './_examples';

const requireContext = require.context('../../docs/_examples', true, /\.example.(ts|tsx)$/, 'lazy');
const EXAMPLES_PATH_PREFIX = './_examples';

export class DocExample extends React.Component<DocExampleProps, DocExampleState> {
componentDidMount(): void {
const { path, onlyCode } = this.props;

if (!onlyCode) {
const exPathRelative = `.${path.substring(EXAMPLES_PATH_PREFIX.length)}`;
requireContext(exPathRelative).then((module: any) => {
this.setState({ component: module.default });
docExampleLoader({ path: exPathRelative }).then((component) => {
this.setState({ component });
});
}

Expand Down Expand Up @@ -72,16 +76,21 @@ export class DocExample extends React.Component<DocExampleProps, DocExampleState

private renderPreview() {
const { raw } = this.state;
const dirPath = this.props.path.split('/').slice(0, -1);
const { path } = this.props;
const dirPath = path.split('/').slice(0, -1);
const theme = getCurrentTheme();
return (
<>
<FlexRow size={ null } vPadding="48" padding="24" borderBottom alignItems="top" columnGap="12">
{this.state.component && React.createElement(this.state.component)}
</FlexRow>
<FlexRow padding="12" vPadding="12" cx={ [css.containerFooter] }>
<Switch value={ this.state.showCode } onValueChange={ this.onSwitchValueChange } label="View code" />
<CodesandboxLink raw={ raw } dirPath={ dirPath } />
</FlexRow>
<div className={ css.containerFooterWrapper }>
<FlexRow padding="12" vPadding="12" cx={ [css.containerFooter] }>
<Switch value={ this.state.showCode } onValueChange={ this.onSwitchValueChange } label="View code" />
<CodesandboxLink raw={ raw } dirPath={ dirPath } />
</FlexRow>
<DocExampleFsBtn path={ path } theme={ theme } />
</div>
{this.state.showCode && this.renderCode()}
</>
);
Expand All @@ -98,3 +107,21 @@ export class DocExample extends React.Component<DocExampleProps, DocExampleState
);
}
}

const LABELS = {
Fullscreen: 'Fullscreen',
};
function DocExampleFsBtn(props: { path: string; theme: TTheme }) {
const regex = /^\.\/_examples\/(.*)\/(\w+)\.example\.tsx$/;
const examplePath = props.path.replace(regex, '$1/$2');
const href = `/docExample?theme=${encodeURIComponent(props.theme)}&examplePath=${encodeURIComponent(examplePath)}`;
return (
<LinkButton
target="_blank"
icon={ PreviewIcon }
href={ href }
caption={ LABELS.Fullscreen }
size="36"
/>
);
}
16 changes: 16 additions & 0 deletions app/src/common/docs/docExampleLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const requireContext = require.context('../../docs/_examples', true, /\.example.(ts|tsx)$/, 'lazy');

/**
* Path must be relative to 'app/src/docs/_examples/' folder.
*
* @param params.path - Example './alert/Basic.example.tsx'
* @param params.shortPath - Example 'alert/Basic'
*/
export async function docExampleLoader(params: { path: string } | { shortPath: string }) {
const path = (params as { path: string }).path;
const shortPath = (params as { shortPath: string }).shortPath;
const pathEffective: string = path ? path : `./${shortPath}.example.tsx`;

const module: any = await requireContext(pathEffective);
return module.default;
}
13 changes: 13 additions & 0 deletions app/src/docExample/docExampleContent.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.exampleRoot {
margin: 24px 0;
padding: 0 60px;
box-sizing: border-box;
}

.exampleContent {
background: var(--uui-surface-main);
border: 1px solid var(--uui-divider);
width: 960px;
max-width: 100%;
padding: 24px;
}
39 changes: 39 additions & 0 deletions app/src/docExample/docExampleContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { Alert, FlexRow, Spinner } from '@epam/uui';
import { Page } from '../common';

import css from './docExampleContent.module.scss';

interface IDocExampleContent {
isLoading: boolean;
errorMsg?: string;
Component?: React.FC;
}
export function DocExampleContent(props: IDocExampleContent) {
const { isLoading, errorMsg, Component } = props;

const renderContent = () => {
if (isLoading) {
return <Spinner />;
}
if (errorMsg) {
return <Alert color="error">{errorMsg}</Alert>;
}
if (Component) {
return (
<FlexRow cx={ css.exampleContent } size={ null } vPadding="48" padding="24" borderBottom alignItems="top" columnGap="12">
<Component />
</FlexRow>
);
}
return null;
};

return (
<Page renderHeader={ () => null }>
<div className={ css.exampleRoot } aria-busy={ isLoading } aria-label="Doc Example Content">
{ renderContent() }
</div>
</Page>
);
}
76 changes: 76 additions & 0 deletions app/src/docExample/docExamplePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from '../helpers';
import { docExampleLoader } from '../common/docs/docExampleLoader';
import { DocExampleContent } from './docExampleContent';
import { usePlayWrightInterface } from '../preview/hooks/usePlayWrightInterface';
import { BuiltInTheme, TTheme } from '../data';
import { svc } from '../services';

interface DocExamplePageParams {
examplePath: string;
theme: TTheme;
}

export function DocExamplePage() {
const [component, setComponent] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const examplePath = useQuery('examplePath'); // E.g.: http://localhost:3793/docExample?examplePath=alert/Basic

const handleNavDocExample = useCallback((newParams: DocExamplePageParams) => {
svc.uuiRouter.redirect({
pathname: '/docExample',
query: {
theme: newParams.theme || BuiltInTheme.promo,
examplePath: newParams.examplePath,
},
});
}, []);

usePlayWrightInterface<DocExamplePageParams>(handleNavDocExample);

useEffect(() => {
let isDestroyed = false;
if (examplePath) {
setComponent(null);
setIsLoading(true);
docExampleLoader({ shortPath: examplePath })
.then((result) => {
if (!isDestroyed) {
setComponent(() => result);
}
})
.catch((err) => {
if (!isDestroyed) {
console.error(`Unable to load example examplePath=${examplePath}`, err);
}
})
.finally(() => {
if (!isDestroyed) {
setIsLoading(false);
}
});
} else {
setIsLoading(false);
}
return () => {
isDestroyed = true;
};
}, [examplePath]);

const errorMsg = useMemo(() => {
if (!examplePath) {
return 'Required query parameter is missing: examplePath';
}
if (!isLoading && !component) {
return `Unable to load example; examplePath=${examplePath}`;
}
}, [component, examplePath, isLoading]);

return (
<DocExampleContent
isLoading={ isLoading }
Component={ component }
errorMsg={ errorMsg }
/>
);
}
5 changes: 2 additions & 3 deletions app/src/preview/hooks/usePlayWrightInterface.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { useEffect } from 'react';
import { TPreviewContentParams } from '../types';
import { PlayWrightInterfaceName } from '../constants';

export function usePlayWrightInterface(setter: (newParams: TPreviewContentParams) => void) {
export function usePlayWrightInterface<T extends object>(setter: (newParams: T) => void) {
useEffect(() => {
(window as any)[PlayWrightInterfaceName] = (_params: string) => {
setter(JSON.parse(_params) as TPreviewContentParams);
setter(JSON.parse(_params) as T);
};
return () => {
delete (window as any)[PlayWrightInterfaceName];
Expand Down
2 changes: 1 addition & 1 deletion app/src/preview/previewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function PreviewPage() {
});
}, []);

usePlayWrightInterface(handleNavPreview);
usePlayWrightInterface<TPreviewContentParams>(handleNavPreview);
usePreviewPageBg();

const key = `${theme}_${isSkin}_${componentId}_${previewId}`;
Expand Down
4 changes: 0 additions & 4 deletions uui-e2e-tests/.env
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,4 @@ UUI_APP_BASE_URL_CI=http://localhost:5000
#
# Explicitly specify a Docker-compatible container engine (docker, podman, etc).
#
# If left empty (recommended), the logic is following:
# - if Podman is installed, then "podman" is used;
# - otherwise "docker" is used as fallback;
#
UUI_DOCKER_CONTAINER_ENGINE=
2 changes: 1 addition & 1 deletion uui-e2e-tests/e2e.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# The version of the base image must be in sync with the version of "@playwright/test" NPM package
FROM mcr.microsoft.com/playwright:v1.44.1-jammy
FROM mcr.microsoft.com/playwright:v1.45.2-jammy

WORKDIR /e2e

Expand Down
1 change: 1 addition & 0 deletions uui-e2e-tests/framework/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PlaywrightTestOptions } from '@playwright/test';

export const PLATFORM = 'linux';
export const PREVIEW_URL = '/preview';
export const DOC_EXAMPLE_URL = '/docExample';
export const PlayWrightInterfaceName = '_uui_playwright_interface';

/*
Expand Down
47 changes: 47 additions & 0 deletions uui-e2e-tests/framework/fixtures/docExamplePage/docExamplePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Locator, Page } from '@playwright/test';
import { DocExamplePageParams, TEngine, TTheme } from '../../types';
import { DOC_EXAMPLE_URL, PlayWrightInterfaceName } from '../../constants';

export class DocExamplePage {
private readonly locators: {
regionContentNotBusy: Locator;
};

private readonly engine: TEngine;
public readonly page: Page;

constructor(params: { page: Page, engine: TEngine }) {
const { page, engine } = params;
this.page = page;
this.engine = engine;
this.locators = {
regionContentNotBusy: page.locator('[aria-label="Doc Example Content"][aria-busy="false"]'),
};
}

async goto() {
await this.page.goto(DOC_EXAMPLE_URL);
}

async editDocExample(params: Pick<DocExamplePageParams, 'examplePath'>) {
const paramsFull: DocExamplePageParams = {
// As we agreed, "doc example" tests must be always run on "loveship" theme
theme: TTheme.loveship,
...params,
};
await this.page.evaluate((_params: string) => {
const [p, i] = _params.split('[||||]');
// @ts-ignore Reason: this specific code will be run in context of web page
(window as any)[i](p);
}, [jsonStringify(paramsFull), PlayWrightInterfaceName].join('[||||]'));
await this.locators.regionContentNotBusy.waitFor();
}

async close() {

}
}

function jsonStringify(json: object) {
return JSON.stringify(json, undefined, 1);
}
29 changes: 29 additions & 0 deletions uui-e2e-tests/framework/fixtures/docExamplePage/fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Page, test as baseTest } from '@playwright/test';
import { mockApi } from '../../mocks/apiMocks';
import { timeoutForFixture } from '../../../playwright.config';
import { TEngine } from '../../types';
import { DocExamplePage } from './docExamplePage';

const test = baseTest.extend<{}, { docExamplePage: DocExamplePage }>({
docExamplePage: [
async ({ browser }, use, { project }) => {
const context = await browser.newContext();
let page: Page | undefined;
let docExamplePage: DocExamplePage | undefined;
try {
page = await context.newPage();
await mockApi(page);
docExamplePage = new DocExamplePage({ page, engine: project.name as TEngine });
await docExamplePage.goto();
await page.waitForTimeout(50);
await use(docExamplePage);
} finally {
docExamplePage && await docExamplePage.close();
await context.close();
}
},
{ scope: 'worker', timeout: timeoutForFixture },
],
});

export { test };
Loading
Loading