Skip to content

Commit 9d20926

Browse files
committed
Merge branch 'feature/up' into develop
2 parents 45a2154 + 1ff5cd4 commit 9d20926

22 files changed

Lines changed: 1689 additions & 101 deletions

.github/workflows/ci.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
push:
8+
branches:
9+
- master
10+
11+
env:
12+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
test:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: 22
29+
cache: npm
30+
31+
- name: Install Rust stable
32+
uses: dtolnay/rust-toolchain@stable
33+
34+
- name: Rust cache
35+
uses: swatinem/rust-cache@v2
36+
with:
37+
workspaces: src-tauri -> target
38+
39+
# Linux system dependencies for Tauri 2
40+
- name: Install Linux deps
41+
run: |
42+
sudo apt-get update
43+
sudo apt-get install -y \
44+
libwebkit2gtk-4.1-dev \
45+
libappindicator3-dev \
46+
librsvg2-dev \
47+
patchelf
48+
49+
- name: Install npm dependencies
50+
run: npm ci
51+
52+
- name: Install Playwright browsers
53+
run: npx playwright install --with-deps chromium
54+
55+
- name: Run all tests
56+
run: npm run test:all

.github/workflows/release.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,51 @@ permissions:
5555
contents: write
5656

5757
jobs:
58+
test:
59+
runs-on: ubuntu-latest
60+
permissions:
61+
contents: read
62+
63+
steps:
64+
- name: Checkout
65+
uses: actions/checkout@v4
66+
67+
- name: Setup Node.js
68+
uses: actions/setup-node@v4
69+
with:
70+
node-version: 22
71+
cache: npm
72+
73+
- name: Install Rust stable
74+
uses: dtolnay/rust-toolchain@stable
75+
76+
- name: Rust cache
77+
uses: swatinem/rust-cache@v2
78+
with:
79+
workspaces: src-tauri -> target
80+
81+
# Linux system dependencies for Tauri 2
82+
- name: Install Linux deps
83+
run: |
84+
sudo apt-get update
85+
sudo apt-get install -y \
86+
libwebkit2gtk-4.1-dev \
87+
libappindicator3-dev \
88+
librsvg2-dev \
89+
patchelf
90+
91+
- name: Install npm dependencies
92+
run: npm ci
93+
94+
- name: Install Playwright browsers
95+
run: npx playwright install --with-deps chromium
96+
97+
- name: Run all tests
98+
run: npm run test:all
99+
58100
release:
101+
needs: test
102+
59103
strategy:
60104
fail-fast: false
61105
matrix:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ src/renderer/dist
2727

2828
tmp
2929
test/tmp
30+
playwright-report
31+
test-results
3032
plan.md
3133

3234
.env

e2e/data-transfer.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
clearMockCalls,
3+
expect,
4+
firstInvokeArg,
5+
getMockCalls,
6+
getMockState,
7+
test,
8+
} from './support/test'
9+
10+
test.describe('data transfer', () => {
11+
test('exports backup data through the settings menu', async ({ page }) => {
12+
await clearMockCalls(page)
13+
14+
await page.getByLabel('Settings').click()
15+
await page.getByRole('menuitem', { name: 'Export' }).click()
16+
17+
await expect
18+
.poll(async () => (await getMockCalls(page)).some((call) => call.cmd === 'export_data'))
19+
.toBe(true)
20+
})
21+
22+
test('imports backup data from a file', async ({ page }) => {
23+
await clearMockCalls(page)
24+
25+
await page.getByLabel('Settings').click()
26+
await page.getByRole('menuitem', { name: /^Import$/ }).click()
27+
28+
await expect(page.locator('[data-id="imported-local"]')).toContainText('Imported Backup')
29+
await expect(page.locator('[data-id="imported-folder"]')).toContainText('Imported Folder')
30+
await expect(page.locator('[data-id="imported-group"]')).toContainText('Imported Group')
31+
await expect
32+
.poll(async () => {
33+
const state = await getMockState(page)
34+
return state.list.map((item) => ({
35+
id: item.id,
36+
type: item.type,
37+
children: item.children?.map((child) => child.id),
38+
}))
39+
})
40+
.toEqual([
41+
{ id: 'imported-local', type: 'local', children: undefined },
42+
{ id: 'imported-folder', type: 'folder', children: ['imported-folder-child'] },
43+
{ id: 'imported-group', type: 'group', children: undefined },
44+
])
45+
46+
const calls = await getMockCalls(page)
47+
expect(calls.some((call) => call.cmd === 'import_data')).toBe(true)
48+
})
49+
50+
test('imports backup data from a URL', async ({ page }) => {
51+
await clearMockCalls(page)
52+
53+
const importUrl = 'https://example.test/swh_data.json'
54+
await page.getByLabel('Settings').click()
55+
await page.getByRole('menuitem', { name: 'Import from URL' }).click()
56+
57+
const dialog = page.getByRole('dialog')
58+
await expect(dialog.getByText('Import from URL')).toBeVisible()
59+
await dialog.locator('input').fill(importUrl)
60+
await dialog.getByRole('button', { name: 'OK' }).click()
61+
62+
await expect(page.locator('[data-id="imported-url"]')).toContainText('Imported From URL')
63+
await page.locator('[data-id="imported-url"]').click()
64+
await expect(page.getByText(importUrl)).toBeVisible()
65+
await expect
66+
.poll(async () => {
67+
const state = await getMockState(page)
68+
return state.list.map((item) => ({ id: item.id, type: item.type, url: item.url }))
69+
})
70+
.toEqual([
71+
{ id: 'imported-url', type: 'remote', url: importUrl },
72+
{ id: 'imported-url-local', type: 'local', url: undefined },
73+
])
74+
75+
const calls = await getMockCalls(page)
76+
const importCall = calls.find((call) => call.cmd === 'import_data_from_url')
77+
expect(importCall).toBeDefined()
78+
expect(firstInvokeArg(importCall!)).toBe(importUrl)
79+
})
80+
})

