Skip to content

Commit 4555f88

Browse files
committed
Start to set up WDIO
1 parent 4959c94 commit 4555f88

File tree

15 files changed

+1586
-254
lines changed

15 files changed

+1586
-254
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src/lib/backend/ipc.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export async function invoke<T>(command: string, params: Record<string, unknown>
7070

7171
try {
7272
if (import.meta.env.VITE_BUILD_TARGET === 'web') {
73-
const response = await fetch('http://localhost:6978', {
73+
const response = await fetch(`http://${getWebUrl()}`, {
7474
method: 'POST',
7575
headers: {
7676
'Content-Type': 'application/json'
@@ -119,7 +119,7 @@ class WebListener {
119119
this.handlers.push(handler);
120120
this.count++;
121121
if (!this.socket) {
122-
this.socket = new WebSocket('ws://localhost:6978/ws');
122+
this.socket = new WebSocket(`ws://${getWebUrl()}/ws`);
123123
this.socket.addEventListener('message', (event) => {
124124
const data: { name: string; payload: any } = JSON.parse(event.data);
125125
for (const handler of this.handlers) {
@@ -143,3 +143,10 @@ class WebListener {
143143
};
144144
}
145145
}
146+
147+
function getWebUrl(): string {
148+
const parsedCookies = document.cookie.split('; ').map((c) => c.split('=', 2));
149+
const host = parsedCookies.find(([k, _v]) => k === 'butlerHost')?.[1] || 'localhost';
150+
const port = parsedCookies.find(([k, _v]) => k === 'butlerPort')?.[1] || '6978';
151+
return `${host}:${port}`;
152+
}

crates/but-path/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ use std::path::PathBuf;
22

33
pub fn app_data_dir() -> anyhow::Result<PathBuf> {
44
if let Ok(test_dir) = std::env::var("TEST_APP_DATA_DIR") {
5-
return Ok(PathBuf::from(test_dir));
5+
return Ok(PathBuf::from(test_dir).join("com.gitbutler.app"));
66
}
77
dirs::data_dir()
88
.ok_or(anyhow::anyhow!("Could not get app data dir"))
99
.map(|dir| dir.join(identifier()))
1010
}
1111

12+
pub fn app_config_dir() -> anyhow::Result<PathBuf> {
13+
if let Ok(test_dir) = std::env::var("TEST_APP_DATA_DIR") {
14+
return Ok(PathBuf::from(test_dir).join("gitbutler"));
15+
}
16+
dirs::data_dir()
17+
.ok_or(anyhow::anyhow!("Could not get app data dir"))
18+
.map(|dir| dir.join("gitbutler"))
19+
}
20+
1221
fn identifier() -> &'static str {
1322
option_env!("IDENTIFIER").unwrap_or_else(|| {
1423
if let Some(channel) = option_env!("CHANNEL") {

crates/but-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ open = "5.3"
7777
url = "2.5"
7878
gitbutler-id.workspace = true
7979
uuid.workspace = true
80+
but-path.workspace = true

crates/but-server/src/lib.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,8 @@ pub async fn run() {
5555
.allow_origin(Any)
5656
.allow_headers(Any);
5757

58-
let config_dir = dirs::config_dir()
59-
.expect("missing config dir")
60-
.join("gitbutler");
61-
62-
// TODO: This should probably be a real com.gitbutler.whatever directory
63-
let app_data_dir = dirs::config_dir()
64-
.expect("missing config dir")
65-
.join("gitbutler-server");
58+
let config_dir = but_path::app_config_dir().unwrap();
59+
let app_data_dir = but_path::app_data_dir().unwrap();
6660

6761
let broadcaster = Arc::new(Mutex::new(Broadcaster::new()));
6862
let extra = Extra {
@@ -102,8 +96,11 @@ pub async fn run() {
10296
.layer(ServiceBuilder::new().layer(cors));
10397

10498
// run our app with hyper, listening globally on port 6978
105-
let listener = tokio::net::TcpListener::bind("0.0.0.0:6978").await.unwrap();
106-
println!("Running at 0.0.0.0:6978");
99+
let port = std::env::var("BUTLER_PORT").unwrap_or("6978".into());
100+
let host = std::env::var("BUTLER_HOST").unwrap_or("0.0.0.0".into());
101+
let url = format!("{}:{}", host, port);
102+
let listener = tokio::net::TcpListener::bind(&url).await.unwrap();
103+
println!("Running at {}", url);
107104
axum::serve(listener, app).await.unwrap();
108105
}
109106

crates/but-settings/src/persistence.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ impl AppSettings {
1515
pub fn load(config_path: &Path) -> Result<Self> {
1616
// If the file on config_path does not exist, create it empty
1717
if !config_path.exists() {
18-
gitbutler_fs::write(config_path, "{}\n")?;
18+
gitbutler_fs::create_dirs_then_write(config_path, "{}\n")?;
1919
}
2020

2121
// merge customizations from disk into the defaults to get a complete set of settings.

crates/but-settings/src/watch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl AppSettingsWithDiskSync {
6767
/// * `config_dir` contains the application settings file.
6868
/// * `subscriber` receives any change to it.
6969
pub fn new(config_dir: impl AsRef<Path>) -> Result<Self> {
70-
let config_path = config_dir.as_ref().join(SETTINGS_FILE);
70+
let config_path = dbg!(config_dir.as_ref().join(SETTINGS_FILE));
7171
let app_settings = AppSettings::load(&config_path)?;
7272
let app_settings = Arc::new(RwLock::new(app_settings));
7373

crates/gitbutler-project/src/controller.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use gitbutler_error::error;
66
use super::{storage, storage::UpdateRequest, Project, ProjectId};
77
use crate::AuthKey;
88

9-
#[derive(Clone)]
9+
#[derive(Clone, Debug)]
1010
pub(crate) struct Controller {
1111
local_data_dir: PathBuf,
1212
projects_storage: storage::Storage,

e2e/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "e2e",
3+
"type": "module",
4+
"devDependencies": {
5+
"@wdio/cli": "^9.18.4",
6+
"@wdio/globals": "^9.17.0",
7+
"@wdio/local-runner": "^9.18.4",
8+
"@wdio/mocha-framework": "^9.18.0",
9+
"@wdio/spec-reporter": "^9.18.0"
10+
},
11+
"scripts": {
12+
"test:web": "wdio run ./wdio.web.conf.ts"
13+
},
14+
"dependencies": {
15+
"get-port": "^7.1.0",
16+
"tmp-promise": "^3.0.3"
17+
}
18+
}

e2e/test/specs/utils.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import getPort from 'get-port';
2+
import { dir } from 'tmp-promise';
3+
import { spawn } from 'node:child_process';
4+
import { Socket } from 'node:net';
5+
import * as path from 'node:path';
6+
7+
interface GitButler {
8+
visit(path: string): Promise<void>;
9+
cleanup(): Promise<void>;
10+
}
11+
12+
const VITE_HOST = 'localhost';
13+
const BUTLER_HOST = 'localhost';
14+
15+
// Colors for console output
16+
const colors = {
17+
reset: '\x1b[0m',
18+
bright: '\x1b[1m',
19+
green: '\x1b[32m',
20+
yellow: '\x1b[33m',
21+
red: '\x1b[31m',
22+
blue: '\x1b[34m',
23+
cyan: '\x1b[36m'
24+
};
25+
26+
function log(message: string, color = colors.reset) {
27+
// eslint-disable-next-line no-console
28+
console.log(`${color}${message}${colors.reset}`);
29+
}
30+
31+
function spawnProcess(
32+
command: string,
33+
args: string[],
34+
cwd = process.cwd(),
35+
env: Record<string, string> = {}
36+
) {
37+
return spawn(command, args, {
38+
cwd,
39+
stdio: 'inherit',
40+
env: {
41+
...process.env,
42+
ELECTRON_ENV: 'development',
43+
VITE_BUILD_TARGET: 'web',
44+
VITE_HOST,
45+
BUTLER_HOST,
46+
...env
47+
}
48+
});
49+
}
50+
51+
async function runCommand(command: string, args: string[], cwd = process.cwd()) {
52+
return await new Promise<void>((resolve, reject) => {
53+
log(`Running: ${command} ${args.join(' ')}`, colors.cyan);
54+
55+
const child = spawnProcess(command, args, cwd);
56+
57+
child.on('close', (code) => {
58+
if (code === 0) {
59+
resolve();
60+
} else {
61+
reject(new Error(`Command failed with exit code ${code}`));
62+
}
63+
});
64+
65+
child.on('error', (error) => {
66+
reject(error);
67+
});
68+
});
69+
}
70+
71+
async function checkPort(port: number, host = 'localhost') {
72+
return await new Promise((resolve) => {
73+
const socket = new Socket();
74+
75+
socket.setTimeout(500);
76+
socket.on('connect', () => {
77+
socket.destroy();
78+
resolve(true);
79+
});
80+
81+
socket.on('timeout', () => {
82+
socket.destroy();
83+
resolve(false);
84+
});
85+
86+
socket.on('error', () => {
87+
resolve(false);
88+
});
89+
90+
socket.connect(port, host);
91+
});
92+
}
93+
94+
async function waitForServer(port: number, host = 'localhost', maxAttempts = 30) {
95+
// log(`Waiting for server on ${host}:${port}...`, colors.yellow);
96+
97+
for (let i = 0; i < maxAttempts; i++) {
98+
if (await checkPort(port, host)) {
99+
// log(`✅ Server is ready on ${host}:${port}`, colors.green);
100+
return true;
101+
}
102+
103+
if (i < maxAttempts - 1) {
104+
await new Promise((resolve) => setTimeout(resolve, 1000));
105+
}
106+
}
107+
108+
return false;
109+
}
110+
111+
let builtDesktop = false;
112+
113+
export async function startGitButler(browser: WebdriverIO.Browser): Promise<GitButler> {
114+
const configDir = await dir({ unsafeCleanup: true });
115+
const workDir = await dir({ unsafeCleanup: true });
116+
117+
const vitePort = await getPort();
118+
const butPort = await getPort();
119+
120+
// Get paths
121+
const rootDir = path.resolve(import.meta.dirname, '../../..');
122+
const desktopDir = path.resolve(rootDir, 'apps/desktop');
123+
124+
// Start the Vite dev server
125+
if (!builtDesktop) {
126+
await runCommand('pnpm', ['build:desktop'], rootDir);
127+
builtDesktop = true;
128+
}
129+
const viteProcess = spawnProcess('pnpm', ['preview', '--port', `${vitePort}`], desktopDir, {
130+
// VITE_PORT: `${vitePort}`,
131+
// BUTLER_PORT: `${butPort}`
132+
});
133+
134+
viteProcess.on('close', (code) => {
135+
if (code !== 0 && code !== null) {
136+
log(`Vite dev server exited with code ${code}`, colors.red);
137+
}
138+
});
139+
140+
viteProcess.on('error', (error) => {
141+
log(`Vite dev server error: ${error.message}`, colors.red);
142+
});
143+
144+
// Wait for Vite to be ready
145+
const serverReady = await waitForServer(vitePort, VITE_HOST);
146+
147+
if (!serverReady) {
148+
throw new Error(`Vite dev server failed to start on ${VITE_HOST}:${vitePort}`);
149+
}
150+
151+
// Start the but-server server
152+
const butProcess = spawnProcess('cargo', ['run', '-p', 'but', '--', 'serve'], rootDir, {
153+
VITE_PORT: `${vitePort}`,
154+
BUTLER_PORT: `${butPort}`,
155+
TEST_APP_DATA_DIR: configDir.path
156+
});
157+
158+
butProcess.on('close', (code) => {
159+
if (code !== 0 && code !== null) {
160+
log(`Butler server exited with code ${code}`, colors.red);
161+
}
162+
});
163+
164+
butProcess.on('error', (error) => {
165+
log(`Butler server error: ${error.message}`, colors.red);
166+
});
167+
168+
// Wait for Vite to be ready
169+
const butReady = await waitForServer(butPort, BUTLER_HOST);
170+
171+
if (!butReady) {
172+
throw new Error(`Butler server failed to start on ${BUTLER_HOST}:${butPort}`);
173+
}
174+
175+
browser.setCookies([
176+
{
177+
name: 'butlerPort',
178+
value: `${butPort}`
179+
},
180+
{
181+
name: 'butlerHost',
182+
value: BUTLER_HOST
183+
}
184+
]);
185+
186+
return {
187+
async visit(path: string) {
188+
if (path.startsWith('/')) {
189+
path = path.slice(1);
190+
}
191+
192+
await browser.url(`http://${VITE_HOST}:${vitePort}/${path}`);
193+
},
194+
async cleanup() {
195+
// log('Stopping Vite dev server...', colors.yellow);
196+
viteProcess.kill();
197+
// log('Stopping butler server...', colors.yellow);
198+
butProcess.kill();
199+
await configDir.cleanup();
200+
await workDir.cleanup();
201+
}
202+
};
203+
}

0 commit comments

Comments
 (0)