diff --git a/.github/workflows/tests_primary.yml b/.github/workflows/tests_primary.yml index f5d8c9b65d..764194b4f4 100644 --- a/.github/workflows/tests_primary.yml +++ b/.github/workflows/tests_primary.yml @@ -76,3 +76,21 @@ jobs: - run: xvfb-run npm run ttest if: matrix.os == 'ubuntu-latest' + test_html_report: + name: HTML Report + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 12 + - run: npm i -g npm@8 + - run: npm ci + env: + DEBUG: pw:install + - run: npm run build + - run: npx playwright install --with-deps + - run: npm run htest + if: matrix.os != 'ubuntu-latest' + - run: xvfb-run npm run htest + if: matrix.os == 'ubuntu-latest' diff --git a/package.json b/package.json index 89d0eafb5d..0867035423 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "wtest": "playwright test --config=tests/config/default.config.ts --project=webkit", "atest": "playwright test --config=tests/config/android.config.ts", "etest": "playwright test --config=tests/config/electron.config.ts", + "htest": "playwright test --config=packages/html-reporter", "ttest": "node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright-test.config.ts", "vtest": "cross-env PLAYWRIGHT_DOCKER=1 node ./tests/playwright-test/stable-test-runner/node_modules/@playwright/test/cli test --config=tests/playwright-test/playwright-test.config.ts", "test": "playwright test --config=tests/config/default.config.ts", diff --git a/packages/html-reporter/package.json b/packages/html-reporter/package.json index a2c087cbac..aee4ad9f33 100644 --- a/packages/html-reporter/package.json +++ b/packages/html-reporter/package.json @@ -1,4 +1,4 @@ { "name": "html-reporter", "private": true -} \ No newline at end of file +} diff --git a/packages/html-reporter/playwright.config.ts b/packages/html-reporter/playwright.config.ts new file mode 100644 index 0000000000..39f093f84e --- /dev/null +++ b/packages/html-reporter/playwright.config.ts @@ -0,0 +1,35 @@ +/** + * 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 { PlaywrightTestConfig, devices } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: 'src', + snapshotDir: 'snapshots', + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + use: { + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}; + +export default config; diff --git a/packages/html-reporter/playwright.stories.tsx b/packages/html-reporter/playwright.stories.tsx new file mode 100644 index 0000000000..1e12dbcb6d --- /dev/null +++ b/packages/html-reporter/playwright.stories.tsx @@ -0,0 +1,19 @@ +/** + * 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 './src/theme.css'; +import './src/chip.story.tsx'; +import './src/headerView.story.tsx'; diff --git a/packages/html-reporter/src/chip.spec.ts b/packages/html-reporter/src/chip.spec.ts new file mode 100644 index 0000000000..5c276543fa --- /dev/null +++ b/packages/html-reporter/src/chip.spec.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * 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 '../test/componentTest'; + +test.use({ webpack: require.resolve('../webpack.config.js') }); + +test('chip expand collapse', async ({ renderComponent }) => { + const component = await renderComponent('ChipComponent'); + await expect(component.locator('text=Chip body')).toBeVisible(); + // expect(await component.screenshot()).toMatchSnapshot('expanded.png'); + await component.locator('text=Title').click(); + await expect(component.locator('text=Chip body')).not.toBeVisible(); + // expect(await component.screenshot()).toMatchSnapshot('collapsed.png'); + await component.locator('text=Title').click(); + await expect(component.locator('text=Chip body')).toBeVisible(); + // expect(await component.screenshot()).toMatchSnapshot('expanded.png'); +}); + +test('chip render long title', async ({ renderComponent }) => { + const title = 'Extremely long title. '.repeat(10); + const component = await renderComponent('ChipComponent', { title }); + await expect(component).toContainText('Extremely long title.'); + await expect(component.locator('text=Extremely long title.')).toHaveAttribute('title', title); +}); + +test('chip setExpanded is called', async ({ renderComponent }) => { + const expandedValues: boolean[] = []; + const component = await renderComponent('ChipComponentWithFunctions', { + setExpanded: (expanded: boolean) => expandedValues.push(expanded) + }); + + await component.locator('text=Title').click(); + expect(expandedValues).toEqual([true]); +}); diff --git a/packages/html-reporter/src/chip.story.tsx b/packages/html-reporter/src/chip.story.tsx new file mode 100644 index 0000000000..b8fef79d65 --- /dev/null +++ b/packages/html-reporter/src/chip.story.tsx @@ -0,0 +1,42 @@ +/** + * 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 React from 'react'; +import { Chip } from './chip'; +import { registerComponent } from '../test/component'; + +const ChipComponent: React.FC<{ + title?: string +}> = ({ title }) => { + const [expanded, setExpanded] = React.useState(true); + return + Chip body + ; +}; +registerComponent('ChipComponent', ChipComponent, { + viewport: { width: 500, height: 500 }, +}); + +const ChipComponentWithFunctions: React.FC<{ + setExpanded: (expanded: boolean) => void, +}> = ({ setExpanded }) => { + return + Chip body + ; +}; +registerComponent('ChipComponentWithFunctions', ChipComponentWithFunctions, { + viewport: { width: 500, height: 500 }, +}); diff --git a/packages/html-reporter/src/chip.tsx b/packages/html-reporter/src/chip.tsx index e05a96dd10..a6c1edffce 100644 --- a/packages/html-reporter/src/chip.tsx +++ b/packages/html-reporter/src/chip.tsx @@ -16,6 +16,8 @@ import * as React from 'react'; import './chip.css'; +import './colors.css'; +import './common.css'; import * as icons from './icons'; export const Chip: React.FunctionComponent<{ @@ -26,7 +28,10 @@ export const Chip: React.FunctionComponent<{ children?: any, }> = ({ header, expanded, setExpanded, children, noInsets }) => { return
-
setExpanded?.(!expanded)}> +
setExpanded?.(!expanded)} + title={typeof header === 'string' ? header : undefined}> {setExpanded && !!expanded && icons.downArrow()} {setExpanded && !expanded && icons.rightArrow()} {header} diff --git a/packages/html-reporter/src/common.css b/packages/html-reporter/src/common.css index 6788413099..f9cb474524 100644 --- a/packages/html-reporter/src/common.css +++ b/packages/html-reporter/src/common.css @@ -14,6 +14,16 @@ limitations under the License. */ +:root { + --box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px; + --monospace-font: "SF Mono", Monaco, Consolas, "Droid Sans Mono", Inconsolata, "Courier New",monospace; + --box-shadow-thick: rgb(0 0 0 / 10%) 0px 1.8px 1.9px, + rgb(0 0 0 / 15%) 0px 6.1px 6.3px, + rgb(0 0 0 / 10%) 0px -2px 4px, + rgb(0 0 0 / 15%) 0px -6.1px 12px, + rgb(0 0 0 / 25%) 0px 6px 12px; +} + * { box-sizing: border-box; min-width: 0; diff --git a/packages/html-reporter/src/headerView.css b/packages/html-reporter/src/headerView.css new file mode 100644 index 0000000000..ef29c442ca --- /dev/null +++ b/packages/html-reporter/src/headerView.css @@ -0,0 +1,32 @@ +/* + 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. +*/ + +.header-view-status-container { + float: right; +} + +@media only screen and (max-width: 600px) { + .header-view-status-container { + float: none; + margin: 0 0 10px 0 !important; + overflow: hidden; + } + + .header-view-status-container .subnav-search-input { + border-left: none; + border-right: none; + } +} diff --git a/packages/html-reporter/src/headerView.spec.ts b/packages/html-reporter/src/headerView.spec.ts new file mode 100644 index 0000000000..7219407de6 --- /dev/null +++ b/packages/html-reporter/src/headerView.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * 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 { Stats } from '@playwright/test/src/reporters/html'; +import { test, expect } from '../test/componentTest'; + +test.use({ webpack: require.resolve('../webpack.config.js') }); + +test('should render counters', async ({ renderComponent }) => { + const stats: Stats = { + total: 100, + expected: 42, + unexpected: 31, + flaky: 17, + skipped: 10, + ok: false, + duration: 100000 + }; + const component = await renderComponent('HeaderView', { stats }); + await expect(component.locator('a').withText('All').locator('.counter')).toHaveText('100'); + await expect(component.locator('a').withText('Passed').locator('.counter')).toHaveText('42'); + await expect(component.locator('a').withText('Failed').locator('.counter')).toHaveText('31'); + await expect(component.locator('a').withText('Flaky').locator('.counter')).toHaveText('17'); + await expect(component.locator('a').withText('Skipped').locator('.counter')).toHaveText('10'); +}); + +test('should toggle filters', async ({ page, renderComponent }) => { + const stats: Stats = { + total: 100, + expected: 42, + unexpected: 31, + flaky: 17, + skipped: 10, + ok: false, + duration: 100000 + }; + const filters: string[] = []; + const component = await renderComponent('HeaderView', { + stats, + setFilterText: (filterText: string) => filters.push(filterText) + }); + await component.locator('a').withText('All').click(); + await component.locator('a').withText('Passed').click(); + await expect(page).toHaveURL(/#\?q=s:passed/); + await component.locator('a').withText('Failed').click(); + await expect(page).toHaveURL(/#\?q=s:failed/); + await component.locator('a').withText('Flaky').click(); + await expect(page).toHaveURL(/#\?q=s:flaky/); + await component.locator('a').withText('Skipped').click(); + await expect(page).toHaveURL(/#\?q=s:skipped/); + expect(filters).toEqual(['', 's:passed', 's:failed', 's:flaky', 's:skipped']); +}); diff --git a/packages/html-reporter/src/headerView.story.tsx b/packages/html-reporter/src/headerView.story.tsx new file mode 100644 index 0000000000..6706ac6c68 --- /dev/null +++ b/packages/html-reporter/src/headerView.story.tsx @@ -0,0 +1,22 @@ +/** + * 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 { registerComponent } from '../test/component'; +import { HeaderView } from './headerView'; + +registerComponent('HeaderView', HeaderView, { + viewport: { width: 720, height: 500 }, +}); diff --git a/packages/html-reporter/src/headerView.tsx b/packages/html-reporter/src/headerView.tsx index 5e90e9a2f1..641de1dca9 100644 --- a/packages/html-reporter/src/headerView.tsx +++ b/packages/html-reporter/src/headerView.tsx @@ -16,7 +16,9 @@ import type { Stats } from '@playwright/test/src/reporters/html'; import * as React from 'react'; +import './colors.css'; import './common.css'; +import './headerView.css'; import * as icons from './icons'; import { Link, navigate } from './links'; import { statusIcon } from './statusIcon'; @@ -36,7 +38,7 @@ export const HeaderView: React.FC<{ }); return
-
+
{ + const checkerboard = window.matchMedia('(prefers-color-scheme: dark)').matches ? checkerboardDark : checkerboardLight; + const bgStyle = { ...checkerboard }; + const fgStyle = { ...fillStyle, ...style }; + return React.createElement( + React.Fragment, null, + React.createElement('div', { style: bgStyle }), + React.createElement('div', { style: fgStyle, id: 'pw-root' }, children)); +}; + +const registry = new Map(); + +export const registerComponent = (name, component, options) => { + registry.set(name, { component, options }); +}; + +function render(name, params) { + const entry = registry.get(name); + ReactDOM.render( + React.createElement(Component, null, React.createElement(entry.component, params || null)), + document.getElementById('root')); +} + +function options(name) { + const entry = registry.get(name); + return entry.options; +} + +window.__playwright_render = render; +window.__playwright_options = options; diff --git a/packages/html-reporter/test/componentTest.ts b/packages/html-reporter/test/componentTest.ts new file mode 100644 index 0000000000..7834ff2af1 --- /dev/null +++ b/packages/html-reporter/test/componentTest.ts @@ -0,0 +1,78 @@ +/** + * 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 * as path from 'path'; +import { test as baseTest, Locator } from '@playwright/test'; + +declare global { + interface Window { + __playwright_render: (component: string, props: any) => void; + __playwright_options: (component: string) => { viewport: { width: number, height: number } }; + } +} + +type TestFixtures = { + renderComponent: (component: string, params?: any) => Promise; + webpack: string; +}; + +export const test = baseTest.extend({ + webpack: '', + renderComponent: async ({ page, webpack }, use, testInfo) => { + const webpackConfig = require(webpack); + const outputPath = webpackConfig.output.path; + const filename = webpackConfig.output.filename.replace('[name]', 'playwright'); + await use(async (component: string, optionalParams?: Object) => { + await page.route('http://component/index.html', route => { + route.fulfill({ + body: ` + + +
+ `, + contentType: 'text/html' + }); + }); + await page.goto('http://component/index.html'); + + await page.addScriptTag({ path: path.resolve(__dirname, outputPath, filename) }); + const options = await page.evaluate((component: string) => { + return window.__playwright_options(component); + }, component); + await page.setViewportSize(options.viewport); + + const params = { ...optionalParams }; + for (const [key, value] of Object.entries(params)) { + if (typeof value === 'function') { + const functionName = '__pw_func_' + key; + await page.exposeFunction(functionName, value); + (params as any)[key] = functionName; + } + } + await page.evaluate(v => { + const params = v.params; + for (const [key, value] of Object.entries(params)) { + if (typeof value === 'string' && (value as string).startsWith('__pw_func_')) + (params as any)[key] = window[value]; + } + window.__playwright_render(v.component, params); + }, { component, params }); + return page.locator('#pw-root'); + }); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/packages/html-reporter/webpack.config.js b/packages/html-reporter/webpack.config.js index 059d769852..f22a09a72c 100644 --- a/packages/html-reporter/webpack.config.js +++ b/packages/html-reporter/webpack.config.js @@ -25,6 +25,7 @@ module.exports = { entry: { zip: require.resolve('@zip.js/zip.js/dist/zip-no-worker-inflate.min.js'), app: path.join(__dirname, 'src', 'index.tsx'), + playwright: path.join(__dirname, 'playwright.stories.tsx'), }, resolve: { extensions: ['.ts', '.js', '.tsx', '.jsx'] @@ -59,6 +60,7 @@ module.exports = { title: 'Playwright Test Report', template: path.join(__dirname, 'src', 'index.html'), inject: true, + excludeChunks: ['playwright'], }), new BundleJsPlugin(), ]