-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(testing): add migration for playwright to use serve-static or pre…
…view for command
- Loading branch information
Showing
3 changed files
with
382 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
234 changes: 234 additions & 0 deletions
234
...ages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; | ||
import { ProjectGraph, type Tree } from '@nx/devkit'; | ||
import useServeStaticPreviewForCommand from './use-serve-static-preview-for-command'; | ||
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; | ||
|
||
let projectGraph: ProjectGraph; | ||
jest.mock('@nx/devkit', () => ({ | ||
...jest.requireActual<any>('@nx/devkit'), | ||
createProjectGraphAsync: jest.fn().mockImplementation(async () => { | ||
return projectGraph; | ||
}), | ||
})); | ||
|
||
describe('useServeStaticPreviewForCommand', () => { | ||
let tree: Tree; | ||
let tempFs: TempFs; | ||
|
||
beforeEach(() => { | ||
tree = createTreeWithEmptyWorkspace(); | ||
tempFs = new TempFs('add-e2e-ci'); | ||
tree.root = tempFs.tempDir; | ||
projectGraph = { | ||
nodes: {}, | ||
dependencies: {}, | ||
externalNodes: {}, | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
tempFs.reset(); | ||
}); | ||
|
||
it('should update when it does not use serve-static for non-vite', async () => { | ||
// ARRANGE | ||
addProject(tree, tempFs, { noVite: true }); | ||
|
||
// ACT | ||
await useServeStaticPreviewForCommand(tree); | ||
|
||
// ASSERT | ||
expect(tree.read('app-e2e/playwright.config.ts', 'utf-8')) | ||
.toMatchInlineSnapshot(` | ||
"import { defineConfig, devices } from '@playwright/test'; | ||
import { nxE2EPreset } from '@nx/playwright/preset'; | ||
import { workspaceRoot } from '@nx/devkit'; | ||
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; | ||
export default defineConfig({ | ||
...nxE2EPreset(__filename, { testDir: './src' }), | ||
use: { | ||
baseURL, | ||
trace: 'on-first-retry', | ||
}, | ||
webServer: { | ||
command: 'npx nx run app:serve-static', | ||
url: 'http://localhost:4200', | ||
reuseExistingServer: !process.env.CI, | ||
cwd: workspaceRoot, | ||
}, | ||
projects: [ | ||
{ | ||
name: 'chromium', | ||
use: { ...devices['Desktop Chrome'] }, | ||
}, | ||
], | ||
}); | ||
" | ||
`); | ||
}); | ||
it('should update when it does not use preview for vite', async () => { | ||
// ARRANGE | ||
addProject(tree, tempFs); | ||
|
||
// ACT | ||
await useServeStaticPreviewForCommand(tree); | ||
|
||
// ASSERT | ||
expect(tree.read('app-e2e/playwright.config.ts', 'utf-8')) | ||
.toMatchInlineSnapshot(` | ||
"import { defineConfig, devices } from '@playwright/test'; | ||
import { nxE2EPreset } from '@nx/playwright/preset'; | ||
import { workspaceRoot } from '@nx/devkit'; | ||
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300'; | ||
export default defineConfig({ | ||
...nxE2EPreset(__filename, { testDir: './src' }), | ||
use: { | ||
baseURL, | ||
trace: 'on-first-retry', | ||
}, | ||
webServer: { | ||
command: 'npx nx run app:preview', | ||
url: 'http://localhost:4300', | ||
reuseExistingServer: !process.env.CI, | ||
cwd: workspaceRoot, | ||
}, | ||
projects: [ | ||
{ | ||
name: 'chromium', | ||
use: { ...devices['Desktop Chrome'] }, | ||
}, | ||
], | ||
}); | ||
" | ||
`); | ||
}); | ||
}); | ||
|
||
const basePlaywrightConfig = ( | ||
appName: string | ||
) => `import { defineConfig, devices } from '@playwright/test'; | ||
import { nxE2EPreset } from '@nx/playwright/preset'; | ||
import { workspaceRoot } from '@nx/devkit'; | ||
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; | ||
export default defineConfig({ | ||
...nxE2EPreset(__filename, { testDir: './src' }), | ||
use: { | ||
baseURL, | ||
trace: 'on-first-retry', | ||
}, | ||
webServer: { | ||
command: 'npx nx run ${appName}:serve', | ||
url: 'http://localhost:4200', | ||
reuseExistingServer: !process.env.CI, | ||
cwd: workspaceRoot, | ||
}, | ||
projects: [ | ||
{ | ||
name: 'chromium', | ||
use: { ...devices['Desktop Chrome'] }, | ||
}, | ||
], | ||
});`; | ||
|
||
const viteConfig = `/// <reference types='vitest' /> | ||
import { defineConfig } from 'vite'; | ||
import react from '@vitejs/plugin-react'; | ||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; | ||
export default defineConfig({ | ||
root: __dirname, | ||
cacheDir: '../../node_modules/.vite/app', | ||
server: { | ||
port: 4200, | ||
host: 'localhost', | ||
}, | ||
preview: { | ||
port: 4300, | ||
host: 'localhost', | ||
}, | ||
plugins: [react(), nxViteTsPaths()], | ||
// Uncomment this if you are using workers. | ||
// worker: { | ||
// plugins: [ nxViteTsPaths() ], | ||
// }, | ||
build: { | ||
outDir: '../../dist/app', | ||
emptyOutDir: true, | ||
reportCompressedSize: true, | ||
commonjsOptions: { | ||
transformMixedEsModules: true, | ||
}, | ||
}, | ||
});`; | ||
|
||
function addProject( | ||
tree: Tree, | ||
tempFs: TempFs, | ||
overrides: { | ||
noVite?: boolean; | ||
} = {} | ||
) { | ||
const appProjectConfig = { | ||
name: 'app', | ||
root: 'app', | ||
sourceRoot: `${'app'}/src`, | ||
projectType: 'application', | ||
}; | ||
|
||
const e2eProjectConfig = { | ||
name: `app-e2e`, | ||
root: `app-e2e`, | ||
sourceRoot: `app-e2e/src`, | ||
projectType: 'application', | ||
}; | ||
|
||
if (!overrides.noVite) { | ||
tree.write(`app/vite.config.ts`, viteConfig); | ||
} else { | ||
tree.write(`app/webpack.config.ts`, ``); | ||
} | ||
|
||
tree.write(`app/project.json`, JSON.stringify(appProjectConfig)); | ||
tree.write(`app-e2e/playwright.config.ts`, basePlaywrightConfig('app')); | ||
tree.write(`app-e2e/project.json`, JSON.stringify(e2eProjectConfig)); | ||
if (!overrides.noVite) { | ||
tempFs.createFile(`app/vite.config.ts`, viteConfig); | ||
} else { | ||
tempFs.createFile(`app/webpack.config.ts`, ``); | ||
} | ||
tempFs.createFilesSync({ | ||
[`app/project.json`]: JSON.stringify(appProjectConfig), | ||
[`app-e2e/playwright.config.ts`]: basePlaywrightConfig('app'), | ||
[`app-e2e/project.json`]: JSON.stringify(e2eProjectConfig), | ||
}); | ||
|
||
projectGraph.nodes['app'] = { | ||
name: 'app', | ||
type: 'app', | ||
data: { | ||
projectType: 'application', | ||
root: 'app', | ||
targets: {}, | ||
}, | ||
}; | ||
|
||
projectGraph.nodes[`app-e2e`] = { | ||
name: `app-e2e`, | ||
type: 'app', | ||
data: { | ||
projectType: 'application', | ||
root: `app-e2e`, | ||
targets: { | ||
e2e: {}, | ||
}, | ||
}, | ||
}; | ||
} |
142 changes: 142 additions & 0 deletions
142
packages/playwright/src/migrations/update-19-5-5/use-serve-static-preview-for-command.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { | ||
createProjectGraphAsync, | ||
formatFiles, | ||
getPackageManagerCommand, | ||
joinPathFragments, | ||
parseTargetString, | ||
type Tree, | ||
visitNotIgnoredFiles, | ||
} from '@nx/devkit'; | ||
import { tsquery } from '@phenomnomnominal/tsquery'; | ||
|
||
export default async function (tree: Tree) { | ||
const graph = await createProjectGraphAsync(); | ||
visitNotIgnoredFiles(tree, '', (path) => { | ||
if (!path.endsWith('playwright.config.ts')) { | ||
return; | ||
} | ||
|
||
let playwrightConfigFileContents = tree.read(path, 'utf-8'); | ||
|
||
const WEBSERVER_COMMAND_SELECTOR = | ||
'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral'; | ||
let ast = tsquery.ast(playwrightConfigFileContents); | ||
const nodes = tsquery(ast, WEBSERVER_COMMAND_SELECTOR, { | ||
visitAllChildren: true, | ||
}); | ||
if (!nodes.length) { | ||
return; | ||
} | ||
|
||
const commandValueNode = nodes[0]; | ||
const command = commandValueNode.getText(); | ||
let project: string; | ||
if (command.includes('nx run')) { | ||
const NX_TARGET_REGEX = "(?<=nx run )[^']+"; | ||
const matches = command.match(NX_TARGET_REGEX); | ||
if (!matches) { | ||
return; | ||
} | ||
const targetString = matches[0]; | ||
const parsedTargetString = parseTargetString(targetString, graph); | ||
|
||
if ( | ||
parsedTargetString.target === 'serve-static' || | ||
parsedTargetString.target === 'preview' | ||
) { | ||
return; | ||
} | ||
|
||
project = parsedTargetString.project; | ||
} else { | ||
const NX_PROJECT_REGEX = "(?<=nx [^ ]+ )[^']+"; | ||
const matches = command.match(NX_PROJECT_REGEX); | ||
if (!matches) { | ||
return; | ||
} | ||
project = matches[0]; | ||
} | ||
|
||
const pathToViteConfig = [ | ||
joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'), | ||
joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'), | ||
].find((p) => tree.exists(p)); | ||
|
||
if (!pathToViteConfig) { | ||
const newCommand = `${ | ||
getPackageManagerCommand().exec | ||
} nx run ${project}:serve-static`; | ||
tree.write( | ||
path, | ||
`${playwrightConfigFileContents.slice( | ||
0, | ||
commandValueNode.getStart() | ||
)}"${newCommand}"${playwrightConfigFileContents.slice( | ||
commandValueNode.getEnd() | ||
)}` | ||
); | ||
} else { | ||
const newCommand = `${ | ||
getPackageManagerCommand().exec | ||
} nx run ${project}:preview`; | ||
tree.write( | ||
path, | ||
`${playwrightConfigFileContents.slice( | ||
0, | ||
commandValueNode.getStart() | ||
)}"${newCommand}"${playwrightConfigFileContents.slice( | ||
commandValueNode.getEnd() | ||
)}` | ||
); | ||
playwrightConfigFileContents = tree.read(path, 'utf-8'); | ||
ast = tsquery.ast(playwrightConfigFileContents); | ||
|
||
const BASE_URL_SELECTOR = | ||
'VariableDeclaration:has(Identifier[name=baseURL])'; | ||
const baseUrlNodes = tsquery(ast, BASE_URL_SELECTOR, { | ||
visitAllChildren: true, | ||
}); | ||
if (!baseUrlNodes.length) { | ||
return; | ||
} | ||
|
||
const baseUrlNode = baseUrlNodes[0]; | ||
const newBaseUrlVariableDeclaration = | ||
"baseURL = process.env['BASE_URL'] || 'http://localhost:4300';"; | ||
tree.write( | ||
path, | ||
`${playwrightConfigFileContents.slice( | ||
0, | ||
baseUrlNode.getStart() | ||
)}${newBaseUrlVariableDeclaration}${playwrightConfigFileContents.slice( | ||
baseUrlNode.getEnd() | ||
)}` | ||
); | ||
|
||
playwrightConfigFileContents = tree.read(path, 'utf-8'); | ||
ast = tsquery.ast(playwrightConfigFileContents); | ||
const WEB_SERVER_URL_SELECTOR = | ||
'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=url]) > StringLiteral'; | ||
const webServerUrlNodes = tsquery(ast, WEB_SERVER_URL_SELECTOR, { | ||
visitAllChildren: true, | ||
}); | ||
if (!webServerUrlNodes.length) { | ||
return; | ||
} | ||
|
||
const webServerUrlNode = webServerUrlNodes[0]; | ||
const newWebServerUrl = "'http://localhost:4300'"; | ||
tree.write( | ||
path, | ||
`${playwrightConfigFileContents.slice( | ||
0, | ||
webServerUrlNode.getStart() | ||
)}${newWebServerUrl}${playwrightConfigFileContents.slice( | ||
webServerUrlNode.getEnd() | ||
)}` | ||
); | ||
} | ||
}); | ||
|
||
await formatFiles(tree); | ||
} |