diff --git a/.gitignore b/.gitignore index 926b610352976..d34ee69b86e5c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ images/settings gitlens-*.vsix tsconfig*.tsbuildinfo .DS_Store +test-results +playwright-report diff --git a/package.json b/package.json index 791dc24ae2246..b187b137e5cd5 100644 --- a/package.json +++ b/package.json @@ -17920,6 +17920,7 @@ "rebuild": "yarn run reset && yarn run build", "reset": "yarn run clean && yarn --frozen-lockfile", "test": "node ./out/test/runTest.js", + "test:e2e": "playwright test -c tests/e2e/playwright.config.ts", "watch": "webpack --watch --mode development", "watch:extension": "webpack --watch --mode development --config-name extension", "watch:webviews": "webpack --watch --mode development --config-name webviews", @@ -17965,6 +17966,7 @@ }, "devDependencies": { "@eamodio/eslint-lite-webpack-plugin": "0.0.8", + "@playwright/test": "1.46.1", "@swc/core": "1.7.3", "@twbs/fantasticon": "3.0.0", "@types/mocha": "10.0.7", @@ -18005,6 +18007,7 @@ "lz-string": "1.5.0", "mini-css-extract-plugin": "2.9.0", "mocha": "10.7.0", + "playwright": "1.46.1", "prettier": "3.1.0", "sass": "1.77.6", "sass-loader": "16.0.0", diff --git a/tests/e2e/.eslintrc.json b/tests/e2e/.eslintrc.json new file mode 100644 index 0000000000000..5bea68d9e8f98 --- /dev/null +++ b/tests/e2e/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["../../.eslintrc.base.json"], + "env": { + "node": true + }, + "files": ["**/*"], + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts new file mode 100644 index 0000000000000..a1fdcd477f214 --- /dev/null +++ b/tests/e2e/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from '@playwright/test'; +import { TestOptions } from './tests/baseTest'; + +export default defineConfig({ + use: { + headless: true, // Ensure headless mode is enabled + viewport: { width: 1920, height: 1080 }, + }, + reporter: 'list', // process.env.CI ? 'html' : 'list', + timeout: 60000, // 1 minute + workers: 1, + expect: { + timeout: 60000, // 1 minute + }, + globalSetup: './setup', + projects: [ + { + name: 'VSCode stable', + use: { + vscodeVersion: 'stable', + }, + }, + ], +}); diff --git a/tests/e2e/setup.ts b/tests/e2e/setup.ts new file mode 100644 index 0000000000000..33a5b256fa4e9 --- /dev/null +++ b/tests/e2e/setup.ts @@ -0,0 +1,6 @@ +import { downloadAndUnzipVSCode } from '@vscode/test-electron'; + +export default async () => { + await downloadAndUnzipVSCode('insiders'); + await downloadAndUnzipVSCode('stable'); +}; diff --git a/tests/e2e/specs/baseTest.ts b/tests/e2e/specs/baseTest.ts new file mode 100644 index 0000000000000..f18dd00f7fc65 --- /dev/null +++ b/tests/e2e/specs/baseTest.ts @@ -0,0 +1,72 @@ +import { test as base, type Page, _electron } from '@playwright/test'; +import { downloadAndUnzipVSCode } from '@vscode/test-electron/out/download'; +export { expect } from '@playwright/test'; +import path from 'path'; +import os from 'os'; +import fs from 'fs'; +import { spawnSync } from 'child_process'; + +export type TestOptions = { + vscodeVersion: string; +}; + +type TestFixtures = TestOptions & { + page: Page; + createTmpDir: () => Promise; +}; + +let testProjectPath: string; +export const test = base.extend({ + vscodeVersion: ['insiders', { option: true }], + page: async ({ vscodeVersion, createTmpDir }, use) => { + const defaultCachePath = await createTmpDir(); + const vscodePath = await downloadAndUnzipVSCode(vscodeVersion); + testProjectPath = path.join(__dirname, '..', '..', '..'); + + const electronApp = await _electron.launch({ + executablePath: vscodePath, + // Got it from https://github.com/microsoft/vscode-test/blob/0ec222ef170e102244569064a12898fb203e5bb7/lib/runTest.ts#L126-L160 + args: [ + '--no-sandbox', // https://github.com/microsoft/vscode/issues/84238 + '--disable-gpu-sandbox', // https://github.com/microsoft/vscode-test/issues/221 + '--disable-updates', // https://github.com/microsoft/vscode-test/issues/120 + '--skip-welcome', + '--skip-release-notes', + '--disable-workspace-trust', + `--extensionDevelopmentPath=${path.join(__dirname, '..', '..', '..')}`, + `--extensions-dir=${path.join(defaultCachePath, 'extensions')}`, + `--user-data-dir=${path.join(defaultCachePath, 'user-data')}`, + testProjectPath, + ], + }); + + const page = await electronApp.firstWindow(); + await page.context().tracing.start({ + screenshots: true, + snapshots: true, + title: test.info().title, + }); + + await use(page); + + const tracePath = test.info().outputPath('trace.zip'); + await page.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'); + if (fs.existsSync(logPath)) { + const logOutputPath = test.info().outputPath('vscode-logs'); + await fs.promises.cp(logPath, logOutputPath, { recursive: true }); + } + }, + createTmpDir: async ({}, use) => { + const tempDirs: string[] = []; + await use(async () => { + const tempDir = await fs.promises.realpath(await fs.promises.mkdtemp(path.join(os.tmpdir(), 'gltest-'))); + tempDirs.push(tempDir); + return tempDir; + }); + for (const tempDir of tempDirs) await fs.promises.rm(tempDir, { recursive: true }); + }, +}); diff --git a/tests/e2e/specs/command_palette.test.ts b/tests/e2e/specs/command_palette.test.ts new file mode 100644 index 0000000000000..3c8b681ae8e22 --- /dev/null +++ b/tests/e2e/specs/command_palette.test.ts @@ -0,0 +1,31 @@ +import { test, expect } from './baseTest'; + +test.describe('Test GitLens Command Palette commands', () => { + test('should open commit graph with the command', async ({ page }) => { + // Close any open tabs to ensure a clean state + const welcomePageTab = page.locator('div[role="tab"][aria-label="Welcome to GitLens"]'); + await welcomePageTab.waitFor({ state: 'visible', timeout: 5000 }); + welcomePageTab.locator('div.tab-actions .action-item a.codicon-close').click(); + + // Open the command palette by clicking on the View menu and selecting Command Palette + const commandPalette = page.locator('div[id="workbench.parts.titlebar"] .command-center-quick-pick'); + await commandPalette.click(); + + // Wait for the command palette input to be visible and fill it + const commandPaletteInput = page.locator('.quick-input-box input'); + await commandPaletteInput.waitFor({ state: 'visible', timeout: 5000 }); + await commandPaletteInput.fill('> GitLens: Show Commit graph'); + await page.waitForTimeout(1000); + page.keyboard.press('Enter'); + + // Click on the first element (GitLens: Show Commit graph) + /* + const commandPaletteFirstLine = page.locator('.quick-input-widget .monaco-list .monaco-list-row.focused'); + await commandPaletteFirstLine.waitFor({ state: 'visible', timeout: 5000 }); + await commandPaletteFirstLine.click(); + */ + // Graph should be opened + await page.locator('.panel.basepanel').waitFor({ state: 'visible' }); + await expect(page.locator('div[id="workbench.view.extension.gitlensPanel"]')).toBeVisible(); + }); +}); diff --git a/tests/e2e/specs/gitlens_install.test.ts b/tests/e2e/specs/gitlens_install.test.ts new file mode 100644 index 0000000000000..94acd2ab39fd1 --- /dev/null +++ b/tests/e2e/specs/gitlens_install.test.ts @@ -0,0 +1,16 @@ +import { test, expect } from './baseTest'; + +test.describe('Test GitLens installation', () => { + test('should display GitLens Welcome page after installation', async ({ page }) => { + const title = await page.textContent('.tab a'); + expect(title).toBe('Welcome to GitLens'); + }); + + test('should contain GitLens & GitLens Inspect icons in activity bar', async ({ page }) => { + await page.getByRole('tab', { name: 'GitLens Inspect' }).waitFor(); + const gitlensIcons = await page.getByRole('tab', { name: 'GitLens' }); + expect(gitlensIcons).toHaveCount(2); + + expect(await page.title()).toContain('[Extension Development Host]'); + }); +}); diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json new file mode 100644 index 0000000000000..f270fd4da1a4b --- /dev/null +++ b/tests/e2e/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extend": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ESNext", + "module": "node", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./out", + "sourceMap": true + }, + "include": ["**/*.ts"] +} diff --git a/yarn.lock b/yarn.lock index 0c101e3be43c5..d558b245eda0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -740,6 +740,13 @@ dependencies: playwright-core "1.45.3" +"@playwright/test@1.46.1": + version "1.46.1" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz#a8dfdcd623c4c23bb1b7ea588058aad41055c188" + integrity sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA== + dependencies: + playwright "1.46.1" + "@polka/url@^1.0.0-next.24": version "1.0.0-next.25" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" @@ -6074,17 +6081,36 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.45.1: + version "1.45.1" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz#549a2701556b58245cc75263f9fc2795c1158dc1" + integrity sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg== + playwright-core@1.45.3: version "1.45.3" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.3.tgz#e77bc4c78a621b96c3e629027534ee1d25faac93" integrity sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA== +playwright-core@1.46.1: + version "1.46.1" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz#28f3ab35312135dda75b0c92a3e5c0e7edb9cc8b" + integrity sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A== + +playwright@1.46.1: + version "1.46.1" + resolved "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz#ea562bc48373648e10420a10c16842f0b227c218" + integrity sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng== + dependencies: + playwright-core "1.46.1" + optionalDependencies: + fsevents "2.3.2" + playwright@^1.45.0: - version "1.45.3" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.3.tgz#75143f73093a6e1467f7097083d2f0846fb8dd2f" - integrity sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww== + version "1.45.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.1.tgz#aaa6b0d6db14796b599d80c6679e63444e942534" + integrity sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg== dependencies: - playwright-core "1.45.3" + playwright-core "1.45.1" optionalDependencies: fsevents "2.3.2"