Skip to content

Commit 9dfcf49

Browse files
committed
Continues e2e test improvements
1 parent 32d6ef5 commit 9dfcf49

File tree

15 files changed

+865
-404
lines changed

15 files changed

+865
-404
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
**/.vscode-clean/**
55
**/.vscode-test/**
66
**/.vscode-test-web/**
7+
**/test-results/**
78
dist
89
out
910
node_modules

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26757,6 +26757,7 @@
2675726757
"reset": "pnpm run clean && pnpm install --force",
2675826758
"test": "vscode-test",
2675926759
"test:e2e": "playwright test -c tests/e2e/playwright.config.ts",
26760+
"test:e2e:insiders": "set VSCODE_VERSION=insiders && playwright test -c tests/e2e/playwright.config.ts",
2676026761
"watch": "webpack --watch --mode development",
2676126762
"watch:extension": "webpack --watch --mode development --config-name extension",
2676226763
"watch:quick": "webpack --watch --mode development --env quick",

tests/e2e/baseTest.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/* eslint-disable no-empty-pattern */
2+
import fs from 'fs';
3+
import os from 'os';
4+
import path from 'path';
5+
import type { ElectronApplication, Page } from '@playwright/test';
6+
import { _electron, test as base } from '@playwright/test';
7+
import { downloadAndUnzipVSCode } from '@vscode/test-electron/out/download';
8+
import { GitFixture } from './fixtures/git';
9+
import { GitLensPage } from './pageObjects/gitLensPage';
10+
11+
export { expect } from '@playwright/test';
12+
export { GitFixture } from './fixtures/git';
13+
14+
export const MaxTimeout = 10000;
15+
16+
export interface LaunchOptions {
17+
vscodeVersion?: string;
18+
/**
19+
* Optional async setup callback that runs before VS Code launches.
20+
* Use this to create a test repo, files, or any other setup needed.
21+
* Returns the path to open in VS Code.
22+
* If not provided, opens the extension folder.
23+
*/
24+
setup?: () => Promise<string>;
25+
}
26+
27+
export interface VSCodeInstance {
28+
page: Page;
29+
electronApp: ElectronApplication;
30+
gitlens: GitLensPage;
31+
}
32+
33+
/** Base fixtures for all E2E tests */
34+
interface BaseFixtures {
35+
createTempFolder: () => Promise<string>;
36+
/**
37+
* Creates and initializes a new Git repository in a temporary directory.
38+
* The repository is automatically cleaned up after the test.
39+
* Returns a GitFixture instance for interacting with the repository.
40+
*/
41+
createGitRepo: () => Promise<GitFixture>;
42+
}
43+
44+
interface WorkerFixtures {
45+
vscode: VSCodeInstance;
46+
vscodeOptions: LaunchOptions;
47+
}
48+
49+
export const test = base.extend<BaseFixtures, WorkerFixtures>({
50+
// Default options (can be overridden per-file)
51+
// eslint-disable-next-line no-restricted-globals
52+
vscodeOptions: [{ vscodeVersion: process.env.VSCODE_VERSION ?? 'stable' }, { scope: 'worker', option: true }],
53+
54+
// vscode launches VS Code with GitLens extension (shared per worker)
55+
vscode: [
56+
async ({ vscodeOptions }, use) => {
57+
const tempDir = await createTmpDir();
58+
const vscodePath = await downloadAndUnzipVSCode(vscodeOptions.vscodeVersion ?? 'stable');
59+
const extensionPath = path.join(__dirname, '..', '..');
60+
61+
// Run setup callback if provided, otherwise open extension folder
62+
const workspacePath = vscodeOptions.setup ? await vscodeOptions.setup() : extensionPath;
63+
64+
const electronApp = await _electron.launch({
65+
executablePath: vscodePath,
66+
args: [
67+
'--no-sandbox',
68+
'--disable-gpu-sandbox',
69+
'--disable-updates',
70+
'--skip-welcome',
71+
'--skip-release-notes',
72+
'--disable-workspace-trust',
73+
`--extensionDevelopmentPath=${extensionPath}`,
74+
`--extensions-dir=${path.join(tempDir, 'extensions')}`,
75+
`--user-data-dir=${path.join(tempDir, 'user-data')}`,
76+
workspacePath,
77+
],
78+
});
79+
80+
const page = await electronApp.firstWindow();
81+
const gitlens = new GitLensPage(page);
82+
83+
// Wait for GitLens to activate before providing to tests
84+
await gitlens.waitForActivation();
85+
86+
await use({ page: page, electronApp: electronApp, gitlens: gitlens } satisfies VSCodeInstance);
87+
88+
// Cleanup
89+
await electronApp.close();
90+
await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
91+
},
92+
{ scope: 'worker' },
93+
],
94+
95+
createGitRepo: async ({}, use) => {
96+
const repos: GitFixture[] = [];
97+
await use(async () => {
98+
const dir = await createTmpDir();
99+
const git = new GitFixture(dir);
100+
await git.init();
101+
repos.push(git);
102+
return git;
103+
});
104+
// Cleanup after test
105+
for (const repo of repos) {
106+
await fs.promises.rm(repo.repoDir, { recursive: true, force: true }).catch(() => {});
107+
}
108+
},
109+
110+
createTempFolder: async ({}, use) => {
111+
const dirs: string[] = [];
112+
await use(async () => {
113+
const dir = await createTmpDir();
114+
dirs.push(dir);
115+
return dir;
116+
});
117+
// Cleanup after test
118+
for (const dir of dirs) {
119+
await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => {});
120+
}
121+
},
122+
});
123+
124+
export async function createTmpDir(): Promise<string> {
125+
return fs.promises.realpath(await fs.promises.mkdtemp(path.join(os.tmpdir(), 'gltest-')));
126+
}

