diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 8ca8d6168..d4f3ba1eb 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -29,6 +29,7 @@ jobs: libayatana-appindicator3-dev \ libwebkit2gtk-4.0-dev \ webkit2gtk-driver \ + ffmpeg \ xvfb - name: Setup rust-toolchain stable id: rust-toolchain diff --git a/.gitignore b/.gitignore index ba04df6db..42575a786 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ generated-do-not-edit/ .sentryclirc .DS_Store +apps/desktop/e2e/videos/*.mp4 .env .env.* diff --git a/apps/desktop/e2e/record.ts b/apps/desktop/e2e/record.ts new file mode 100644 index 000000000..100174aab --- /dev/null +++ b/apps/desktop/e2e/record.ts @@ -0,0 +1,82 @@ +import path from 'node:path'; +import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'; +import type { Frameworks } from '@wdio/types'; + +function filePath({ + test, + videoPath, + extension +}: { + test: Frameworks.Test; + videoPath: string; + extension: string; +}) { + return path.join(videoPath, `${fileName(test.parent)}-${fileName(test.title)}.${extension}`); +} + +function fileName(title: string) { + return encodeURIComponent(title.trim().replace(/\s+/g, '-')); +} + +export class TestRecorder { + ffmpeg!: ChildProcessWithoutNullStreams; + + constructor() {} + + stop() { + this.ffmpeg?.kill('SIGINT'); + } + + start(test: Frameworks.Test, videoPath: string) { + if (!videoPath) { + throw new Error('Video path not set. Set using setPath() function.'); + } + + if (process.env.DISPLAY && process.env.DISPLAY.startsWith(':')) { + const parsedPath = filePath({ + test, + videoPath, + extension: 'mp4' + }); + + this.ffmpeg = spawn('ffmpeg', [ + '-f', + 'x11grab', // Grab the X11 display + '-video_size', + '1280x1024', // Video size + '-i', + process.env.DISPLAY, // Input file url + '-loglevel', + 'error', // Log only errors + '-y', // Overwrite output files without asking + '-pix_fmt', + 'yuv420p', // QuickTime Player support, "Use -pix_fmt yuv420p for compatibility with outdated media players" + parsedPath // Output file + ]); + + const logBuffer = function (buffer: Buffer, prefix: string) { + const lines = buffer.toString().trim().split('\n'); + lines.forEach(function (line) { + console.log(prefix + line); + }); + }; + + this.ffmpeg.stdout.on('data', (data: Buffer) => { + logBuffer(data, '[ffmpeg:stdout] '); + }); + + this.ffmpeg.stderr.on('data', (data: Buffer) => { + logBuffer(data, '[ffmpeg:error] '); + }); + + this.ffmpeg.on('close', (code: number, signal: string | unknown) => { + if (code !== null) { + console.log(`[ffmpeg:stdout] exited with code ${code}: ${videoPath}`); + } + if (signal !== null) { + console.log(`[ffmpeg:stdout] received signal ${signal}: ${videoPath}`); + } + }); + } + } +} diff --git a/apps/desktop/e2e/scripts/init-repositories.sh b/apps/desktop/e2e/scripts/init-repositories.sh index 9cfe1b4f3..312574566 100755 --- a/apps/desktop/e2e/scripts/init-repositories.sh +++ b/apps/desktop/e2e/scripts/init-repositories.sh @@ -8,6 +8,12 @@ CLI=${1:?The first argument is the GitButler CLI} # Convert to absolute path CLI=$(realpath "$CLI") +function setGitDefaults() { + git config user.email "test@example.com" + git config user.name "Test User" + git config init.defaultBranch master +} + function tick() { if test -z "${tick+set}"; then tick=1675176957 @@ -20,7 +26,9 @@ function tick() { } tick -mkdir "$TEMP_DIR" +if [ ! -d "$TEMP_DIR" ]; then + mkdir "$TEMP_DIR" +fi cd "$TEMP_DIR" git init remote @@ -28,9 +36,7 @@ git init remote ( cd remote - git config user.email "test@example.com" - git config user.name "Test User" - git config init.defaultBranch master + setGitDefaults echo first >file git add . && git commit -m "init" diff --git a/apps/desktop/e2e/videos/.keep b/apps/desktop/e2e/videos/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/desktop/wdio.conf.ts b/apps/desktop/wdio.conf.ts index 222c061c6..55c222ff1 100644 --- a/apps/desktop/wdio.conf.ts +++ b/apps/desktop/wdio.conf.ts @@ -1,8 +1,10 @@ -import { spawn, ChildProcess } from 'node:child_process'; +import { TestRecorder } from './e2e/record.js'; +import { spawn, type ChildProcess } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; -import type { Options } from '@wdio/types'; +import type { Options, Frameworks } from '@wdio/types'; +const videoRecorder = new TestRecorder(); let tauriDriver: ChildProcess; export const config: Options.WebdriverIO = { @@ -37,6 +39,15 @@ export const config: Options.WebdriverIO = { connectionRetryTimeout: 120000, connectionRetryCount: 0, + beforeTest: function (test: Frameworks.Test) { + const videoPath = path.join(import.meta.dirname, '/e2e/videos'); + videoRecorder.start(test, videoPath); + }, + + afterTest: function () { + videoRecorder.stop(); + }, + // ensure we are running `tauri-driver` before the session starts so that we can proxy the webdriver requests beforeSession: () => (tauriDriver = spawn(path.resolve(os.homedir(), '.cargo', 'bin', 'tauri-driver'), [], {