Skip to content
Merged

e2e #19

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
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
// eslint-disable-next-line mmkal/import/no-extraneous-dependencies
const recommended = require('eslint-plugin-mmkal').getRecommended()

/** @type {import('eslint').Linter.Config} */
module.exports = {
...recommended,
ignorePatterns: [...recommended.ignorePatterns, 'test-results/**'],
overrides: [
...recommended.overrides,
{
files: ['*.md'],
rules: {
'mmkal/unicorn/filename-case': 'off',
'mmkal/prettier/prettier': 'off',
'no-trailing-spaces': 'off',
},
},
],
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
},
"eslint.validate": [
"javascript",
"typescript"
"typescript",
"markdown"
],
"typescript.tsdk": "node_modules/typescript/lib"
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export * from './some/path/module-c'

![](./gifs/barrel.gif)

<!-- codegen:start {preset: markdownFromJsdoc, source: src/presets/custom.ts, export: custom} -->
<!-- codegen:start {preset: markdownFromJsdoc, source: src/presets/custom.ts, export: custom} -->
#### [custom](./src/presets/custom.ts#L32)

Define your own codegen function, which will receive all options specified. Import the `Preset` type from this library to define a strongly-typed preset function:
Expand Down
159 changes: 159 additions & 0 deletions e2e/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// eslint-disable-next-line mmkal/unicorn/filename-case
/* eslint-disable no-console */
/* eslint-disable mmkal/import/no-extraneous-dependencies */
/**
* 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 as base, type Page, _electron} from '@playwright/test'
import {downloadAndUnzipVSCode} from '@vscode/test-electron/out/download'

export {expect} from '@playwright/test'
import {spawnSync} from 'child_process'
import dedent from 'dedent'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'

export type TestOptions = {
vscodeVersion: string
}

type TestFixtures = TestOptions & {
workbox: Page
createProject: () => Promise<string>
createTempDir: () => Promise<string>
}

export const test = base.extend<TestFixtures>({
vscodeVersion: ['insiders', {option: true}],
async workbox({vscodeVersion, createProject, createTempDir}, use) {
const defaultCachePath = await createTempDir()
const vscodePath = await downloadAndUnzipVSCode(vscodeVersion)
await fs.promises.cp(
'/Users/mmkal/.vscode/extensions/dbaeumer.vscode-eslint-2.4.2',
path.join(defaultCachePath, 'extensions', 'dbaeumer.vscode-eslint-2.4.2'),
{recursive: true},
)
const projectPath = await createProject()
const electronApp = await _electron.launch({
executablePath: vscodePath,
args: [
// Stolen from https://github.com/microsoft/vscode-test/blob/0ec222ef170e102244569064a12898fb203e5bb7/lib/runTest.ts#L126-L160
// https://github.com/microsoft/vscode/issues/84238
'--no-sandbox',
// https://github.com/microsoft/vscode-test/issues/221
'--disable-gpu-sandbox',
// https://github.com/microsoft/vscode-test/issues/120
'--disable-updates',
'--skip-welcome',
'--skip-release-notes',
'--disable-workspace-trust',
`--extensionDevelopmentPath=${path.join(__dirname, '..', '..')}`,
`--extensions-dir=${path.join(defaultCachePath, 'extensions')}`,
// `--extensions-dir=/Users/mmkal/.vscode/extensions`,
`--user-data-dir=${path.join(defaultCachePath, 'user-data')}`,
projectPath,
],
recordVideo: {dir: 'test-results/videos'},
})
const workbox = await electronApp.firstWindow()
await workbox.context().tracing.start({screenshots: true, snapshots: true, title: test.info().title})
await use(workbox)
const tracePath = test.info().outputPath('trace.zip')
await workbox.context().tracing.stop({path: tracePath})
test.info().attachments.push({name: 'trace', path: tracePath, contentType: 'application/zip'})
await electronApp.close()
const logPath = path.join(defaultCachePath, 'user-data')
const video = workbox.video()
if (video) {
// await workbox.video()?.saveAs(path.join(process.cwd(), 'videos', slugify(test.info().title) + '.webm'))
const exec = (command: string) => spawnSync(command, {cwd: projectPath, stdio: 'inherit', shell: true})
exec(`ffmpeg -y -i ${await video.path()} -pix_fmt rgb24 ${process.cwd()}/gifs/${slugify(test.info().title)}.gif`)
}

if (fs.existsSync(logPath)) {
const logOutputPath = test.info().outputPath('vscode-logs')
await fs.promises.cp(logPath, logOutputPath, {recursive: true})
}
},
async createProject({createTempDir}, use) {
await use(async () => {
// We want to be outside of the project directory to avoid already installed dependencies.
const projectPath = await createTempDir()
if (fs.existsSync(projectPath)) await fs.promises.rm(projectPath, {recursive: true})
console.log(`Creating project in ${projectPath}`)
await fs.promises.mkdir(projectPath)
const exec = (command: string) => spawnSync(command, {cwd: projectPath, stdio: 'inherit', shell: true})
const write = (name: string, content: string) => {
const fullpath = path.join(projectPath, name)
fs.mkdirSync(path.dirname(fullpath), {recursive: true})
fs.writeFileSync(fullpath, content)
}

// exec(`npm init playwright@latest --yes -- --quiet --browser=chromium --gha --install-deps`)
exec('npm init -y')
exec(`pnpm install eslint eslint-plugin-codegen eslint-plugin-mmkal typescript ts-node --save-dev`)

write('tsconfig.json', fs.readFileSync(path.join(__dirname, '..', 'tsconfig.json'), 'utf8'))
write(
'.eslintrc.js',
// parserOptions: {project: null} // this is more of an eslint-plugin-mmkal thing but typescript-eslint doesn't like the processor I've got
// also, need to not pass fatal messages in the processor to eslint-plugin-markdown
dedent`
module.exports = {
...require('eslint-plugin-mmkal').getRecommended(),
parserOptions: {project: null},
plugins: ['codegen'],
extends: ['plugin:codegen/recommended'],
rules: {
'mmkal/codegen/codegen': 'off',
'codegen/codegen': 'warn',
},
}
`,
)

write('src/barrel/a.ts', 'export const a = 1')
write('src/barrel/b.ts', 'export const b = 1')
write('src/barrel/index.ts', '')
write('src/custom/index.ts', '')
write('README.md', '')

write(
'.vscode/settings.json',
fs
.readFileSync(path.join(__dirname, '../.vscode/settings.json'))
.toString()
.replace('{', '{\n "editor.autoClosingBrackets": "never",')
.replace('{', '{\n "editor.autoIndent": "none",')
.replace('{', '{\n "editor.insertSpaces": false,'),
)

return projectPath
})
},
// eslint-disable-next-line no-empty-pattern
async createTempDir({}, use) {
const tempDirs: string[] = []
await use(async () => {
const tempDir = await fs.promises.realpath(await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pwtest-')))
tempDirs.push(tempDir)
return tempDir
})
for (const tempDir of tempDirs) await fs.promises.rm(tempDir, {recursive: true})
},
})

const slugify = (string: string) => string.replace(/\W+/g, ' ').trim().replace(/\s+/g, '-')
137 changes: 137 additions & 0 deletions e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {Page} from '@playwright/test'
import dedent from 'dedent'
import {test} from './base'

test.describe.configure({mode: 'serial'})

// https://github.com/microsoft/playwright/issues/28266 https://github.com/microsoft/playwright-vscode/tree/main/tests-integration

const _installEslint = async (page: Page) => {
await page.getByRole('tab', {name: 'Extensions (⇧⌘X)'}).locator('a').click()
await page.locator('.view-line').click()
await page.getByLabel('The editor is not accessible').fill('eslint')
await page.getByLabel('ESLint, 2.4.2, Verified').getByRole('button', {name: 'Install'}).click()
await page.getByRole('tab', {name: 'Explorer (⇧⌘E)'}).locator('a').click()
await page.getByLabel('Files Explorer').press('Meta+p')
await page.getByPlaceholder('Search files by name (append').fill('index.ts')
await page.getByPlaceholder('Search files by name (append').press('Enter')
}

async function openExistingFile(page: Page, name: string) {
await new Promise(r => setTimeout(r, 500))
await page.keyboard.press('Meta+p')
await new Promise(r => setTimeout(r, 500))
await page.keyboard.type(name)
await new Promise(r => setTimeout(r, 500))
await page.keyboard.press('Enter')
}

const codegen = async (page: Page, params: {file: string; type: () => Promise<void>; result: string}) => {
await openExistingFile(page, params.file)

await new Promise(r => setTimeout(r, 500))
await page.keyboard.press('Meta+1')
await params.type()
await page.getByTitle('Show Code Actions. Preferred').click()
await new Promise(r => setTimeout(r, 500))
// await page.getByText('Fix all auto-fixable problems').click() // didn't work - loses focus somehow, so use Down-Down-Down-Enter instead 🤷‍♂️
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
await new Promise(r => setTimeout(r, 1000))
await page.keyboard.press('Enter')

for (const line of params.result.split('\n')) {
await page.getByText(line).first().waitFor()
}

await new Promise(r => setTimeout(r, 1500))
}

test('barrel', async ({workbox: page}) => {
await codegen(page, {
file: 'barrel/index.ts',
type: async () => page.keyboard.type('// codegen:start {preset: barrel}', {delay: 50}),
result: dedent`
// codegen:start {preset: barrel}
export * from './a'
export * from './b'
// codegen:end
`,
})
})

test('markdownTOC', async ({workbox: page}) => {
await codegen(page, {
file: 'README.md',
async type() {
await page.keyboard.type(dedent`
# foo package

## Table of contents

## Getting started

### Installation

Install via npm:

\`\`\`
npm install foo
\`\`\`

### Setup

First, do a handstand. Then you are ready!

## Advanced usage

Do a one-handed handstand.

## Troubleshooting

If you can't do a handstand, try a headstand.

## FAQ

Q: What if I can't do a headstand?
A: Bad luck
`)
await page.keyboard.press('Meta+s')
await page.getByText('Table of contents').click()
await page.keyboard.press('Meta+ArrowRight')
await page.keyboard.press('Enter')
await page.keyboard.type('<!-- codegen:start {preset: markdownTOC} -->', {delay: 50})
await new Promise(r => setTimeout(r, 500))
},
result: '#installation',
})
})

test('custom', async ({workbox: page}) => {
await codegen(page, {
file: 'custom/index.ts',
async type() {
await page.keyboard.type(
dedent`
export const findEslintDependencies: import('eslint-plugin-codegen').Preset = ({dependencies: {glob}}) => {
const packages = glob.globSync('node_modules/eslint*/package.json', {cwd: process.cwd()})
const folders = packages.map(p => p.split('/').at(-2))
return \`export const eslintDependencies = \${JSON.stringify(folders, null, 2)}\`
}
`,
)