e2e/folder-choice-mode.spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { clearMockCalls, expect, findEntry, getMockCalls, getMockState, test } from './support/test'
2+
3+
test.describe('folder choice mode', () => {
4+
test('enforces single-choice mode for folder children while applying hosts', async ({ page }) => {
5+
await clearMockCalls(page)
6+
7+
const alpha = page.locator('[data-id="folder-single-alpha"]')
8+
const beta = page.locator('[data-id="folder-single-beta"]')
9+
await expect(alpha).toContainText('Single Alpha')
10+
await expect(beta).toContainText('Single Beta')
11+
12+
const alphaToggle = alpha.getByRole('switch')
13+
const betaToggle = beta.getByRole('switch')
14+
await expect(alphaToggle).toHaveAttribute('aria-checked', 'false')
15+
await expect(betaToggle).toHaveAttribute('aria-checked', 'false')
16+
17+
await alphaToggle.click()
18+
await expect
19+
.poll(async () => {
20+
const state = await getMockState(page)
21+
return {
22+
alphaOn: findEntry(state, 'folder-single-alpha')?.on,
23+
betaOn: findEntry(state, 'folder-single-beta')?.on,
24+
hasAlpha: state.systemHosts.includes('single-alpha.local'),
25+
hasBeta: state.systemHosts.includes('single-beta.local'),
26+
}
27+
})
28+
.toEqual({ alphaOn: true, betaOn: false, hasAlpha: true, hasBeta: false })
29+
30+
await betaToggle.click()
31+
await expect(alphaToggle).toHaveAttribute('aria-checked', 'false')
32+
await expect(betaToggle).toHaveAttribute('aria-checked', 'true')
33+
await expect
34+
.poll(async () => {
35+
const state = await getMockState(page)
36+
return {
37+
alphaOn: findEntry(state, 'folder-single-alpha')?.on,
38+
betaOn: findEntry(state, 'folder-single-beta')?.on,
39+
hasAlpha: state.systemHosts.includes('single-alpha.local'),
40+
hasBeta: state.systemHosts.includes('single-beta.local'),
41+
}
42+
})
43+
.toEqual({ alphaOn: false, betaOn: true, hasAlpha: false, hasBeta: true })
44+
45+
const applyCalls = (await getMockCalls(page)).filter(
46+
(call) => call.cmd === 'apply_hosts_selection',
47+
)
48+
expect(applyCalls).toHaveLength(2)
49+
})
50+
51+
test('allows multiple folder children to stay enabled while applying hosts', async ({ page }) => {
52+
await clearMockCalls(page)
53+
54+
const alpha = page.locator('[data-id="folder-multiple-alpha"]')
55+
const beta = page.locator('[data-id="folder-multiple-beta"]')
56+
await expect(alpha).toContainText('Multiple Alpha')
57+
await expect(beta).toContainText('Multiple Beta')
58+
59+
const alphaToggle = alpha.getByRole('switch')
60+
const betaToggle = beta.getByRole('switch')
61+
await expect(alphaToggle).toHaveAttribute('aria-checked', 'false')
62+
await expect(betaToggle).toHaveAttribute('aria-checked', 'false')
63+
64+
await alphaToggle.click()
65+
await expect
66+
.poll(async () => {
67+
const state = await getMockState(page)
68+
return {
69+
alphaOn: findEntry(state, 'folder-multiple-alpha')?.on,
70+
betaOn: findEntry(state, 'folder-multiple-beta')?.on,
71+
hasAlpha: state.systemHosts.includes('multiple-alpha.local'),
72+
hasBeta: state.systemHosts.includes('multiple-beta.local'),
73+
}
74+
})
75+
.toEqual({ alphaOn: true, betaOn: false, hasAlpha: true, hasBeta: false })
76+
77+
await betaToggle.click()
78+
await expect(alphaToggle).toHaveAttribute('aria-checked', 'true')
79+
await expect(betaToggle).toHaveAttribute('aria-checked', 'true')
80+
await expect
81+
.poll(async () => {
82+
const state = await getMockState(page)
83+
return {
84+
alphaOn: findEntry(state, 'folder-multiple-alpha')?.on,
85+
betaOn: findEntry(state, 'folder-multiple-beta')?.on,
86+
hasAlpha: state.systemHosts.includes('multiple-alpha.local'),
87+
hasBeta: state.systemHosts.includes('multiple-beta.local'),
88+
}
89+
})
90+
.toEqual({ alphaOn: true, betaOn: true, hasAlpha: true, hasBeta: true })
91+
92+
const applyCalls = (await getMockCalls(page)).filter(
93+
(call) => call.cmd === 'apply_hosts_selection',
94+
)
95+
expect(applyCalls).toHaveLength(2)
96+
})
97+
})

0 commit comments

Comments
 (0)