chore(ui): start adding ui mode tests (2) (#21608)

This commit is contained in:
Pavel Feldman 2023-03-13 12:14:51 -07:00 committed by GitHub
parent 17498c30dc
commit 1d870ac407
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 372 additions and 20 deletions

View File

@ -87,7 +87,8 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
if (traceViewerBrowser === 'chromium')
await installAppIcon(page);
await syncLocalStorageWithSettings(page, 'traceviewer');
if (!isUnderTest())
await syncLocalStorageWithSettings(page, 'traceviewer');
const params = traceUrls.map(t => `trace=${t}`);
if (isUnderTest()) {

View File

@ -62,7 +62,7 @@ class UIMode {
projectDirs.add(p.testDir);
let coalescingTimer: NodeJS.Timeout | undefined;
const watcher = chokidar.watch([...projectDirs], { ignoreInitial: true, persistent: true }).on('all', async event => {
if (event !== 'add' && event !== 'change')
if (event !== 'add' && event !== 'change' && event !== 'unlink')
return;
if (coalescingTimer)
clearTimeout(coalescingTimer);

View File

@ -72,7 +72,7 @@
margin-top: 5px;
}
.list-view-entry:not(.highlighted) .toolbar-button:not(.toggled) {
.list-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) {
display: none;
}

View File

@ -149,7 +149,7 @@ export const WatchModeView: React.FC<{}> = ({
<div className='vbox watch-mode-sidebar'>
<Toolbar>
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
<ToolbarButton icon='play' title='Run' onClick={() => runTests(visibleTestIds)} disabled={isRunningTest}></ToolbarButton>
<ToolbarButton icon='play' title='Run all' onClick={() => runTests(visibleTestIds)} disabled={isRunningTest}></ToolbarButton>
<ToolbarButton icon='debug-stop' title='Stop' onClick={() => sendMessageNoReply('stop')} disabled={!isRunningTest}></ToolbarButton>
<ToolbarButton icon='refresh' title='Reload' onClick={() => refreshRootSuite(true)} disabled={isRunningTest}></ToolbarButton>
<div className='spacer'></div>
@ -202,7 +202,7 @@ export const WatchModeView: React.FC<{}> = ({
</div>;
};
const TreeListView = TreeView<TreeItem>;
const TestTreeView = TreeView<TreeItem>;
export const TestList: React.FC<{
projects: Map<string, boolean>,
@ -299,7 +299,7 @@ export const TestList: React.FC<{
if (!isVisible)
return <></>;
return <TreeListView
return <TestTreeView
treeState={treeState}
setTreeState={setTreeState}
rootItem={rootItem}
@ -336,6 +336,7 @@ export const TestList: React.FC<{
runningState.itemSelectedByUser = true;
setSelectedTreeItemId(treeItem.id);
}}
autoExpandDeep={!!filterText}
noItemsMessage='No tests' />;
};

View File

@ -40,6 +40,7 @@ export type TreeViewProps<T> = {
dataTestId?: string,
treeState: TreeState,
setTreeState: (treeState: TreeState) => void,
autoExpandDeep?: boolean,
};
const TreeListView = ListView<TreeItem>;
@ -57,12 +58,13 @@ export function TreeView<T extends TreeItem>({
setTreeState,
noItemsMessage,
dataTestId,
autoExpandDeep,
}: TreeViewProps<T>) {
const treeItems = React.useMemo(() => {
for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent)
treeState.expandedItems.set(item.id, true);
return flattenTree<T>(rootItem, treeState.expandedItems);
}, [rootItem, selectedItem, treeState]);
return flattenTree<T>(rootItem, treeState.expandedItems, autoExpandDeep);
}, [rootItem, selectedItem, treeState, autoExpandDeep]);
return <TreeListView
items={[...treeItems.keys()]}
@ -126,12 +128,12 @@ type TreeItemData = {
parent: TreeItem | null,
};
function flattenTree<T extends TreeItem>(rootItem: T, expandedItems: Map<string, boolean | undefined>): Map<T, TreeItemData> {
function flattenTree<T extends TreeItem>(rootItem: T, expandedItems: Map<string, boolean | undefined>, autoExpandDeep?: boolean): Map<T, TreeItemData> {
const result = new Map<T, TreeItemData>();
const appendChildren = (parent: T, depth: number) => {
for (const item of parent.children as T[]) {
const expandState = expandedItems.get(item.id);
const autoExpandMatches = depth === 0 && result.size < 25 && expandState !== false;
const autoExpandMatches = (autoExpandDeep || depth === 0) && result.size < 25 && expandState !== false;
const expanded = item.children.length ? expandState || autoExpandMatches : undefined;
result.set(item, { depth, expanded, parent: rootItem === parent ? null : parent });
if (expanded)

View File

@ -223,6 +223,7 @@ export type RunOptions = {
};
type Fixtures = {
writeFiles: (files: Files) => Promise<string>;
deleteFile: (file: string) => Promise<void>;
runInlineTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<RunResult>;
runWatchTest: (files: Files, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
runTSC: (files: Files) => Promise<TSCResult>;
@ -237,6 +238,13 @@ export const test = base
await use(files => writeFiles(testInfo, files, false));
},
deleteFile: async ({}, use, testInfo) => {
await use(async file => {
const baseDir = testInfo.outputPath();
await fs.promises.unlink(path.join(baseDir, file));
});
},
runInlineTest: async ({ childProcess }, use, testInfo: TestInfo) => {
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
await use(async (files: Files, params: Params = {}, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => {

View File

@ -38,10 +38,16 @@ export function dumpTestTree(page: Page): () => Promise<string> {
return ' ';
if (icon === 'circle-outline')
return '◯';
if (icon === 'circle-slash')
return '⊘';
if (icon === 'check')
return '✅';
if (icon === 'error')
return '❌';
if (icon === 'eye')
return '👁';
if (icon === 'loading')
return '↻';
return icon;
}
@ -52,8 +58,9 @@ export function dumpTestTree(page: Page): () => Promise<string> {
const treeIcon = iconName(iconElements[0]);
const statusIcon = iconName(iconElements[1]);
const indent = listItem.querySelectorAll('.list-view-indent').length;
const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : '';
const selected = listItem.classList.contains('selected') ? ' <=' : '';
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + listItem.textContent + selected);
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + listItem.textContent + watch + selected);
}
return '\n' + result.join('\n') + '\n ';
});

View File

@ -0,0 +1,99 @@
/**
* 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, dumpTestTree } from './ui-mode-fixtures';
test.describe.configure({ mode: 'parallel' });
const basicTestTree = {
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => { expect(1).toBe(2); });
test.describe('suite', () => {
test('inner passes', () => {});
test('inner fails', () => { expect(1).toBe(2); });
});
`,
'b.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => { expect(1).toBe(2); });
`,
'c.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test.skip('skipped', () => {});
`,
};
test('should run visible', async ({ runUITest }) => {
const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
`);
await page.getByTitle('Run all').click();
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails <=
suite
b.test.ts
passes
fails
c.test.ts
passes
skipped
`);
});
test('should run on double click', async ({ runUITest }) => {
const page = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => { expect(1).toBe(2); });
`,
});
await page.getByText('passes').dblclick();
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes <=
fails
`);
});
test('should run on Enter', async ({ runUITest }) => {
const page = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => { expect(1).toBe(2); });
`,
});
await page.getByText('fails').click();
await page.keyboard.press('Enter');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails <=
`);
});

View File

@ -37,7 +37,7 @@ const basicTestTree = {
test('should list tests', async ({ runUITest }) => {
const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page)).toBe(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails
@ -51,7 +51,7 @@ test('should list tests', async ({ runUITest }) => {
test('should traverse up/down', async ({ runUITest }) => {
const page = await runUITest(basicTestTree);
await page.getByText('a.test.ts').click();
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts <=
passes
fails
@ -59,14 +59,14 @@ test('should traverse up/down', async ({ runUITest }) => {
`);
await page.keyboard.press('ArrowDown');
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes <=
fails
suite
`);
await page.keyboard.press('ArrowDown');
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes
fails <=
@ -74,7 +74,7 @@ test('should traverse up/down', async ({ runUITest }) => {
`);
await page.keyboard.press('ArrowUp');
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes <=
fails
@ -87,7 +87,7 @@ test('should expand / collapse groups', async ({ runUITest }) => {
await page.getByText('suite').click();
await page.keyboard.press('ArrowRight');
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes
fails
@ -97,7 +97,7 @@ test('should expand / collapse groups', async ({ runUITest }) => {
`);
await page.keyboard.press('ArrowLeft');
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes
fails
@ -106,14 +106,25 @@ test('should expand / collapse groups', async ({ runUITest }) => {
await page.getByText('passes').first().click();
await page.keyboard.press('ArrowLeft');
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts <=
passes
fails
`);
await page.keyboard.press('ArrowLeft');
await expect.poll(dumpTestTree(page)).toContain(`
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts <=
`);
});
test('should filter by title', async ({ runUITest }) => {
const page = await runUITest(basicTestTree);
await page.getByPlaceholder('Filter').fill('inner');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
suite
inner passes
inner fails
`);
});

View File

@ -0,0 +1,170 @@
/**
* 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, dumpTestTree } from './ui-mode-fixtures';
test.describe.configure({ mode: 'parallel' });
const basicTestTree = {
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => {});
test.describe('suite', () => {
test('inner passes', () => {});
test('inner fails', () => {});
});
`,
'b.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => {});
`,
};
test('should pick new / deleted files', async ({ runUITest, writeFiles, deleteFile }) => {
const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails
suite
b.test.ts
passes
fails
`);
await writeFiles({
'c.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => {});
`
});
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails
suite
b.test.ts
passes
fails
c.test.ts
passes
fails
`);
await deleteFile('a.test.ts');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
b.test.ts
passes
fails
c.test.ts
passes
fails
`);
});
test('should pick new / deleted tests', async ({ runUITest, writeFiles, deleteFile }) => {
const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails
suite
b.test.ts
passes
fails
`);
await writeFiles({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('new', () => {});
test('fails', () => {});
`
});
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
new
fails
b.test.ts
passes
fails
`);
await deleteFile('a.test.ts');
await writeFiles({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('new', () => {});
`
});
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
new
b.test.ts
passes
fails
`);
});
test('should pick new / deleted nested tests', async ({ runUITest, writeFiles, deleteFile }) => {
const page = await runUITest(basicTestTree);
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes
fails
suite
`);
await page.getByText('suite').click();
await page.keyboard.press('ArrowRight');
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes
fails
suite <=
inner passes
inner fails
`);
await writeFiles({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test.describe('suite', () => {
test('inner new', () => {});
test('inner fails', () => {});
});
`
});
await expect.poll(dumpTestTree(page), { timeout: 0 }).toContain(`
a.test.ts
passes
suite <=
inner new
inner fails
`);
});

View File

@ -0,0 +1,53 @@
/**
* 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, dumpTestTree } from './ui-mode-fixtures';
test.describe.configure({ mode: 'parallel' });
test('should watch files', async ({ runUITest, writeFiles }) => {
const page = await runUITest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => { expect(1).toBe(2); });
`,
});
await page.getByText('fails').click();
await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Watch').click();
await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Run').click();
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails 👁 <=
`);
await writeFiles({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
test('fails', () => { expect(1).toBe(1); });
`
});
await expect.poll(dumpTestTree(page), { timeout: 0 }).toBe(`
a.test.ts
passes
fails 👁 <=
`);
});