From 74ab343e2becd2963f72ae67ecba7c90c6bceb21 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 25 Aug 2022 11:58:58 +0200 Subject: [PATCH] feat(codegen): add NUnit/MSTest (#16803) --- packages/playwright-core/src/cli/cli.ts | 2 +- .../playwright-core/src/server/recorder.ts | 14 ++-- .../src/server/recorder/csharp.ts | 69 +++++++++++++++-- .../inspector/cli-codegen-csharp.spec.ts | 77 +++++++++++++++++++ tests/library/inspector/inspectorTest.ts | 2 + 5 files changed, 152 insertions(+), 12 deletions(-) diff --git a/packages/playwright-core/src/cli/cli.ts b/packages/playwright-core/src/cli/cli.ts index ba9fe78eec..effcb0ab7e 100755 --- a/packages/playwright-core/src/cli/cli.ts +++ b/packages/playwright-core/src/cli/cli.ts @@ -69,7 +69,7 @@ Examples: commandWithOpenOptions('codegen [url]', 'open page and generate code for user actions', [ ['-o, --output ', 'saves the generated script to a file'], - ['--target ', `language to generate, one of javascript, test, python, python-async, pytest, csharp, java`, language()], + ['--target ', `language to generate, one of javascript, test, python, python-async, pytest, csharp, csharp-mstest, csharp-nunit, java`, language()], ['--save-trace ', 'record a trace for the session and save it to a file'], ]).action(function(url, options) { codegen(options, url, options.target, options.output).catch(logErrorAndExit); diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index a5f2808164..b1174b63f5 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -362,12 +362,14 @@ class ContextRecorder extends EventEmitter { setOutput(language: string, outputFile: string | undefined) { const languages = new Set([ new JavaLanguageGenerator(), - new JavaScriptLanguageGenerator(false), - new JavaScriptLanguageGenerator(true), - new PythonLanguageGenerator(false, true), - new PythonLanguageGenerator(false, false), - new PythonLanguageGenerator(true, false), - new CSharpLanguageGenerator(), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false), + new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false), + new CSharpLanguageGenerator('mstest'), + new CSharpLanguageGenerator('nunit'), + new CSharpLanguageGenerator('library'), ]); const primaryLanguage = [...languages].find(l => l.id === language); if (!primaryLanguage) diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/recorder/csharp.ts index ccb0b47547..a147fc45f7 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/recorder/csharp.ts @@ -25,17 +25,46 @@ import { toModifiers } from './utils'; import { escapeWithQuotes } from '../../utils/isomorphic/stringUtils'; import deviceDescriptors from '../deviceDescriptors'; +type CSharpLanguageMode = 'library' | 'mstest' | 'nunit'; + export class CSharpLanguageGenerator implements LanguageGenerator { - id = 'csharp'; - groupName = '.NET'; - name = 'Library C#'; + id: string; + groupName = '.NET C#'; + name: string; highlighter = 'csharp'; + _mode: CSharpLanguageMode; + + constructor(mode: CSharpLanguageMode) { + if (mode === 'library') { + this.name = 'Library'; + this.id = 'csharp'; + } else if (mode === 'mstest') { + this.name = 'MSTest'; + this.id = 'csharp-mstest'; + } else if (mode === 'nunit') { + this.name = 'NUnit'; + this.id = 'csharp-nunit'; + } else { + throw new Error(`Unknown C# language mode: ${mode}`); + } + this._mode = mode; + } generateAction(actionInContext: ActionInContext): string { + const action = this._generateActionInner(actionInContext); + if (action) + return action + '\n'; + return ''; + } + + _generateActionInner(actionInContext: ActionInContext): string { const action = actionInContext.action; - const pageAlias = actionInContext.frame.pageAlias; + if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage')) + return ''; + let pageAlias = actionInContext.frame.pageAlias; + if (this._mode !== 'library') + pageAlias = pageAlias.replace('page', 'Page'); const formatter = new CSharpFormatter(8); - formatter.newLine(); formatter.add('// ' + actionTitle(action)); if (action.name === 'openPage') { @@ -137,6 +166,12 @@ export class CSharpLanguageGenerator implements LanguageGenerator { } generateHeader(options: LanguageGeneratorOptions): string { + if (this._mode === 'library') + return this.generateStandaloneHeader(options); + return this.generateTestRunnerHeader(options); + } + + generateStandaloneHeader(options: LanguageGeneratorOptions): string { const formatter = new CSharpFormatter(0); formatter.add(` using Microsoft.Playwright; @@ -150,6 +185,30 @@ export class CSharpLanguageGenerator implements LanguageGenerator { using var playwright = await Playwright.CreateAsync(); await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatObject(options.launchOptions, ' ', 'BrowserTypeLaunchOptions')}); var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`); + formatter.newLine(); + return formatter.format(); + } + + generateTestRunnerHeader(options: LanguageGeneratorOptions): string { + const formatter = new CSharpFormatter(0); + formatter.add(` + using Microsoft.Playwright.${this._mode === 'nunit' ? 'NUnit' : 'MSTest'}; + using Microsoft.Playwright; + + ${this._mode === 'nunit' ? '[Parallelizable(ParallelScope.Self)]' : '[TestClass]'} + public class Tests : PageTest + {`); + const formattedContextOptions = formatContextOptions(options.contextOptions, options.deviceName); + if (formattedContextOptions) { + formatter.add(`public override BrowserNewContextOptions ContextOptions() + { + return ${formattedContextOptions}; + }`); + formatter.newLine(); + } + formatter.add(` [${this._mode === 'nunit' ? 'Test' : 'TestMethod'}] + public async Task MyTest() + {`); return formatter.format(); } diff --git a/tests/library/inspector/cli-codegen-csharp.spec.ts b/tests/library/inspector/cli-codegen-csharp.spec.ts index 3c22373a1a..4973906116 100644 --- a/tests/library/inspector/cli-codegen-csharp.spec.ts +++ b/tests/library/inspector/cli-codegen-csharp.spec.ts @@ -193,3 +193,80 @@ test('should work with --save-har', async ({ runCLI }, testInfo) => { const json = JSON.parse(fs.readFileSync(harFileName, 'utf-8')); expect(json.log.creator.name).toBe('Playwright'); }); + +for (const testFramework of ['nunit', 'mstest'] as const) { + test(`should not print context options method override in ${testFramework} if no options were passed`, async ({ runCLI }) => { + const cli = runCLI([`--target=csharp-${testFramework}`, emptyHTML]); + await cli.waitFor(`Page.GotoAsync("${emptyHTML}")`); + expect(cli.text()).not.toContain('public override BrowserNewContextOptions ContextOptions()'); + }); + + test(`should print context options method override in ${testFramework} if options were passed`, async ({ runCLI }) => { + const cli = runCLI([`--target=csharp-${testFramework}`, '--color-scheme=dark', emptyHTML]); + await cli.waitFor(`Page.GotoAsync("${emptyHTML}")`); + expect(cli.text()).toContain(` public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions + { + ColorScheme = ColorScheme.Dark, + }; + } +`); + }); +} + +test(`should print a valid basic program in mstest`, async ({ runCLI }) => { + const cli = runCLI([`--target=csharp-mstest`, '--color-scheme=dark', emptyHTML]); + await cli.waitFor(`Page.GotoAsync("${emptyHTML}")`); + const expected = `using Microsoft.Playwright.MSTest; +using Microsoft.Playwright; + +[TestClass] +public class Tests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions + { + ColorScheme = ColorScheme.Dark, + }; + } + + [TestMethod] + public async Task MyTest() + { + // Go to ${emptyHTML} + await Page.GotoAsync("${emptyHTML}"); + + } +}`; + expect(cli.text()).toContain(expected); +}); + +test(`should print a valid basic program in nunit`, async ({ runCLI }) => { + const cli = runCLI([`--target=csharp-nunit`, '--color-scheme=dark', emptyHTML]); + await cli.waitFor(`Page.GotoAsync("${emptyHTML}")`); + const expected = `using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; + +[Parallelizable(ParallelScope.Self)] +public class Tests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() + { + return new BrowserNewContextOptions + { + ColorScheme = ColorScheme.Dark, + }; + } + + [Test] + public async Task MyTest() + { + // Go to ${emptyHTML} + await Page.GotoAsync("${emptyHTML}"); + + } +}`; + expect(cli.text()).toContain(expected); +}); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index cc952207eb..6e68337230 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -35,6 +35,8 @@ const codegenLang2Id: Map = new Map([ ['Python Async', 'python-async'], ['Pytest', 'pytest'], ['C#', 'csharp'], + ['C# NUnit', 'csharp-nunit'], + ['C# MSTest', 'csharp-mstest'], ['Playwright Test', 'test'], ]); const codegenLangId2lang = new Map([...codegenLang2Id.entries()].map(([lang, langId]) => [langId, lang]));