tests/e2e/fixtures/git.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/* eslint-disable no-restricted-globals */
2+
import { spawn } from 'child_process';
3+
import * as fs from 'fs/promises';
4+
import * as path from 'path';
5+
6+
export class GitFixture {
7+
constructor(private readonly repoPath: string) {}
8+
9+
get repoDir(): string {
10+
return this.repoPath;
11+
}
12+
13+
async init(): Promise<void> {
14+
await fs.mkdir(this.repoPath, { recursive: true });
15+
await this.git('init');
16+
// Configure user for commits
17+
await this.git('config', 'user.email', '[email protected]');
18+
await this.git('config', 'user.name', 'Your Name');
19+
// Initial commit to have a HEAD
20+
await this.commit('Initial commit');
21+
}
22+
23+
async commit(message: string, fileName: string = 'test-file.txt', content: string = 'content'): Promise<void> {
24+
const filePath = path.join(this.repoPath, fileName);
25+
await fs.writeFile(filePath, content);
26+
await this.git('add', fileName);
27+
await this.git('commit', '-m', message);
28+
}
29+
30+
async branch(name: string): Promise<void> {
31+
await this.git('branch', name);
32+
}
33+
34+
async checkout(name: string, create: boolean = false): Promise<void> {
35+
if (create) {
36+
await this.git('checkout', '-b', name);
37+
} else {
38+
await this.git('checkout', name);
39+
}
40+
}
41+
42+
async worktree(worktreePath: string, branch: string): Promise<void> {
43+
await this.git('worktree', 'add', worktreePath, branch);
44+
}
45+
46+
/**
47+
* Start an interactive rebase. This will open the rebase editor.
48+
* @param onto The commit or ref to rebase onto (e.g., 'HEAD~3')
49+
* @param env Additional environment variables
50+
*/
51+
async rebaseInteractive(onto: string, env?: Record<string, string>): Promise<void> {
52+
await this.git('rebase', '-i', onto, env);
53+
}
54+
55+
/**
56+
* Abort an in-progress rebase
57+
*/
58+
async rebaseAbort(): Promise<void> {
59+
await this.git('rebase', '--abort');
60+
}
61+
62+
/**
63+
* Get the current branch name
64+
*/
65+
async getCurrentBranch(): Promise<string> {
66+
return this.git('rev-parse', '--abbrev-ref', 'HEAD');
67+
}
68+
69+
/**
70+
* Get the short SHA of a ref
71+
*/
72+
async getShortSha(ref: string = 'HEAD'): Promise<string> {
73+
return this.git('rev-parse', '--short', ref);
74+
}
75+
76+
/**
77+
* Check if a rebase is in progress
78+
*/
79+
async isRebaseInProgress(): Promise<boolean> {
80+
try {
81+
await this.git('rev-parse', '--verify', 'REBASE_HEAD');
82+
return true;
83+
} catch {
84+
return false;
85+
}
86+
}
87+
88+
/**
89+
* Reset the repository to a specific ref
90+
* @param ref The ref to reset to (default: HEAD)
91+
* @param mode The reset mode: 'soft', 'mixed', or 'hard' (default: 'hard')
92+
*/
93+
async reset(ref: string = 'HEAD', mode: 'soft' | 'mixed' | 'hard' = 'hard'): Promise<void> {
94+
await this.git('reset', `--${mode}`, ref);
95+
}
96+
97+
/**
98+
* Clean untracked files and directories
99+
*/
100+
async clean(): Promise<void> {
101+
await this.git('clean', '-fd');
102+
}
103+
104+
private async git(command: string, ...args: (string | Record<string, string> | undefined)[]): Promise<string> {
105+
// Separate command args from env options
106+
const cmdArgs: string[] = [];
107+
let envOverrides: Record<string, string> | undefined;
108+
109+
for (const arg of args) {
110+
if (typeof arg === 'string') {
111+
cmdArgs.push(arg);
112+
} else if (typeof arg === 'object' && arg !== null) {
113+
envOverrides = arg;
114+
}
115+
}
116+
117+
const fullArgs = [command, ...cmdArgs];
118+
return new Promise((resolve, reject) => {
119+
const child = spawn('git', fullArgs, {
120+
cwd: this.repoPath,
121+
env: envOverrides ? { ...process.env, ...envOverrides } : process.env,
122+
});
123+
let stdout = '';
124+
let stderr = '';
125+
126+
child.stdout.on('data', (data: string | Buffer) => (stdout += data.toString()));
127+
child.stderr.on('data', (data: string | Buffer) => (stderr += data.toString()));
128+
129+
child.on('close', code => {
130+
if (code === 0) {
131+
resolve(stdout.trim());
132+
} else {
133+
reject(new Error(`Git command failed: git ${fullArgs.join(' ')}\n${stderr}`));
134+
}
135+
});
136+
});
137+
}
138+
}

tests/e2e/pageObjects/components/activityBar.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Locator, Page } from '@playwright/test';
2-
import { MaxTimeout } from '../../specs/baseTest';
2+
import { MaxTimeout } from '../../baseTest';
3+
import type { VSCodePage } from '../vscodePage';
34

45
/**
56
* Component for VS Code Activity Bar interactions.
@@ -8,7 +9,10 @@ import { MaxTimeout } from '../../specs/baseTest';
89
export class ActivityBar {
910
private readonly container: Locator;
1011

11-
constructor(private readonly page: Page) {
12+
constructor(
13+
private readonly vscode: VSCodePage,
14+
private readonly page: Page,
15+
) {
1216
this.container = page.locator('[id="workbench.parts.activitybar"]');
1317
}
1418

0 commit comments

Comments
 (0)