Skip to content

Commit 100d67d

Browse files
committed
Add playwright component test for UI package component
A component test for VirtualBranch.svelte.
1 parent 2eead4c commit 100d67d

File tree

14 files changed

+1043
-436
lines changed

14 files changed

+1043
-436
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Component Tests Playwright
2+
on:
3+
push:
4+
branches:
5+
- master
6+
pull_request:
7+
branches: [master]
8+
workflow_dispatch:
9+
inputs:
10+
sha:
11+
type: string
12+
required: false
13+
description: Target SHA
14+
15+
jobs:
16+
test:
17+
name: ct-playwright
18+
runs-on: ubuntu-latest
19+
container:
20+
image: ghcr.io/gitbutlerapp/ci-base-image:latest
21+
timeout-minutes: 10
22+
steps:
23+
- uses: actions/checkout@v5
24+
with:
25+
persist-credentials: false
26+
if: ${{ github.event_name != 'workflow_dispatch' }}
27+
- uses: actions/checkout@v5
28+
if: ${{ github.event_name == 'workflow_dispatch' }}
29+
with:
30+
persist-credentials: false
31+
ref: ${{ github.event.inputs.sha }}
32+
- name: Setup node environment
33+
uses: ./.github/actions/init-env-node
34+
if: ${{ github.ref != 'refs/heads/master' }}
35+
- id: get_playwright_version
36+
uses: eviden-actions/get-playwright-version@v1
37+
if: ${{ github.ref != 'refs/heads/master' }}
38+
- name: Cache playwright binaries
39+
if: ${{ github.ref != 'refs/heads/master' }}
40+
uses: actions/cache@v4
41+
id: playwright-cache
42+
with:
43+
path: |
44+
~/.cache/ct-playwright
45+
key: ${{ runner.os }}-playwright-${{ steps.get_playwright_version.outputs.playwright-version }}
46+
- run: cd packages/ui && pnpm exec playwright install --with-deps
47+
if: ${{ steps.playwright-cache.outputs.cache-hit != 'true' && github.ref != 'refs/heads/master' }}
48+
- name: Run Playwright tests
49+
run: pnpm exec turbo run test:ct
50+
if: ${{ github.ref != 'refs/heads/master' }}
51+
- uses: actions/upload-artifact@v5
52+
if: ${{ !cancelled() }}
53+
with:
54+
name: playwright-report
55+
path: |
56+
./packages/ui/playwright-report/
57+
./packages/ui/test-results/
58+
retention-days: 30

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ storybook-static
5353

5454
# Cypress
5555
**/cypress/screenshots
56+
packages/ui/tests/.cache
5657

5758
# Vercel
5859
.vercel

apps/desktop/src/components/codegen/CodegenMessages.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,8 @@
464464
{:else}
465465
<VirtualList
466466
grow
467+
tail
467468
stickToBottom
468-
initialPosition="bottom"
469469
items={formattedMessages}
470470
batchSize={1}
471471
visibility={$userSettings.scrollbarVisibilityState}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"package": "turbo run package",
1818
"test": "turbo run test --no-daemon",
1919
"test:watch": "pnpm --filter @gitbutler/desktop run test:watch",
20+
"test:ct": "turbo run --filter @gitbutler/ui run test:ct",
2021
"test:e2e:playwright": "pnpm --filter @gitbutler/e2e run test:e2e:playwright",
2122
"test:e2e": "pnpm --filter @gitbutler/e2e run test:e2e",
2223
"test:e2e:blackbox": "pnpm --filter @gitbutler/e2e run test:e2e:blackbox",
@@ -51,7 +52,7 @@
5152
"eslint-import-resolver-next": "^0.6.0",
5253
"eslint-import-resolver-typescript": "^4.4.4",
5354
"eslint-plugin-import-x": "4.16.1",
54-
"eslint-plugin-storybook": "9.0.18",
55+
"eslint-plugin-storybook": "10.0.2",
5556
"eslint-plugin-svelte": "3.11.0",
5657
"globals": "^15.15.0",
5758
"postcss": "catalog:postcss",

