Skip to content
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const ignores = [
"packages/playwright-core/src/tools/skills/",
"packages/html-reporter/bundle.ts",
"packages/html-reporter/playwright.config.ts",
"packages/html-reporter/playwright/*",
"packages/html-reporter/tests/*",
"packages/html-reporter/vite.config.ts",
"test-results/",
"tests/assets/",
Expand Down
4 changes: 2 additions & 2 deletions packages/html-reporter/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default defineConfig({
],
tag: process.env.PW_TAG,
use: {
baseURL: 'http://localhost:3101/playwright/gallery/index.html',
baseURL: 'http://localhost:3101/tests/index.html',
serviceWorkers: 'block',
reuseContext: true,
trace: 'on-first-retry',
Expand All @@ -49,7 +49,7 @@ export default defineConfig({
}],
webServer: {
command: 'npx vite --port 3101 --strictPort',
url: 'http://localhost:3101/playwright/gallery/index.html',
url: 'http://localhost:3101/tests/index.html',
reuseExistingServer: !process.env.CI,
},
});
16 changes: 7 additions & 9 deletions packages/html-reporter/src/chip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@

import { expect, test } from '@playwright/test';

import type { Auto, AutoCollapsed, NotExpandable, Stateful, WithBody } from './chip.story';

test.use({ viewport: { width: 500, height: 500 } });

test('expand collapse', async ({ mount }) => {
const component = await mount<typeof Auto>('chip/Auto');
const component = await mount('html-reporter/chip/Auto');
await expect(component.getByText('Chip body')).toBeVisible();
await component.getByText('Title').click();
await expect(component.getByText('Chip body')).not.toBeVisible();
Expand All @@ -31,27 +29,27 @@ test('expand collapse', async ({ mount }) => {

test('render long title', async ({ mount }) => {
const title = 'Extremely long title. '.repeat(10);
const component = await mount<typeof Auto>('chip/Auto', { header: title });
const component = await mount('html-reporter/chip/Auto', { header: title });
await expect(component).toContainText('Extremely long title.');
await expect(component.getByText('Extremely long title.')).toHaveAttribute('title', title);
});

test('setExpanded is called', async ({ mount }) => {
const component = await mount<typeof Stateful>('chip/Stateful');
const component = await mount('html-reporter/chip/Stateful');
await component.getByText('Title').click();
await expect(component.getByTestId('expanded')).toHaveValue('true');
await component.getByText('Title').click();
await expect(component.getByTestId('expanded')).toHaveValue('false');
});

test('body render prop is rendered', async ({ mount }) => {
const component = await mount<typeof WithBody>('chip/WithBody');
const component = await mount('html-reporter/chip/WithBody');
await expect(component.getByText('Body from render prop')).toBeVisible();
await expect(component.getByText('Chip children')).toBeVisible();
});

test('chip without setExpanded is a heading', async ({ mount }) => {
const component = await mount<typeof NotExpandable>('chip/NotExpandable');
const component = await mount('html-reporter/chip/NotExpandable');
await expect(component.getByRole('button')).toHaveCount(0);
await expect(component).toMatchAriaSnapshot(`
- heading "Title" [level=2]
Expand All @@ -60,7 +58,7 @@ test('chip without setExpanded is a heading', async ({ mount }) => {
});

test('expand collapse with the keyboard', async ({ mount, page }) => {
const component = await mount<typeof AutoCollapsed>('chip/AutoCollapsed');
const component = await mount('html-reporter/chip/AutoCollapsed');
const header = component.getByRole('button', { name: 'Title' });
await header.focus();
await expect(header).toBeFocused();
Expand All @@ -71,7 +69,7 @@ test('expand collapse with the keyboard', async ({ mount, page }) => {
});

test('setExpanded should work', async ({ mount }) => {
const component = await mount<typeof AutoCollapsed>('chip/AutoCollapsed');
const component = await mount('html-reporter/chip/AutoCollapsed');
await component.getByText('Title').click();
await expect(component).toMatchAriaSnapshot(`
- button "Title" [expanded]
Expand Down
8 changes: 3 additions & 5 deletions packages/html-reporter/src/headerView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@

import { expect, test } from '@playwright/test';

import type { Default } from './headerView.story';

test.use({ viewport: { width: 720, height: 200 } });

test('should render counters', async ({ mount }) => {
const component = await mount<typeof Default>('headerView/Default');
const component = await mount('html-reporter/headerView/Default');
await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('90');
await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42');
await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
Expand All @@ -38,7 +36,7 @@ test('should render counters', async ({ mount }) => {
});

test('should open settings with keyboard', async ({ mount }) => {
const component = await mount<typeof Default>('headerView/Default');
const component = await mount('html-reporter/headerView/Default');
const settings = component.getByRole('button', { name: 'Settings' });
const dialog = component.getByTestId('settings-dialog');
await settings.focus();
Expand All @@ -51,7 +49,7 @@ test('should open settings with keyboard', async ({ mount }) => {
});

test('should toggle filters', async ({ page, mount }) => {
const component = await mount<typeof Default>('headerView/Default');
const component = await mount('html-reporter/headerView/Default');
const filterText = component.getByTestId('filter-text');
await component.locator('a', { hasText: 'All' }).click();
await expect(filterText).toHaveValue('');
Expand Down
16 changes: 7 additions & 9 deletions packages/html-reporter/src/testCaseView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@

import { expect, test } from '@playwright/test';

import type { AnnotationLinks, AttachmentLinks, Default, PrevNext, TwoAttempts } from './testCaseView.story';

test.use({ viewport: { width: 800, height: 600 } });

test('should render test case', async ({ mount }) => {
const component = await mount<typeof Default>('testCaseView/Default');
const component = await mount('html-reporter/testCaseView/Default');
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
await expect(component.getByText('Hidden annotation')).toBeHidden();
await component.getByText('Annotations').click();
Expand All @@ -37,7 +35,7 @@ test('should render test case', async ({ mount }) => {
test('should render copy buttons for annotations', async ({ mount, page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);

const component = await mount<typeof Default>('testCaseView/Default');
const component = await mount('html-reporter/testCaseView/Default');
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
await component.getByText('Annotation text', { exact: false }).first().hover();
await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible();
Expand All @@ -48,7 +46,7 @@ test('should render copy buttons for annotations', async ({ mount, page, context
});

test('should correctly render links in annotations', async ({ mount }) => {
const component = await mount<typeof AnnotationLinks>('testCaseView/AnnotationLinks');
const component = await mount('html-reporter/testCaseView/AnnotationLinks');

const firstLink = component.getByText('https://playwright.dev/docs/intro').first();
await expect(firstLink).toBeVisible();
Expand All @@ -68,7 +66,7 @@ test('should correctly render links in annotations', async ({ mount }) => {
});

test('should correctly render links in attachments', async ({ mount }) => {
const component = await mount<typeof AttachmentLinks>('testCaseView/AttachmentLinks');
const component = await mount('html-reporter/testCaseView/AttachmentLinks');
await component.getByText('first attachment').click();
const body = component.getByText('The body with https://playwright.dev/docs/intro link');
await expect(body).toBeVisible();
Expand All @@ -81,7 +79,7 @@ test('should correctly render links in attachments', async ({ mount }) => {
});

test('should correctly render links in attachment name', async ({ mount }) => {
const component = await mount<typeof AttachmentLinks>('testCaseView/AttachmentLinks');
const component = await mount('html-reporter/testCaseView/AttachmentLinks');
const link = component.getByText('attachment with inline link').locator('a');
await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284');
Expand All @@ -91,7 +89,7 @@ test('should correctly render links in attachment name', async ({ mount }) => {
});

test('should correctly render prev and next', async ({ mount }) => {
const component = await mount<typeof PrevNext>('testCaseView/PrevNext');
const component = await mount('html-reporter/testCaseView/PrevNext');
await expect(component).toMatchAriaSnapshot(`
- text: group
- link "« previous"
Expand All @@ -101,7 +99,7 @@ test('should correctly render prev and next', async ({ mount }) => {
});

test('total duration is selected run duration', async ({ mount, page }) => {
const component = await mount<typeof TwoAttempts>('testCaseView/TwoAttempts');
const component = await mount('html-reporter/testCaseView/TwoAttempts');
await expect(component).toMatchAriaSnapshot(`
- text: "Test with two attempts test.spec.ts:42 200ms chromium"
- tablist:
Expand Down
4 changes: 1 addition & 3 deletions packages/html-reporter/src/testFileView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@

import { expect, test } from '@playwright/test';

import type { Default } from './testFileView.story';

test.use({ viewport: { width: 800, height: 600 } });

test('should render project links', async ({ mount, page }) => {
const component = await mount<typeof Default>('testFileView/Default');
const component = await mount('html-reporter/testFileView/Default');
await expect(component.locator('.label', { hasText: 'chromium' })).toHaveCount(5);
await expect(component.locator('.label', { hasText: 'webkit' })).toHaveCount(1);
const webkitLabel = component.locator('.label', { hasText: 'webkit' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

import { flushSync } from 'react-dom';
import { createRoot, type Root } from 'react-dom/client';
import '../../src/theme.css';
import { SearchParamsProvider } from '../../src/links';
import '../src/theme.css';
import { SearchParamsProvider } from '../src/links';
import packageJSON from '../package.json';

const stories = import.meta.glob('../../src/**/*.story.{tsx,jsx}');
const storyId = (file: string) => file.replace(/^(\.\.\/)+src\//, '').replace(/\.story\.\w+$/, '');
const stories = import.meta.glob('../src/**/*.story.{tsx,jsx}');
const storyId = (file: string) => packageJSON.name + '/' + file.replace(/^(\.\.\/)+src\//, '').replace(/\.story\.\w+$/, '');

async function resolveStory(id: string): Promise<React.ComponentType<any> | undefined> {
const sep = id.lastIndexOf('/');
Expand Down
28 changes: 28 additions & 0 deletions packages/html-reporter/tests/stories.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 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.
*/
// This file is generated by storyTypes.ts, do not edit manually.

type StoriesOf<Prefix extends string, Mod> = { [K in keyof Mod & string as `${Prefix}/${K}`]: Mod[K] };
Comment thread
pavelfeldman marked this conversation as resolved.

declare module '@playwright/test' {
interface Stories extends
StoriesOf<'html-reporter/chip', typeof import('../src/chip.story')>,
StoriesOf<'html-reporter/headerView', typeof import('../src/headerView.story')>,
StoriesOf<'html-reporter/testCaseView', typeof import('../src/testCaseView.story')>,
StoriesOf<'html-reporter/testFileView', typeof import('../src/testFileView.story')> {}
}

export {};
95 changes: 95 additions & 0 deletions packages/html-reporter/tests/storyTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* 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 fs from 'fs';
import path from 'path';
import type { Plugin } from 'vite';

const storyFile = /\.story\.(tsx|jsx)$/;
const license = `/**
* 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.
*/`;

// Story ids follow the gallery convention in main.tsx.
export function storyTypes(options: { prefix: string, src: string, outFile: string }): Plugin {
const generate = () => {
const content = render(listStoryFiles(options.src), options);
if (fs.existsSync(options.outFile) && fs.readFileSync(options.outFile, 'utf8') === content)
return;
fs.writeFileSync(options.outFile, content);
};
return {
name: 'playwright-story-types',
buildStart() {
this.addWatchFile(options.src);
generate();
},
configureServer(server) {
const onFile = (file: string) => {
if (storyFile.test(file))
generate();
};
server.watcher.on('add', onFile);
server.watcher.on('unlink', onFile);
},
};
}

function listStoryFiles(dir: string): string[] {
const result: string[] = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const file = path.join(dir, entry.name);
if (entry.isDirectory())
result.push(...listStoryFiles(file));
else if (storyFile.test(entry.name))
result.push(file);
}
return result.sort();
}

function render(files: string[], { prefix, src, outFile }: { prefix: string, src: string, outFile: string }): string {
const outDir = path.dirname(outFile);
const posix = (p: string) => p.split(path.sep).join('/');
const entries = files.map(file => {
const id = prefix + '/' + posix(path.relative(src, file)).replace(storyFile, '');
const specifier = posix(path.relative(outDir, file)).replace(/\.\w+$/, '');
return ` StoriesOf<'${id}', typeof import('${specifier.startsWith('.') ? specifier : './' + specifier}')>`;
});
const stories = entries.length ? `interface Stories extends\n${entries.join(',\n')} {}` : 'interface Stories {}';
return `${license}
// This file is generated by ${posix(path.relative(outDir, __filename))}, do not edit manually.

type StoriesOf<Prefix extends string, Mod> = { [K in keyof Mod & string as \`\${Prefix}/\${K}\`]: Mod[K] };

declare module '@playwright/test' {
${stories}
}

export {};
`;
}
5 changes: 4 additions & 1 deletion packages/html-reporter/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { bundle } from './bundle';
import { storyTypes } from './tests/storyTypes';
import packageJSON from './package.json';
import path from 'path';

// https://vitejs.dev/config/
export default defineConfig({
base: '',
plugins: [
react(),
bundle()
bundle(),
storyTypes({ prefix: packageJSON.name, src: path.resolve(__dirname, 'src'), outFile: path.resolve(__dirname, 'tests/stories.d.ts') }),
],
resolve: {
alias: {
Expand Down
Loading
Loading