await page.keyboard.press('Enter')
await page.keyboard.press('Enter')
await page.keyboard.press('Meta+s')
await new Promise(r => setTimeout(r, 500))

await page.keyboard.type(dedent`
// codegen:start {preset: custom, export: findEslintDependencies}
`)
await page.keyboard.press('Meta+Shift+Tab')
},
result: '"eslint-plugin-codegen"',
})
})
Binary file modified gifs/barrel.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified gifs/custom.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified gifs/markdownTOC.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ module.exports = {
testEnvironment: 'node',
prettierPath: require.resolve('prettier-2'),
modulePathIgnorePatterns: ['<rootDir>/.vscode-test'],
testPathIgnorePatterns: ['<rootDir>/e2e'],
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"eslint": "eslint --ext '.ts,.js,.md'",
"lint": "tsc && eslint .",
"build": "tsc -p tsconfig.lib.json",
"test": "jest"
"test": "jest",
"e2e": "playwright test"
},
"dependencies": {
"@babel/core": "^7.11.6",
Expand All @@ -47,7 +48,7 @@
"@types/jest": "29.0.0",
"@types/js-yaml": "3.12.5",
"@types/lodash": "^4.14.202",
"@types/node": "^14.0.0",
"@types/node": "^20.0.0",
"dedent": "^1.5.1",
"eslint-plugin-markdown": "^3.0.1",
"expect": "^29.7.0",
Expand All @@ -62,14 +63,15 @@
},
"devDependencies": {
"@babel/types": "7.12.11",
"@playwright/test": "^1.40.0",
"@types/babel__generator": "7.6.2",
"@types/babel__traverse": "7.11.0",
"@types/dedent": "0.7.0",
"@types/glob": "7.1.3",
"@types/jest": "29.5.8",
"@types/js-yaml": "3.12.5",
"@types/minimatch": "3.0.3",
"@types/node": "^20.0.0",
"@vscode/test-electron": "^2.3.6",
"eslint": "8.54.0",
"eslint-plugin-mmkal": "0.2.0",
"expect-type2": "npm:expect-type@0.14.0",
Expand Down
Loading