feat(testing): add create-nodes plugin for playwright e2e targets (#20099)
This commit is contained in:
parent
3981c90fe8
commit
2374d8eaba
@ -87,6 +87,7 @@ It only uses language primitives and immutable objects
|
|||||||
- [StringChange](../../devkit/documents/StringChange)
|
- [StringChange](../../devkit/documents/StringChange)
|
||||||
- [TargetDefaults](../../devkit/documents/TargetDefaults)
|
- [TargetDefaults](../../devkit/documents/TargetDefaults)
|
||||||
- [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor)
|
- [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor)
|
||||||
|
- [ToJSOptions](../../devkit/documents/ToJSOptions)
|
||||||
- [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration)
|
- [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration)
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
|
|||||||
11
docs/generated/devkit/ToJSOptions.md
Normal file
11
docs/generated/devkit/ToJSOptions.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Type alias: ToJSOptions
|
||||||
|
|
||||||
|
Ƭ **ToJSOptions**: `Object`
|
||||||
|
|
||||||
|
#### Type declaration
|
||||||
|
|
||||||
|
| Name | Type |
|
||||||
|
| :---------- | :------------------------------ |
|
||||||
|
| `extension` | `".js"` \| `".mjs"` \| `".cjs"` |
|
||||||
|
| `module?` | `ModuleKind` |
|
||||||
|
| `target?` | `ScriptTarget` |
|
||||||
@ -1,14 +1,15 @@
|
|||||||
# Function: toJS
|
# Function: toJS
|
||||||
|
|
||||||
▸ **toJS**(`tree`): `void`
|
▸ **toJS**(`tree`, `options?`): `void`
|
||||||
|
|
||||||
Rename and transpile any new typescript files created to javascript files
|
Rename and transpile any new typescript files created to javascript files
|
||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
| Name | Type |
|
| Name | Type |
|
||||||
| :----- | :------------------------------------ |
|
| :--------- | :-------------------------------------------------- |
|
||||||
| `tree` | [`Tree`](../../devkit/documents/Tree) |
|
| `tree` | [`Tree`](../../devkit/documents/Tree) |
|
||||||
|
| `options?` | [`ToJSOptions`](../../devkit/documents/ToJSOptions) |
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,7 @@ It only uses language primitives and immutable objects
|
|||||||
- [StringChange](../../devkit/documents/StringChange)
|
- [StringChange](../../devkit/documents/StringChange)
|
||||||
- [TargetDefaults](../../devkit/documents/TargetDefaults)
|
- [TargetDefaults](../../devkit/documents/TargetDefaults)
|
||||||
- [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor)
|
- [TaskGraphExecutor](../../devkit/documents/TaskGraphExecutor)
|
||||||
|
- [ToJSOptions](../../devkit/documents/ToJSOptions)
|
||||||
- [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration)
|
- [WorkspaceJsonConfiguration](../../devkit/documents/WorkspaceJsonConfiguration)
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"implementation": "/packages/playwright/src/executors/playwright/playwright.ts",
|
"implementation": "/packages/playwright/src/executors/playwright/playwright.impl.ts",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$schema": "http://json-schema.org/schema",
|
"$schema": "http://json-schema.org/schema",
|
||||||
"version": 2,
|
"version": 2,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
ensurePlaywrightBrowsersInstallation,
|
ensurePlaywrightBrowsersInstallation,
|
||||||
getPackageManagerCommand,
|
getPackageManagerCommand,
|
||||||
getSelectedPackageManager,
|
getSelectedPackageManager,
|
||||||
|
readJson,
|
||||||
} from '@nx/e2e/utils';
|
} from '@nx/e2e/utils';
|
||||||
|
|
||||||
const TEN_MINS_MS = 600_000;
|
const TEN_MINS_MS = 600_000;
|
||||||
@ -67,3 +68,97 @@ describe('Playwright E2E Test runner', () => {
|
|||||||
TEN_MINS_MS
|
TEN_MINS_MS
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Playwright E2E Test Runner - PCV3', () => {
|
||||||
|
let env: string | undefined;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
env = process.env.NX_PCV3;
|
||||||
|
newProject({
|
||||||
|
name: uniq('playwright'),
|
||||||
|
unsetProjectNameAndRootFormat: false,
|
||||||
|
});
|
||||||
|
process.env.NX_PCV3 = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (env) {
|
||||||
|
process.env.NX_PCV3 = env;
|
||||||
|
} else {
|
||||||
|
delete process.env.NX_PCV3;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
'should test and lint example app',
|
||||||
|
|
||||||
|
() => {
|
||||||
|
ensurePlaywrightBrowsersInstallation();
|
||||||
|
|
||||||
|
const pmc = getPackageManagerCommand();
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`g @nx/web:app demo-e2e --directory apps/demo-e2e --unitTestRunner=none --bundler=vite --e2eTestRunner=none --style=css --no-interactive --projectNameAndRootFormat=as-provided`
|
||||||
|
);
|
||||||
|
runCLI(
|
||||||
|
`g @nx/playwright:configuration --project demo-e2e --webServerCommand="${pmc.runNx} serve demo-e2e" --webServerAddress="http://localhost:4200"`
|
||||||
|
);
|
||||||
|
|
||||||
|
const e2eResults = runCLI(`e2e demo-e2e`);
|
||||||
|
expect(e2eResults).toContain('Successfully ran target e2e for project');
|
||||||
|
|
||||||
|
const { targets } = readJson('apps/demo-e2e/project.json');
|
||||||
|
expect(targets?.e2e).not.toBeDefined();
|
||||||
|
|
||||||
|
const { plugins } = readJson('nx.json');
|
||||||
|
const playwrightPlugin = plugins?.find(
|
||||||
|
(p) => p.plugin === '@nx/playwright/plugin'
|
||||||
|
);
|
||||||
|
expect(playwrightPlugin).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"targetName": "e2e",
|
||||||
|
},
|
||||||
|
"plugin": "@nx/playwright/plugin",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
TEN_MINS_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
'should test and lint example app with js',
|
||||||
|
() => {
|
||||||
|
ensurePlaywrightBrowsersInstallation();
|
||||||
|
|
||||||
|
const pmc = getPackageManagerCommand();
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`g @nx/web:app demo-js-e2e --directory apps/demo-js-e2e --unitTestRunner=none --bundler=vite --e2eTestRunner=none --style=css --no-interactive --projectNameAndRootFormat=as-provided`
|
||||||
|
);
|
||||||
|
runCLI(
|
||||||
|
`g @nx/playwright:configuration --project demo-js-e2e --js --webServerCommand="${pmc.runNx} serve demo-e2e" --webServerAddress="http://localhost:4200"`
|
||||||
|
);
|
||||||
|
|
||||||
|
const e2eResults = runCLI(`e2e demo-js-e2e`);
|
||||||
|
expect(e2eResults).toContain('Successfully ran target e2e for project');
|
||||||
|
|
||||||
|
const { targets } = readJson('apps/demo-js-e2e/project.json');
|
||||||
|
expect(targets?.e2e).not.toBeDefined();
|
||||||
|
|
||||||
|
const { plugins } = readJson('nx.json');
|
||||||
|
const playwrightPlugin = plugins?.find(
|
||||||
|
(p) => p.plugin === '@nx/playwright/plugin'
|
||||||
|
);
|
||||||
|
expect(playwrightPlugin).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"targetName": "e2e",
|
||||||
|
},
|
||||||
|
"plugin": "@nx/playwright/plugin",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
TEN_MINS_MS
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@ -157,7 +157,9 @@ export function getStrippedEnvironmentVariables() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.startsWith('NX_')) {
|
const allowedKeys = ['NX_PCV3'];
|
||||||
|
|
||||||
|
if (key.startsWith('NX_') && !allowedKeys.includes(key)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -303,11 +303,19 @@ function getInputs(
|
|||||||
/**
|
/**
|
||||||
* Load the module after ensuring that the require cache is cleared.
|
* Load the module after ensuring that the require cache is cleared.
|
||||||
*/
|
*/
|
||||||
|
const packageInstallationDirectories = ['node_modules', '.yarn'];
|
||||||
|
|
||||||
function load(path: string): any {
|
function load(path: string): any {
|
||||||
// Clear cache if the path is in the cache
|
// Clear cache if the path is in the cache
|
||||||
if (require.cache[path]) {
|
if (require.cache[path]) {
|
||||||
for (const k of Object.keys(require.cache)) {
|
for (const k of Object.keys(require.cache)) {
|
||||||
delete require.cache[k];
|
// We don't want to clear the require cache of installed packages.
|
||||||
|
// Clearing them can cause some issues when running Nx without the daemon
|
||||||
|
// and may cause issues for other packages that use the module state
|
||||||
|
// in some to store cached information.
|
||||||
|
if (!packageInstallationDirectories.some((dir) => k.includes(dir))) {
|
||||||
|
delete require.cache[k];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export { generateFiles } from './src/generators/generate-files';
|
|||||||
/**
|
/**
|
||||||
* @category Generators
|
* @category Generators
|
||||||
*/
|
*/
|
||||||
export { toJS } from './src/generators/to-js';
|
export { toJS, ToJSOptions } from './src/generators/to-js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @category Generators
|
* @category Generators
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
import type { Tree } from 'nx/src/generators/tree';
|
import type { Tree } from 'nx/src/generators/tree';
|
||||||
|
import type { ScriptTarget, ModuleKind } from 'typescript';
|
||||||
import { typescriptVersion } from '../utils/versions';
|
import { typescriptVersion } from '../utils/versions';
|
||||||
import { ensurePackage } from '../utils/package-json';
|
import { ensurePackage } from '../utils/package-json';
|
||||||
|
|
||||||
|
export type ToJSOptions = {
|
||||||
|
target?: ScriptTarget;
|
||||||
|
module?: ModuleKind;
|
||||||
|
extension: '.js' | '.mjs' | '.cjs';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rename and transpile any new typescript files created to javascript files
|
* Rename and transpile any new typescript files created to javascript files
|
||||||
*/
|
*/
|
||||||
export function toJS(tree: Tree): void {
|
export function toJS(tree: Tree, options?: ToJSOptions): void {
|
||||||
const { JsxEmit, ScriptTarget, transpile } = ensurePackage(
|
const { JsxEmit, ScriptTarget, transpile, ModuleKind } = ensurePackage(
|
||||||
'typescript',
|
'typescript',
|
||||||
typescriptVersion
|
typescriptVersion
|
||||||
);
|
) as typeof import('typescript');
|
||||||
|
|
||||||
for (const c of tree.listChanges()) {
|
for (const c of tree.listChanges()) {
|
||||||
if (
|
if (
|
||||||
@ -21,10 +28,14 @@ export function toJS(tree: Tree): void {
|
|||||||
transpile(c.content.toString('utf-8'), {
|
transpile(c.content.toString('utf-8'), {
|
||||||
allowJs: true,
|
allowJs: true,
|
||||||
jsx: JsxEmit.Preserve,
|
jsx: JsxEmit.Preserve,
|
||||||
target: ScriptTarget.ESNext,
|
target: options?.target ?? ScriptTarget.ESNext,
|
||||||
|
module: options?.module ?? ModuleKind.ESNext,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
tree.rename(c.path, c.path.replace(/\.tsx?$/, '.js'));
|
tree.rename(
|
||||||
|
c.path,
|
||||||
|
c.path.replace(/\.tsx?$/, options?.extension ?? '.js')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/nx/src/native/index.d.ts
vendored
1
packages/nx/src/native/index.d.ts
vendored
@ -176,4 +176,5 @@ export class WorkspaceContext {
|
|||||||
incrementalUpdate(updatedFiles: Array<string>, deletedFiles: Array<string>): Record<string, string>
|
incrementalUpdate(updatedFiles: Array<string>, deletedFiles: Array<string>): Record<string, string>
|
||||||
updateProjectFiles(projectRootMappings: ProjectRootMappings, projectFiles: ExternalObject<ProjectFiles>, globalFiles: ExternalObject<Array<FileData>>, updatedFiles: Record<string, string>, deletedFiles: Array<string>): UpdatedWorkspaceFiles
|
updateProjectFiles(projectRootMappings: ProjectRootMappings, projectFiles: ExternalObject<ProjectFiles>, globalFiles: ExternalObject<Array<FileData>>, updatedFiles: Record<string, string>, deletedFiles: Array<string>): UpdatedWorkspaceFiles
|
||||||
allFileData(): Array<FileData>
|
allFileData(): Array<FileData>
|
||||||
|
getFilesInDirectory(directory: string): Array<string>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::native::utils::normalize_trait::Normalize;
|
use crate::native::{utils::normalize_trait::Normalize, types::FileData};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
impl Normalize for Path {
|
impl Normalize for Path {
|
||||||
@ -28,3 +28,50 @@ where
|
|||||||
path.as_ref().display().to_string()
|
path.as_ref().display().to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_child_files<P: AsRef<Path>>(directory: P, files: Vec<FileData>) -> Vec<String> {
|
||||||
|
files
|
||||||
|
.into_iter()
|
||||||
|
.filter(|file_data| Path::new(&file_data.file).starts_with(directory.as_ref()))
|
||||||
|
.map(|file_data| file_data.file)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_get_child_files() {
|
||||||
|
let directory = PathBuf::from("foo");
|
||||||
|
let files = vec![
|
||||||
|
FileData {
|
||||||
|
file: "foo/bar".into(),
|
||||||
|
hash: "123".into(),
|
||||||
|
},
|
||||||
|
FileData {
|
||||||
|
file: "foo/baz".into(),
|
||||||
|
hash: "123".into(),
|
||||||
|
},
|
||||||
|
FileData {
|
||||||
|
file: "foo/child/bar".into(),
|
||||||
|
hash: "123".into(),
|
||||||
|
},
|
||||||
|
FileData {
|
||||||
|
file: "bar/baz".into(),
|
||||||
|
hash: "123".into(),
|
||||||
|
},
|
||||||
|
FileData {
|
||||||
|
file: "foo-other/not-child".into(),
|
||||||
|
hash: "123".into(),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let child_files = get_child_files(&directory, files);
|
||||||
|
assert_eq!(child_files, [
|
||||||
|
"foo/bar",
|
||||||
|
"foo/baz",
|
||||||
|
"foo/child/bar",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ use napi::bindgen_prelude::External;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::native::hasher::hash;
|
use crate::native::hasher::hash;
|
||||||
use crate::native::utils::Normalize;
|
use crate::native::utils::{Normalize, path::get_child_files};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -299,4 +299,10 @@ impl WorkspaceContext {
|
|||||||
pub fn all_file_data(&self) -> Vec<FileData> {
|
pub fn all_file_data(&self) -> Vec<FileData> {
|
||||||
self.files_worker.get_files()
|
self.files_worker.get_files()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_files_in_directory(&self, directory: String) -> Vec<String> {
|
||||||
|
get_child_files(&directory, self.files_worker
|
||||||
|
.get_files())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,14 @@ export function getAllFileDataInContext(workspaceRoot: string) {
|
|||||||
return workspaceContext.allFileData();
|
return workspaceContext.allFileData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFilesInDirectoryUsingContext(
|
||||||
|
workspaceRoot: string,
|
||||||
|
dir: string
|
||||||
|
) {
|
||||||
|
ensureContextAvailable(workspaceRoot);
|
||||||
|
return workspaceContext.getFilesInDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateProjectFiles(
|
export function updateProjectFiles(
|
||||||
projectRootMappings: Record<string, string>,
|
projectRootMappings: Record<string, string>,
|
||||||
rustReferences: NxWorkspaceFilesExternals,
|
rustReferences: NxWorkspaceFilesExternals,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"executors": {
|
"executors": {
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"implementation": "./src/executors/playwright/playwright",
|
"implementation": "./src/executors/playwright/playwright.impl",
|
||||||
"schema": "./src/executors/playwright/schema.json",
|
"schema": "./src/executors/playwright/schema.json",
|
||||||
"description": "Run Playwright tests."
|
"description": "Run Playwright tests."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
playwrightExecutor,
|
playwrightExecutor,
|
||||||
PlaywrightExecutorSchema,
|
PlaywrightExecutorSchema,
|
||||||
} from './src/executors/playwright/playwright';
|
} from './src/executors/playwright/playwright.impl';
|
||||||
export { initGenerator } from './src/generators/init/init';
|
export { initGenerator } from './src/generators/init/init';
|
||||||
export { configurationGenerator } from './src/generators/configuration/configuration';
|
export { configurationGenerator } from './src/generators/configuration/configuration';
|
||||||
|
|||||||
3
packages/playwright/migrations.json
Normal file
3
packages/playwright/migrations.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"generators": {}
|
||||||
|
}
|
||||||
@ -34,7 +34,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nx/devkit": "file:../devkit",
|
"@nx/devkit": "file:../devkit",
|
||||||
"@nx/eslint": "file:../eslint",
|
"@nx/eslint": "file:../eslint",
|
||||||
"tslib": "^2.3.0"
|
"@nx/js": "file:../js",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"minimatch": "3.0.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@playwright/test": "^1.36.0"
|
"@playwright/test": "^1.36.0"
|
||||||
@ -54,6 +56,10 @@
|
|||||||
"./generators/*/schema.json": "./src/generators/*/schema.json",
|
"./generators/*/schema.json": "./src/generators/*/schema.json",
|
||||||
"./executors.json": "./executors.json",
|
"./executors.json": "./executors.json",
|
||||||
"./executors/*/schema.json": "./src/executors/*/schema.json",
|
"./executors/*/schema.json": "./src/executors/*/schema.json",
|
||||||
|
"./plugin": "./plugin.js",
|
||||||
"./preset": "./src/utils/preset.js"
|
"./preset": "./src/utils/preset.js"
|
||||||
|
},
|
||||||
|
"nx-migrations": {
|
||||||
|
"migrations": "./migrations.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/playwright/plugin.ts
Normal file
1
packages/playwright/plugin.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { createNodes, PlaywrightPluginOptions } from './src/plugins/plugin';
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ensurePackage,
|
||||||
formatFiles,
|
formatFiles,
|
||||||
generateFiles,
|
generateFiles,
|
||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
@ -15,6 +16,7 @@ import * as path from 'path';
|
|||||||
import { ConfigurationGeneratorSchema } from './schema';
|
import { ConfigurationGeneratorSchema } from './schema';
|
||||||
import initGenerator from '../init/init';
|
import initGenerator from '../init/init';
|
||||||
import { addLinterToPlaywrightProject } from '../../utils/add-linter';
|
import { addLinterToPlaywrightProject } from '../../utils/add-linter';
|
||||||
|
import { typescriptVersion } from '@nx/js/src/utils/versions';
|
||||||
|
|
||||||
export async function configurationGenerator(
|
export async function configurationGenerator(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
@ -36,8 +38,17 @@ export async function configurationGenerator(
|
|||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
addE2eTarget(tree, options);
|
const hasPlugin = readNxJson(tree).plugins?.some((p) =>
|
||||||
setupE2ETargetDefaults(tree);
|
typeof p === 'string'
|
||||||
|
? p === '@nx/playwright/plugin'
|
||||||
|
: p.plugin === '@nx/playwright/plugin'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasPlugin) {
|
||||||
|
addE2eTarget(tree, options);
|
||||||
|
setupE2ETargetDefaults(tree);
|
||||||
|
}
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
await addLinterToPlaywrightProject(tree, {
|
await addLinterToPlaywrightProject(tree, {
|
||||||
project: options.project,
|
project: options.project,
|
||||||
@ -51,7 +62,11 @@ export async function configurationGenerator(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (options.js) {
|
if (options.js) {
|
||||||
toJS(tree);
|
const { ModuleKind } = ensurePackage(
|
||||||
|
'typescript',
|
||||||
|
typescriptVersion
|
||||||
|
) as typeof import('typescript');
|
||||||
|
toJS(tree, { extension: '.cjs', module: ModuleKind.CommonJS });
|
||||||
}
|
}
|
||||||
if (!options.skipFormat) {
|
if (!options.skipFormat) {
|
||||||
await formatFiles(tree);
|
await formatFiles(tree);
|
||||||
@ -93,7 +108,7 @@ Rename or remove the existing e2e target.`);
|
|||||||
outputs: [`{workspaceRoot}/dist/.playwright/${projectConfig.root}`],
|
outputs: [`{workspaceRoot}/dist/.playwright/${projectConfig.root}`],
|
||||||
options: {
|
options: {
|
||||||
config: `${projectConfig.root}/playwright.config.${
|
config: `${projectConfig.root}/playwright.config.${
|
||||||
options.js ? 'js' : 'ts'
|
options.js ? 'cjs' : 'ts'
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
106
packages/playwright/src/generators/init/init.spec.ts
Normal file
106
packages/playwright/src/generators/init/init.spec.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { Tree, readNxJson, updateNxJson } from '@nx/devkit';
|
||||||
|
import { withEnvironmentVariables } from '@nx/devkit/internal-testing-utils';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
|
||||||
|
import { initGenerator } from './init';
|
||||||
|
|
||||||
|
describe('@nx/playwright:init', () => {
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add the plugin if PCV3 is set', async () => {
|
||||||
|
await withEnvironmentVariables(
|
||||||
|
{
|
||||||
|
NX_PCV3: 'true',
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await initGenerator(tree, {
|
||||||
|
skipFormat: true,
|
||||||
|
skipPackageJson: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
expect(nxJson.plugins).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"targetName": "e2e",
|
||||||
|
},
|
||||||
|
"plugin": "@nx/playwright/plugin",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not overwrite existing plugins', async () => {
|
||||||
|
updateNxJson(tree, {
|
||||||
|
plugins: ['foo'],
|
||||||
|
});
|
||||||
|
await withEnvironmentVariables(
|
||||||
|
{
|
||||||
|
NX_PCV3: 'true',
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await initGenerator(tree, {
|
||||||
|
skipFormat: true,
|
||||||
|
skipPackageJson: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
expect(nxJson.plugins).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
"foo",
|
||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"targetName": "e2e",
|
||||||
|
},
|
||||||
|
"plugin": "@nx/playwright/plugin",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add plugin if already in array', async () => {
|
||||||
|
updateNxJson(tree, {
|
||||||
|
plugins: ['@nx/playwright/plugin'],
|
||||||
|
});
|
||||||
|
await withEnvironmentVariables(
|
||||||
|
{
|
||||||
|
NX_PCV3: 'true',
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await initGenerator(tree, {
|
||||||
|
skipFormat: true,
|
||||||
|
skipPackageJson: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
expect(nxJson.plugins).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
"@nx/playwright/plugin",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add plugin if environment variable is not set', async () => {
|
||||||
|
await withEnvironmentVariables(
|
||||||
|
{
|
||||||
|
NX_PCV3: undefined,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
await initGenerator(tree, {
|
||||||
|
skipFormat: true,
|
||||||
|
skipPackageJson: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
expect(nxJson.plugins).toMatchInlineSnapshot(`undefined`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -4,9 +4,11 @@ import {
|
|||||||
GeneratorCallback,
|
GeneratorCallback,
|
||||||
getPackageManagerCommand,
|
getPackageManagerCommand,
|
||||||
output,
|
output,
|
||||||
|
readNxJson,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
updateJson,
|
updateJson,
|
||||||
|
updateNxJson,
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { InitGeneratorSchema } from './schema';
|
import { InitGeneratorSchema } from './schema';
|
||||||
@ -56,6 +58,10 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.NX_PCV3 === 'true') {
|
||||||
|
addPlugin(tree);
|
||||||
|
}
|
||||||
|
|
||||||
if (!options.skipInstall) {
|
if (!options.skipInstall) {
|
||||||
tasks.push(() => {
|
tasks.push(() => {
|
||||||
output.log({
|
output.log({
|
||||||
@ -70,4 +76,25 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) {
|
|||||||
return runTasksInSerial(...tasks);
|
return runTasksInSerial(...tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPlugin(tree: Tree) {
|
||||||
|
const nxJson = readNxJson(tree);
|
||||||
|
nxJson.plugins ??= [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!nxJson.plugins.some((p) =>
|
||||||
|
typeof p === 'string'
|
||||||
|
? p === '@nx/playwright/plugin'
|
||||||
|
: p.plugin === '@nx/playwright/plugin'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
nxJson.plugins.push({
|
||||||
|
plugin: '@nx/playwright/plugin',
|
||||||
|
options: {
|
||||||
|
targetName: 'e2e',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updateNxJson(tree, nxJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default initGenerator;
|
export default initGenerator;
|
||||||
|
|||||||
245
packages/playwright/src/plugins/plugin.spec.ts
Normal file
245
packages/playwright/src/plugins/plugin.spec.ts
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import { CreateNodesContext } from '@nx/devkit';
|
||||||
|
import { TempFs } from '@nx/devkit/internal-testing-utils';
|
||||||
|
|
||||||
|
import { createNodes } from './plugin';
|
||||||
|
import { PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
// Jest can't handle the dynamic import, and mocking it doesn't work either.
|
||||||
|
// we overwrite the dynamic import function to use the regular syntax, which
|
||||||
|
// jest does handle.
|
||||||
|
import * as lcf from '../utils/load-config-file';
|
||||||
|
(lcf as any).dynamicImport = (m) => import(m);
|
||||||
|
|
||||||
|
describe('@nx/playwright/plugin', () => {
|
||||||
|
let createNodesFunction = createNodes[1];
|
||||||
|
let context: CreateNodesContext;
|
||||||
|
let tempFs: TempFs;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempFs = new TempFs('playwright-plugin');
|
||||||
|
await tempFs.createFiles({
|
||||||
|
'package.json': '{}',
|
||||||
|
'playwright.config.js': 'module.exports = {}',
|
||||||
|
});
|
||||||
|
|
||||||
|
context = {
|
||||||
|
nxJsonConfiguration: {
|
||||||
|
namedInputs: {
|
||||||
|
default: ['{projectRoot}/**/*'],
|
||||||
|
production: ['!{projectRoot}/**/*.spec.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspaceRoot: tempFs.tempDir,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
tempFs.cleanup();
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create nodes with default playwright configuration', async () => {
|
||||||
|
await mockPlaywrightConfig(tempFs, {});
|
||||||
|
const { projects } = await createNodesFunction(
|
||||||
|
'playwright.config.js',
|
||||||
|
{
|
||||||
|
targetName: 'e2e',
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(projects).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
".": {
|
||||||
|
"root": ".",
|
||||||
|
"targets": {
|
||||||
|
"e2e": {
|
||||||
|
"cache": true,
|
||||||
|
"command": "playwright test",
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production",
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "{projectRoot}",
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/test-results",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"e2e-ci": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [],
|
||||||
|
"executor": "nx:noop",
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production",
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/test-results",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create nodes with reporters configured', async () => {
|
||||||
|
await mockPlaywrightConfig(tempFs, {
|
||||||
|
reporter: [
|
||||||
|
['list'],
|
||||||
|
['json', { outputFile: 'test-results/report.json' }],
|
||||||
|
['html', { outputFolder: 'test-results/html' }],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const { projects } = await createNodesFunction(
|
||||||
|
'playwright.config.js',
|
||||||
|
{
|
||||||
|
targetName: 'e2e',
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(projects).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
".": {
|
||||||
|
"root": ".",
|
||||||
|
"targets": {
|
||||||
|
"e2e": {
|
||||||
|
"cache": true,
|
||||||
|
"command": "playwright test",
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production",
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "{projectRoot}",
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/playwright-report",
|
||||||
|
"{projectRoot}/test-results/report.json",
|
||||||
|
"{projectRoot}/test-results/html",
|
||||||
|
"{projectRoot}/test-results",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"e2e-ci": {
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [],
|
||||||
|
"executor": "nx:noop",
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production",
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/playwright-report",
|
||||||
|
"{projectRoot}/test-results/report.json",
|
||||||
|
"{projectRoot}/test-results/html",
|
||||||
|
"{projectRoot}/test-results",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create nodes for distributed CI', async () => {
|
||||||
|
await mockPlaywrightConfig(
|
||||||
|
tempFs,
|
||||||
|
`module.exports = {
|
||||||
|
testDir: 'tests',
|
||||||
|
testIgnore: [/.*skip.*/, '**/ignored/**'],
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
await tempFs.createFiles({
|
||||||
|
'tests/run-me.spec.ts': '',
|
||||||
|
'tests/run-me-2.spec.ts': '',
|
||||||
|
'tests/skip-me.spec.ts': '',
|
||||||
|
'tests/ignored/run-me.spec.ts': '',
|
||||||
|
'not-tests/run-me.spec.ts': '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { projects } = await createNodesFunction(
|
||||||
|
'playwright.config.js',
|
||||||
|
{
|
||||||
|
targetName: 'e2e',
|
||||||
|
ciTargetName: 'e2e-ci',
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
const { targets } = projects['.'];
|
||||||
|
expect(targets['e2e-ci']).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"cache": true,
|
||||||
|
"dependsOn": [
|
||||||
|
{
|
||||||
|
"params": "forward",
|
||||||
|
"projects": "self",
|
||||||
|
"target": "e2e-ci--tests/run-me-2.spec.ts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"params": "forward",
|
||||||
|
"projects": "self",
|
||||||
|
"target": "e2e-ci--tests/run-me.spec.ts",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"executor": "nx:noop",
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production",
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/test-results",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(targets['e2e-ci--tests/run-me.spec.ts']).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"cache": true,
|
||||||
|
"command": "playwright test tests/run-me.spec.ts",
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production",
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "{projectRoot}",
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/test-results",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(targets['e2e-ci--tests/run-me-2.spec.ts']).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"cache": true,
|
||||||
|
"command": "playwright test tests/run-me-2.spec.ts",
|
||||||
|
"inputs": [
|
||||||
|
"default",
|
||||||
|
"^production",
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "{projectRoot}",
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
"{projectRoot}/test-results",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(targets['e2e-ci--tests/skip-me.spec.ts']).not.toBeDefined();
|
||||||
|
expect(targets['e2e-ci--tests/ignored/run-me.spec.ts']).not.toBeDefined();
|
||||||
|
expect(targets['e2e-ci--not-tests/run-me.spec.ts']).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function mockPlaywrightConfig(
|
||||||
|
tempFs: TempFs,
|
||||||
|
config: PlaywrightTestConfig | string
|
||||||
|
) {
|
||||||
|
await tempFs.writeFile(
|
||||||
|
'playwright.config.js',
|
||||||
|
typeof config === 'string'
|
||||||
|
? config
|
||||||
|
: `module.exports = ${JSON.stringify(config)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
284
packages/playwright/src/plugins/plugin.ts
Normal file
284
packages/playwright/src/plugins/plugin.ts
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import { existsSync, readdirSync } from 'fs';
|
||||||
|
import { dirname, join, relative } from 'path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CreateDependencies,
|
||||||
|
CreateNodes,
|
||||||
|
CreateNodesContext,
|
||||||
|
detectPackageManager,
|
||||||
|
joinPathFragments,
|
||||||
|
readJsonFile,
|
||||||
|
TargetConfiguration,
|
||||||
|
writeJsonFile,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
|
||||||
|
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||||
|
|
||||||
|
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
import { getFilesInDirectoryUsingContext } from 'nx/src/utils/workspace-context';
|
||||||
|
import minimatch = require('minimatch');
|
||||||
|
import { loadPlaywrightConfig } from '../utils/load-config-file';
|
||||||
|
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
||||||
|
import { getLockFileName } from '@nx/js';
|
||||||
|
|
||||||
|
export interface PlaywrightPluginOptions {
|
||||||
|
targetName?: string;
|
||||||
|
ciTargetName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NormalizedOptions {
|
||||||
|
targetName: string;
|
||||||
|
ciTargetName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachePath = join(projectGraphCacheDirectory, 'playwright.hash');
|
||||||
|
|
||||||
|
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
|
||||||
|
|
||||||
|
const calculatedTargets: Record<
|
||||||
|
string,
|
||||||
|
Record<string, TargetConfiguration>
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
function readTargetsCache(): Record<
|
||||||
|
string,
|
||||||
|
Record<string, TargetConfiguration>
|
||||||
|
> {
|
||||||
|
return readJsonFile(cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeTargetsToCache(
|
||||||
|
targets: Record<string, Record<string, TargetConfiguration>>
|
||||||
|
) {
|
||||||
|
writeJsonFile(cachePath, targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDependencies: CreateDependencies = () => {
|
||||||
|
writeTargetsToCache(calculatedTargets);
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNodes: CreateNodes<PlaywrightPluginOptions> = [
|
||||||
|
'**/playwright.config.{js,ts,cjs,cts,mjs,mts}',
|
||||||
|
async (configFilePath, options, context) => {
|
||||||
|
const projectRoot = dirname(configFilePath);
|
||||||
|
|
||||||
|
// Do not create a project if package.json and project.json isn't there.
|
||||||
|
const siblingFiles = readdirSync(projectRoot);
|
||||||
|
if (
|
||||||
|
!siblingFiles.includes('package.json') &&
|
||||||
|
!siblingFiles.includes('project.json')
|
||||||
|
) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedOptions = normalizeOptions(options);
|
||||||
|
|
||||||
|
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
|
||||||
|
getLockFileName(detectPackageManager(context.workspaceRoot)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const targets =
|
||||||
|
targetsCache[hash] ??
|
||||||
|
(await buildPlaywrightTargets(
|
||||||
|
configFilePath,
|
||||||
|
projectRoot,
|
||||||
|
normalizedOptions,
|
||||||
|
context
|
||||||
|
));
|
||||||
|
|
||||||
|
calculatedTargets[hash] = targets;
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects: {
|
||||||
|
[projectRoot]: {
|
||||||
|
root: projectRoot,
|
||||||
|
targets,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function buildPlaywrightTargets(
|
||||||
|
configFilePath: string,
|
||||||
|
projectRoot: string,
|
||||||
|
options: NormalizedOptions,
|
||||||
|
context: CreateNodesContext
|
||||||
|
) {
|
||||||
|
const playwrightConfig: PlaywrightTestConfig = await loadPlaywrightConfig(
|
||||||
|
join(context.workspaceRoot, configFilePath)
|
||||||
|
);
|
||||||
|
|
||||||
|
const namedInputs = getNamedInputs(projectRoot, context);
|
||||||
|
|
||||||
|
const targets: Record<string, TargetConfiguration<unknown>> = {};
|
||||||
|
|
||||||
|
const baseTargetConfig: TargetConfiguration = {
|
||||||
|
command: 'playwright test',
|
||||||
|
options: {
|
||||||
|
cwd: '{projectRoot}',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
targets[options.targetName] = {
|
||||||
|
...baseTargetConfig,
|
||||||
|
cache: true,
|
||||||
|
inputs:
|
||||||
|
'production' in namedInputs
|
||||||
|
? ['default', '^production']
|
||||||
|
: ['default', '^default'],
|
||||||
|
outputs: getOutputs(projectRoot, playwrightConfig),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.ciTargetName) {
|
||||||
|
const ciBaseTargetConfig: TargetConfiguration = {
|
||||||
|
...baseTargetConfig,
|
||||||
|
cache: true,
|
||||||
|
inputs:
|
||||||
|
'production' in namedInputs
|
||||||
|
? ['default', '^production']
|
||||||
|
: ['default', '^default'],
|
||||||
|
outputs: getOutputs(projectRoot, playwrightConfig),
|
||||||
|
};
|
||||||
|
|
||||||
|
const testDir = playwrightConfig.testDir
|
||||||
|
? joinPathFragments(projectRoot, playwrightConfig.testDir)
|
||||||
|
: projectRoot;
|
||||||
|
|
||||||
|
// Playwright defaults to the following pattern.
|
||||||
|
playwrightConfig.testMatch ??= '**/*.@(spec|test).?(c|m)[jt]s?(x)';
|
||||||
|
|
||||||
|
const dependsOn: TargetConfiguration['dependsOn'] = [];
|
||||||
|
forEachTestFile(
|
||||||
|
(testFile) => {
|
||||||
|
const relativeToProjectRoot = relative(projectRoot, testFile);
|
||||||
|
const targetName = `${options.ciTargetName}--${relativeToProjectRoot}`;
|
||||||
|
targets[targetName] = {
|
||||||
|
...ciBaseTargetConfig,
|
||||||
|
command: `${baseTargetConfig.command} ${relativeToProjectRoot}`,
|
||||||
|
};
|
||||||
|
dependsOn.push({
|
||||||
|
target: targetName,
|
||||||
|
projects: 'self',
|
||||||
|
params: 'forward',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
context,
|
||||||
|
path: testDir,
|
||||||
|
config: playwrightConfig,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
targets[options.ciTargetName] ??= {};
|
||||||
|
|
||||||
|
targets[options.ciTargetName] = {
|
||||||
|
executor: 'nx:noop',
|
||||||
|
cache: ciBaseTargetConfig.cache,
|
||||||
|
inputs: ciBaseTargetConfig.inputs,
|
||||||
|
outputs: ciBaseTargetConfig.outputs,
|
||||||
|
dependsOn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forEachTestFile(
|
||||||
|
cb: (path: string) => void,
|
||||||
|
opts: {
|
||||||
|
context: CreateNodesContext;
|
||||||
|
path: string;
|
||||||
|
config: PlaywrightTestConfig;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const files = getFilesInDirectoryUsingContext(
|
||||||
|
opts.context.workspaceRoot,
|
||||||
|
opts.path
|
||||||
|
);
|
||||||
|
const matcher = createMatcher(opts.config.testMatch);
|
||||||
|
const ignoredMatcher = opts.config.testIgnore
|
||||||
|
? createMatcher(opts.config.testIgnore)
|
||||||
|
: () => false;
|
||||||
|
for (const file of files) {
|
||||||
|
if (matcher(file) && !ignoredMatcher(file)) {
|
||||||
|
cb(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMatcher(pattern: string | RegExp | Array<string | RegExp>) {
|
||||||
|
if (Array.isArray(pattern)) {
|
||||||
|
const matchers = pattern.map((p) => createMatcher(p));
|
||||||
|
return (path: string) => matchers.some((m) => m(path));
|
||||||
|
} else if (pattern instanceof RegExp) {
|
||||||
|
return (path: string) => pattern.test(path);
|
||||||
|
} else {
|
||||||
|
return (path: string) => {
|
||||||
|
try {
|
||||||
|
return minimatch(path, pattern);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Error matching ${path} with ${pattern}: ${e.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOutputs(
|
||||||
|
projectRoot: string,
|
||||||
|
playwrightConfig: PlaywrightTestConfig
|
||||||
|
): string[] {
|
||||||
|
function getOutput(path: string): string {
|
||||||
|
if (path.startsWith('..')) {
|
||||||
|
return join('{workspaceRoot}', join(projectRoot, path));
|
||||||
|
} else {
|
||||||
|
return join('{projectRoot}', path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputs = [];
|
||||||
|
|
||||||
|
const { reporter, outputDir } = playwrightConfig;
|
||||||
|
|
||||||
|
if (reporter) {
|
||||||
|
const DEFAULT_REPORTER_OUTPUT = getOutput('playwright-report');
|
||||||
|
if (reporter === 'html' || reporter === 'json') {
|
||||||
|
// Reporter is a string, so it uses the default output directory.
|
||||||
|
outputs.push(DEFAULT_REPORTER_OUTPUT);
|
||||||
|
} else if (Array.isArray(reporter)) {
|
||||||
|
for (const r of reporter) {
|
||||||
|
const [, opts] = r;
|
||||||
|
// There are a few different ways to specify an output file or directory
|
||||||
|
// depending on the reporter. This is a best effort to find the output.
|
||||||
|
if (!opts) {
|
||||||
|
outputs.push(DEFAULT_REPORTER_OUTPUT);
|
||||||
|
} else if (opts.outputFile) {
|
||||||
|
outputs.push(getOutput(opts.outputFile));
|
||||||
|
} else if (opts.outputDir) {
|
||||||
|
outputs.push(getOutput(opts.outputDir));
|
||||||
|
} else if (opts.outputFolder) {
|
||||||
|
outputs.push(getOutput(opts.outputFolder));
|
||||||
|
} else {
|
||||||
|
outputs.push(DEFAULT_REPORTER_OUTPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputDir) {
|
||||||
|
outputs.push(getOutput(outputDir));
|
||||||
|
} else {
|
||||||
|
outputs.push(getOutput('./test-results'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptions(options: PlaywrightPluginOptions): NormalizedOptions {
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
targetName: options.targetName ?? 'e2e',
|
||||||
|
ciTargetName: options.ciTargetName ?? 'e2e-ci',
|
||||||
|
};
|
||||||
|
}
|
||||||
56
packages/playwright/src/utils/load-config-file.ts
Normal file
56
packages/playwright/src/utils/load-config-file.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { extname } from 'path';
|
||||||
|
import { getRootTsConfigPath } from '@nx/js';
|
||||||
|
import { registerTsProject } from '@nx/js/src/internal';
|
||||||
|
|
||||||
|
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export let dynamicImport = new Function(
|
||||||
|
'modulePath',
|
||||||
|
'return import(modulePath);'
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function loadPlaywrightConfig(
|
||||||
|
configFilePath
|
||||||
|
): Promise<PlaywrightTestConfig> {
|
||||||
|
{
|
||||||
|
let module: any;
|
||||||
|
if (extname(configFilePath) === '.ts') {
|
||||||
|
const tsConfigPath = getRootTsConfigPath();
|
||||||
|
|
||||||
|
if (tsConfigPath) {
|
||||||
|
const unregisterTsProject = registerTsProject(tsConfigPath);
|
||||||
|
try {
|
||||||
|
// Require's cache doesn't notice when the file is updated, and
|
||||||
|
// this function is ran during daemon operation. If the config file
|
||||||
|
// is updated, we need to read its new contents, so we need to clear the cache.
|
||||||
|
// We can't just delete the cache entry for the config file, because
|
||||||
|
// it might have imports that need to be updated as well.
|
||||||
|
clearRequireCache();
|
||||||
|
// ts-node doesn't support dynamic import, so we need to use require
|
||||||
|
module = require(configFilePath);
|
||||||
|
} finally {
|
||||||
|
unregisterTsProject();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
module = await dynamicImport(configFilePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
module = await dynamicImport(configFilePath);
|
||||||
|
}
|
||||||
|
return module.default ?? module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageInstallationDirectories = ['node_modules', '.yarn'];
|
||||||
|
|
||||||
|
function clearRequireCache() {
|
||||||
|
Object.keys(require.cache).forEach((key: string) => {
|
||||||
|
// We don't want to clear the require cache of installed packages.
|
||||||
|
// Clearing them can cause some issues when running Nx without the daemon
|
||||||
|
// and may cause issues for other packages that use the module state
|
||||||
|
// in some to store cached information.
|
||||||
|
if (!packageInstallationDirectories.some((dir) => key.includes(dir))) {
|
||||||
|
delete require.cache[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user