From 29b2b6b62f4d31a5f201fffc9432fdeed84d5bd7 Mon Sep 17 00:00:00 2001 From: Mattias Granlund Date: Mon, 2 Sep 2024 12:41:44 +0300 Subject: [PATCH] Drag & drop proof of concept E2E - native dnd is still problematic - uses injected script for simulating dnd --- apps/desktop/e2e/scripts/confirm-analytics.sh | 11 +++ apps/desktop/e2e/scripts/init.sh | 47 +++++++++++ apps/desktop/e2e/tests/add-project.spec.ts | 24 ++++-- apps/desktop/e2e/tests/drag-file.spec.ts | 38 +++++++++ apps/desktop/package.json | 1 + apps/desktop/src/lib/branch/BranchCard.svelte | 2 +- apps/desktop/src/lib/branch/BranchLane.svelte | 2 +- apps/desktop/src/lib/branch/Dropzones.svelte | 4 +- apps/desktop/src/lib/dragging/dropzone.ts | 2 +- apps/desktop/src/lib/dropzone/Dropzone.svelte | 3 + apps/desktop/src/lib/file/FileListItem.svelte | 24 +++--- packages/ui/src/lib/file/FileListItem.svelte | 1 + pnpm-lock.yaml | 77 +++++++++++++------ 13 files changed, 190 insertions(+), 46 deletions(-) create mode 100644 apps/desktop/e2e/scripts/confirm-analytics.sh create mode 100644 apps/desktop/e2e/scripts/init.sh create mode 100644 apps/desktop/e2e/tests/drag-file.spec.ts diff --git a/apps/desktop/e2e/scripts/confirm-analytics.sh b/apps/desktop/e2e/scripts/confirm-analytics.sh new file mode 100644 index 0000000000..7bc759a341 --- /dev/null +++ b/apps/desktop/e2e/scripts/confirm-analytics.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +DATA_DIR="$HOME/.local/share/com.gitbutler.app.dev" +if [ ! -d "$DATA_DIR" ]; then + echo "Creating data dir: $DATA_DIR" + mkdir -p $DATA_DIR +fi +echo "Confirming analytics" +echo '{"appAnalyticsConfirmed":true}' > $DATA_DIR/settings.json \ No newline at end of file diff --git a/apps/desktop/e2e/scripts/init.sh b/apps/desktop/e2e/scripts/init.sh new file mode 100644 index 0000000000..7ebe5dd42c --- /dev/null +++ b/apps/desktop/e2e/scripts/init.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +TEMP_DIR=${1:?The first argument is a temp dir} +# Convert to absolute path +TEMP_DIR=$(realpath "$TEMP_DIR") + + +CLI=$(realpath "../../target/debug/gitbutler-cli") + +DATA_DIR="$HOME/.local/share/com.gitbutler.app.dev" +if [ -d "$DATA_DIR" ]; then + rm -rf $DATA_DIR +fi + +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 + else + tick=$($tick + 60) + fi + GIT_COMMITTER_DATE="$tick +0100" + GIT_AUTHOR_DATE="$tick +0100" + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} +tick + +if [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" +fi +mkdir "$TEMP_DIR" + +( + cd "$TEMP_DIR" + git init remote + cd remote + setGitDefaults + echo first >file + git add . && git commit -m "init" +) \ No newline at end of file diff --git a/apps/desktop/e2e/tests/add-project.spec.ts b/apps/desktop/e2e/tests/add-project.spec.ts index bb625497fb..0524396742 100644 --- a/apps/desktop/e2e/tests/add-project.spec.ts +++ b/apps/desktop/e2e/tests/add-project.spec.ts @@ -1,10 +1,19 @@ -import { spawnAndLog, findAndClick, setElementValue } from '../utils.js'; +import { spawnAndLog, findAndClick, setElementValue } from '../utils'; + +const TEMP_DIR = '/tmp/gitbutler-add-project'; +const REPO_NAME = 'one-vbranch-on-integration'; describe('Project', () => { before(() => { spawnAndLog('bash', [ '-c', - './e2e/scripts/init-repositories.sh ../../target/debug/gitbutler-cli' + ` + source ./e2e/scripts/init.sh ${TEMP_DIR} + cd ${TEMP_DIR}; + git clone remote ${REPO_NAME} && cd ${REPO_NAME} + $CLI project -s dev add --switch-to-integration "$(git rev-parse --symbolic-full-name "@{u}")" + $CLI branch create virtual + ` ]); }); @@ -12,13 +21,12 @@ describe('Project', () => { await findAndClick('button[data-testid="analytics-continue"]'); const dirInput = await $('input[data-testid="test-directory-path"]'); - setElementValue(dirInput, '/tmp/gb-e2e-repos/one-vbranch-on-integration'); + setElementValue(dirInput, `${TEMP_DIR}/${REPO_NAME}`); - await findAndClick('button[data-testid="add-local-project"]'); - await findAndClick('button[data-testid="set-base-branch"]'); - await findAndClick('button[data-testid="accept-git-auth"]'); + await $('button[data-testid="add-local-project"]').then(async (b) => await b.click()); + await $('button[data-testid="set-base-branch"]').then(async (b) => await b.click()); + await $('button[data-testid="accept-git-auth"]').then(async (b) => await b.click()); - const workspaceButton = await $('button=Workspace'); - await expect(workspaceButton).toExist(); + await expect($('button=Workspace')).toExist(); }); }); diff --git a/apps/desktop/e2e/tests/drag-file.spec.ts b/apps/desktop/e2e/tests/drag-file.spec.ts new file mode 100644 index 0000000000..a5d2c4ceba --- /dev/null +++ b/apps/desktop/e2e/tests/drag-file.spec.ts @@ -0,0 +1,38 @@ +import { spawnAndLog } from '../utils'; +import { codeForSelectors as dragAndDrop } from 'html-dnd'; + +const TEMP_DIR = '/tmp/gitbutler-drag-files'; +const REPO_NAME = 'simple-drag-test'; + +describe('Drag', () => { + before(() => { + spawnAndLog('bash', [ + '-c', + ` + source ./e2e/scripts/init.sh ${TEMP_DIR} + bash ./e2e/scripts/confirm-analytics.sh + cd ${TEMP_DIR}; + git clone remote ${REPO_NAME} && cd ${REPO_NAME} + $CLI project -s dev add --switch-to-integration "$(git rev-parse --symbolic-full-name "@{u}")" + $CLI branch create virtual-one + $CLI branch create virtual-two + echo "hello world" > hello + ` + ]); + }); + + it('drag file from one lane to another', async () => { + const fileSelector = '[data-testid="file-helloworld.txt"]'; + const dropSelector = '[data-testid="virtual-two-files-dz"]'; + + const fileItem = await $(fileSelector); + const dropTarget = await $(dropSelector); + await fileItem.waitForDisplayed(); + await dropTarget.waitForDisplayed(); + + // The actual drop target can be different from the element with the `dropZone` directive.. + await driver.executeScript(dragAndDrop, [fileSelector, dropSelector + ' .dropzone-target']); + + await expect('[data-testid="branch-virtual-two"] [data-testid="file-hello"]').toBeDisplayed(); + }); +}); diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e556243ca1..8552acba85 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -71,6 +71,7 @@ "diff-match-patch": "^1.0.5", "fuse.js": "^7.0.0", "git-url-parse": "^14.0.0", + "html-dnd": "^1.2.1", "jsdom": "^24.1.1", "lscache": "^1.3.2", "marked": "^10.0.0", diff --git a/apps/desktop/src/lib/branch/BranchCard.svelte b/apps/desktop/src/lib/branch/BranchCard.svelte index 42e6c0edc1..6b9a161c5d 100644 --- a/apps/desktop/src/lib/branch/BranchCard.svelte +++ b/apps/desktop/src/lib/branch/BranchCard.svelte @@ -156,7 +156,7 @@ /> {:else if branch.commits.length === 0} - +
This is a new branch diff --git a/apps/desktop/src/lib/branch/BranchLane.svelte b/apps/desktop/src/lib/branch/BranchLane.svelte index 5130c49bb9..bec74c6726 100644 --- a/apps/desktop/src/lib/branch/BranchLane.svelte +++ b/apps/desktop/src/lib/branch/BranchLane.svelte @@ -117,7 +117,7 @@ }); -
+
{#await $selectedFile then [commitId, selected]} diff --git a/apps/desktop/src/lib/branch/Dropzones.svelte b/apps/desktop/src/lib/branch/Dropzones.svelte index d3550b68b2..7c63ad9ec4 100644 --- a/apps/desktop/src/lib/branch/Dropzones.svelte +++ b/apps/desktop/src/lib/branch/Dropzones.svelte @@ -11,9 +11,10 @@ interface Props { children: Snippet; + id?: string; } - const { children }: Props = $props(); + const { children, id }: Props = $props(); const actions = $derived(branchDragActionsFactory.build($branch)); @@ -42,6 +43,7 @@ accepts={actions.acceptBranchDrop.bind(actions)} ondrop={actions.onBranchDrop.bind(actions)} fillHeight + id={id ? id + '-files-dz' : undefined} > {@render children()} diff --git a/apps/desktop/src/lib/dragging/dropzone.ts b/apps/desktop/src/lib/dragging/dropzone.ts index d686b23512..5fefba1a7f 100644 --- a/apps/desktop/src/lib/dragging/dropzone.ts +++ b/apps/desktop/src/lib/dragging/dropzone.ts @@ -43,9 +43,9 @@ export class Dropzone { this.registerListeners(); // Mark the dropzone as active + this.activated = true; setTimeout(() => { this.configuration.onActivationStart(); - this.activated = true; }, 10); } diff --git a/apps/desktop/src/lib/dropzone/Dropzone.svelte b/apps/desktop/src/lib/dropzone/Dropzone.svelte index 49f0c481d9..a89628f3aa 100644 --- a/apps/desktop/src/lib/dropzone/Dropzone.svelte +++ b/apps/desktop/src/lib/dropzone/Dropzone.svelte @@ -5,6 +5,7 @@ interface Props { disabled?: boolean; fillHeight?: boolean; + id?: string; accepts: (data: any) => boolean; ondrop: (data: any) => Promise | void; overlay: Snippet<[{ hovered: boolean; activated: boolean }]>; @@ -14,6 +15,7 @@ const { disabled = false, fillHeight = false, + id, accepts, ondrop, overlay, @@ -53,6 +55,7 @@ target: '.dropzone-target' }} class:fill-height={fillHeight} + data-testid={id} class="dropzone-container" > {@render overlay({ hovered, activated })} diff --git a/apps/desktop/src/lib/file/FileListItem.svelte b/apps/desktop/src/lib/file/FileListItem.svelte index b813002810..d2e7dbaaa2 100644 --- a/apps/desktop/src/lib/file/FileListItem.svelte +++ b/apps/desktop/src/lib/file/FileListItem.svelte @@ -166,18 +166,18 @@ draggableEl && addAnimationEndListener(draggableEl); } - onDestroy(() => { - // Ensure any listeners are removed if the component is destroyed before animation ends - if (draggableEl && animationEndHandler) { - draggableEl.removeEventListener('animationend', animationEndHandler); - } - files.forEach((f) => { - const lockedElement = document.getElementById(`file-${f.id}`); - if (lockedElement && animationEndHandler) { - lockedElement.removeEventListener('animationend', animationEndHandler); - } - }); - }); + // onDestroy(() => { + // // Ensure any listeners are removed if the component is destroyed before animation ends + // if (draggableEl && animationEndHandler) { + // draggableEl.removeEventListener('animationend', animationEndHandler); + // } + // files.forEach((f) => { + // const lockedElement = document.getElementById(`file-${f.id}`); + // if (lockedElement && animationEndHandler) { + // lockedElement.removeEventListener('animationend', animationEndHandler); + // } + // }); + // }); }} oncontextmenu={async (e) => { if (fileIdSelection.has(file.id, $commit?.id)) { diff --git a/packages/ui/src/lib/file/FileListItem.svelte b/packages/ui/src/lib/file/FileListItem.svelte index 7a389c15a9..d9ffdd613a 100644 --- a/packages/ui/src/lib/file/FileListItem.svelte +++ b/packages/ui/src/lib/file/FileListItem.svelte @@ -68,6 +68,7 @@ tabindex="-1" {onclick} {onkeydown} + data-testid={'file-' + filePath} oncontextmenu={(e) => { if (oncontextmenu) { e.preventDefault(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2837cd749d..3d5c96ae9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,28 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + vite: + specifier: 5.2.13 + version: 5.2.13 + svelte: + '@sveltejs/adapter-static': + specifier: 3.0.2 + version: 3.0.2 + '@sveltejs/kit': + specifier: 2.5.18 + version: 2.5.18 + '@sveltejs/vite-plugin-svelte': + specifier: 3.1.1 + version: 3.1.1 + svelte: + specifier: 5.0.0-next.196 + version: 5.0.0-next.196 + svelte-check: + specifier: 3.8.4 + version: 3.8.4 + importers: .: @@ -234,6 +256,9 @@ importers: git-url-parse: specifier: ^14.0.0 version: 14.0.0 + html-dnd: + specifier: ^1.2.1 + version: 1.2.1 jsdom: specifier: ^24.1.1 version: 24.1.1 @@ -3964,6 +3989,10 @@ packages: resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} engines: {node: '>=16'} + get-stdin@4.0.1: + resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} + engines: {node: '>=0.10.0'} + get-stdin@9.0.0: resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} engines: {node: '>=12'} @@ -4163,6 +4192,9 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html-dnd@1.2.1: + resolution: {integrity: sha512-yO4Jg4TqFCEGVxvmAXZ8EpgQFF8YACuPiK2DXMgWGQ73bTdtZm6AgyFa/NUrmD3TyQXd4F2U3IoNWmR5cXzvgg==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -4895,6 +4927,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multiline@1.0.2: + resolution: {integrity: sha512-DGpmDIZKKQ+EVx0sh0757V6qlb+ouuByoC5CWH7J0bOd6KRM6ka6l9LGHWfe17OKxm+4AsLs1tgiK4vZIx66RQ==} + engines: {node: '>=0.10.0'} + mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -5912,6 +5948,11 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-indent@1.0.1: + resolution: {integrity: sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==} + engines: {node: '>=0.10.0'} + hasBin: true + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -6692,28 +6733,6 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} -catalogs: - default: - vite: - specifier: 5.2.13 - version: 5.2.13 - svelte: - '@sveltejs/adapter-static': - specifier: 3.0.2 - version: 3.0.2 - '@sveltejs/kit': - specifier: 2.5.18 - version: 2.5.18 - '@sveltejs/vite-plugin-svelte': - specifier: 3.1.1 - version: 3.1.1 - svelte: - specifier: 5.0.0-next.196 - version: 5.0.0-next.196 - svelte-check: - specifier: 3.8.4 - version: 3.8.4 - snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -11238,6 +11257,8 @@ snapshots: get-port@7.1.0: {} + get-stdin@4.0.1: {} + get-stdin@9.0.0: {} get-stream@5.2.0: @@ -11484,6 +11505,10 @@ snapshots: dependencies: lru-cache: 10.2.2 + html-dnd@1.2.1: + dependencies: + multiline: 1.0.2 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -12206,6 +12231,10 @@ snapshots: ms@2.1.3: {} + multiline@1.0.2: + dependencies: + strip-indent: 1.0.1 + mute-stream@1.0.0: {} nanoevents@9.0.0: {} @@ -13318,6 +13347,10 @@ snapshots: strip-final-newline@3.0.0: {} + strip-indent@1.0.1: + dependencies: + get-stdin: 4.0.1 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1