packages/ui/package.json

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"storybook:build": "storybook build",
2525
"test": "vitest run --mode development",
2626
"test:watch": "vitest --watch --mode development",
27+
"test:ct": "playwright test -c playwright-ct.config.ts",
28+
"test:ct:ui": "playwright test -c playwright-ct.config.ts --ui",
2729
"playwright:install": "playwright install --with-deps chromium"
2830
},
2931
"devDependencies": {
@@ -43,15 +45,16 @@
4345
"@codemirror/language": "^6.11.2",
4446
"@codemirror/legacy-modes": "^6.5.1",
4547
"@csstools/postcss-bundler": "^2.0.8",
48+
"@gitbutler/design-core": "^1.2.6",
4649
"@lezer/common": "^1.2.3",
4750
"@lezer/highlight": "^1.2.1",
51+
"@playwright/experimental-ct-svelte": "^1.56.1",
4852
"@replit/codemirror-lang-svelte": "^6.0.0",
49-
"@storybook/addon-docs": "^9.1.1",
50-
"@storybook/addon-links": "^9.1.1",
51-
"@storybook/addon-svelte-csf": "5.0.7",
52-
"@storybook/addon-vitest": "9.0.18",
53-
"@storybook/experimental-addon-test": "^8.6.14",
54-
"@storybook/sveltekit": "^9.1.1",
53+
"@storybook/addon-docs": "^10.0.2",
54+
"@storybook/addon-links": "^10.0.2",
55+
"@storybook/addon-svelte-csf": "5.0.10",
56+
"@storybook/addon-vitest": "10.0.2",
57+
"@storybook/sveltekit": "^10.0.2",
5558
"@sveltejs/adapter-static": "catalog:svelte",
5659
"@sveltejs/package": "catalog:svelte",
5760
"@sveltejs/vite-plugin-svelte": "catalog:svelte",
@@ -65,17 +68,16 @@
6568
"diff-match-patch": "^1.0.5",
6669
"isomorphic-dompurify": "^2.26.0",
6770
"marked": "catalog:",
68-
"playwright": "1.54.1",
71+
"playwright": "1.56.1",
6972
"postcss": "catalog:postcss",
7073
"postcss-cli": "catalog:postcss",
7174
"postcss-nesting": "catalog:postcss",
7275
"postcss-pxtorem": "catalog:postcss",
7376
"rimraf": "^6.0.1",
74-
"storybook": "^9.1.1",
77+
"storybook": "^10.0.2",
7578
"svelte-check": "catalog:svelte",
7679
"vite": "catalog:",
77-
"vitest": "catalog:",
78-
"@gitbutler/design-core": "^1.2.6"
80+
"vitest": "catalog:"
7981
},
8082
"peerDependencies": {
8183
"@sveltejs/kit": "catalog:svelte",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { defineConfig, devices } from '@playwright/experimental-ct-svelte';
2+
import { resolve } from 'path';
3+
4+
/**
5+
* See https://playwright.dev/docs/test-configuration.
6+
*/
7+
export default defineConfig({
8+
testDir: './tests',
9+
/* Maximum time one test can run for. */
10+
timeout: 10 * 1000,
11+
/* Run tests in files in parallel */
12+
fullyParallel: true,
13+
/* Fail the build on CI if you accidentally left test.only in the source code. */
14+
forbidOnly: !!process.env.CI,
15+
/* Retry on CI only */
16+
retries: process.env.CI ? 2 : 0,
17+
/* Opt out of parallel tests on CI. */
18+
workers: process.env.CI ? 1 : undefined,
19+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
20+
reporter: 'html',
21+
22+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
23+
use: {
24+
/* Port to use for Playwright component endpoint. */
25+
ctPort: 3100,
26+
27+
/* Template for component tests */
28+
ctTemplateDir: 'tests',
29+
30+
ctViteConfig: {
31+
resolve: {
32+
alias: {
33+
$components: resolve('./src/lib/components'),
34+
$lib: resolve('./src/lib')
35+
}
36+
}
37+
}
38+
},
39+
40+
/* Configure projects for major browsers */
41+
projects: [
42+
{
43+
name: 'chromium',
44+
use: { ...devices['Desktop Chrome'] }
45+
}
46+
]
47+
});

packages/ui/src/lib/components/VirtualList.svelte

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
/** Handler for when scroll has reached with a margin of the bottom. */
3535
onloadmore?: () => Promise<void>;
3636
grow?: boolean;
37-
/** Whether to initialize scroll position at top or bottom. */
38-
initialPosition?: 'top' | 'bottom';
37+
/** Whether to initialize scroll position at top or bottom (tail). */
38+
tail?: boolean;
3939
/** Auto-scroll to bottom when new items are added (useful for chat). */
4040
stickToBottom?: boolean;
4141
visibility: ScrollbarVisilitySettings;
@@ -59,7 +59,7 @@
5959
padding,
6060
visibility,
6161
defaultHeight,
62-
initialPosition = 'top',
62+
tail,
6363
stickToBottom = false
6464
}: Props = $props();
6565
@@ -79,8 +79,8 @@
7979
8080
// Virtual scrolling state
8181
let visibleRange = $state({
82-
start: initialPosition === 'bottom' ? Infinity : 0,
83-
end: initialPosition === 'bottom' ? Infinity : 0
82+
start: tail ? Infinity : 0,
83+
end: tail ? Infinity : 0
8484
});
8585
8686
// An array mapping items to element heights
@@ -223,15 +223,15 @@
223223
224224
let isRecalculating = false;
225225
226-
async function recalculateVisibleRange(isScroll?: boolean) {
226+
async function recalculateVisibleRange() {
227227
if (!viewport || !visibleRowElements) return;
228228
if (isRecalculating) return; // One at a time.
229229
230230
isRecalculating = true;
231231
heightMap.length = itemChunks.length;
232232
233233
// Handle bottom initialization
234-
if (!hasInitialized && initialPosition === 'bottom') {
234+
if (!hasInitialized && tail) {
235235
// Start from the last chunk and work backwards
236236
visibleRange.end = itemChunks.length;
237237
offset.bottom = 0;
@@ -249,15 +249,16 @@
249249
}, 20);
250250
} else {
251251
await tick();
252-
const previousDistanceFromBottom = lastDistanceFromBottom;
253252
const previousStartIndex = visibleRange.start;
254253
255254
visibleRange = {
256255
start: await calculateVisibleStartIndex(),
257256
end: await calculateVisibleEndIndex()
258257
};
259-
offset.bottom = calculateHeightSum(visibleRange.end, heightMap.length);
260-
offset.top = calculateHeightSum(0, visibleRange.start);
258+
offset = {
259+
bottom: calculateHeightSum(visibleRange.end, heightMap.length),
260+
top: calculateHeightSum(0, visibleRange.start)
261+
};
261262
262263
if (visibleRange.start < previousStartIndex) {
263264
await tick();
@@ -269,20 +270,6 @@
269270
}
270271
}
271272
await tick();
272-
273-
if (
274-
!isScroll &&
275-
stickToBottom &&
276-
previousDistanceFromBottom < STICKY_DISTANCE &&
277-
getDistanceFromBottom() > previousDistanceFromBottom
278-
) {
279-
setTimeout(() => {
280-
if (!viewport) return;
281-
viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' });
282-
}, 0);
283-
await tick();
284-
}
285-
286273
totalHeight = calculateHeightSum(0, heightMap.length);
287274
}
288275
@@ -302,7 +289,20 @@
302289
$effect(() => {
303290
if (viewport) {
304291
visibleRowElements = viewport.getElementsByClassName('list-row');
305-
resizeObserver = new ResizeObserver(() => untrack(() => recalculateVisibleRange()));
292+
resizeObserver = new ResizeObserver(() =>
293+
untrack(() => {
294+
// recalculateVisibleRange();
295+
const hasGrown = getDistanceFromBottom() > lastDistanceFromBottom;
296+
if (hasGrown && stickToBottom && lastDistanceFromBottom < STICKY_DISTANCE) {
297+
if (viewport) {
298+
viewport.scrollTo({
299+
top: viewport.scrollHeight,
300+
behavior: hasInitialized ? 'smooth' : 'instant'
301+
});
302+
}
303+
}
304+
})
305+
);
306306
return () => {
307307
resizeObserver?.disconnect();
308308
};
@@ -333,8 +333,7 @@
333333
if (!viewport) return;
334334
hasNewUnreadItems = false;
335335
visibleRange = { end: itemChunks.length, start: itemChunks.length - 1 };
336-
offset.bottom = 0;
337-
offset.top = calculateHeightSum(0, visibleRange.start);
336+
offset = { bottom: 0, top: calculateHeightSum(0, visibleRange.start) };
338337
lastDistanceFromBottom = 0;
339338
await tick();
340339
viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'instant' });
@@ -352,14 +351,17 @@
352351
// bottom of the chat bubble.
353352
setTimeout(() => {
354353
if (!viewport) return;
355-
viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' });
356-
}, 1000);
354+
viewport.scrollTo({
355+
top: viewport.scrollHeight,
356+
behavior: hasInitialized ? 'smooth' : 'instant'
357+
});
358+
}, 0);
357359
});
358360
} else if (items) {
359361
untrack(() => {
360362
const hadNewItems = items.length > previousItemsLength && items.length > visibleRange.end;
361363
recalculateVisibleRange();
362-
if (initialPosition === 'bottom' && hadNewItems) {
364+
if (tail && hadNewItems) {
363365
hasNewUnreadItems = true;
364366
}
365367
});
@@ -371,7 +373,7 @@
371373
<ScrollableContainer
372374
bind:viewportHeight
373375
bind:viewport
374-
onscroll={() => recalculateVisibleRange(true)}
376+
onscroll={() => recalculateVisibleRange()}
375377
wide={grow}
376378
whenToShow={visibility}
377379
{padding}

0 commit comments

Comments
 (0)