Skip to content

Commit 66f8dde

Browse files
refactor: extract composite actions and consolidate test mocks (task-336.3)
- Add .github/actions/resolve-release-tag: bash+pwsh composite action; replace 4 inline blocks in release.yml - Add .github/actions/setup-frontend: setup-node@v6+npm-ci composite action; replace 3 inline blocks in test.yml/test-local.yml - Add taskfiles/tauri.yml _deps-base-build internal task; replace 7 inlined dep triples - Add app/frontend/__tests__/mocks/tauri.js and mocks/api.js shared mock factories - Update player.props.test.js, playback-regression.test.js, setup-player-mocks.js to import from mocks/
1 parent 3470c24 commit 66f8dde

13 files changed

Lines changed: 249 additions & 209 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: 'Resolve Release Tag'
2+
description: 'Resolve the release tag and export RELEASE_TAG to the environment. Supports bash and pwsh.'
3+
4+
inputs:
5+
event-name:
6+
description: 'github.event_name from the calling workflow'
7+
required: true
8+
release-tag:
9+
description: 'The release-tag workflow input (defaults to "latest")'
10+
required: false
11+
default: 'latest'
12+
release-tag-name:
13+
description: 'github.event.release.tag_name for release events'
14+
required: false
15+
default: ''
16+
github-token:
17+
description: 'GitHub token for gh CLI'
18+
required: true
19+
20+
runs:
21+
using: 'composite'
22+
steps:
23+
- name: Resolve release tag (bash)
24+
if: runner.os != 'Windows'
25+
shell: bash
26+
env:
27+
GH_TOKEN: ${{ inputs.github-token }}
28+
run: |
29+
if [ "${{ inputs.event-name }}" = "release" ]; then
30+
tag="${{ inputs.release-tag-name }}"
31+
elif [ "${{ inputs.release-tag }}" != "latest" ] && [ -n "${{ inputs.release-tag }}" ]; then
32+
tag="${{ inputs.release-tag }}"
33+
else
34+
tag="$(gh release view --json tagName -q .tagName)"
35+
fi
36+
echo "RELEASE_TAG=$tag" >> "$GITHUB_ENV"
37+
echo "Resolved release tag: $tag"
38+
39+
- name: Resolve release tag (pwsh)
40+
if: runner.os == 'Windows'
41+
shell: pwsh
42+
env:
43+
GH_TOKEN: ${{ inputs.github-token }}
44+
run: |
45+
if ("${{ inputs.event-name }}" -eq "release") {
46+
$tag = "${{ inputs.release-tag-name }}"
47+
} elseif ("${{ inputs.release-tag }}" -ne "latest" -and "${{ inputs.release-tag }}" -ne "") {
48+
$tag = "${{ inputs.release-tag }}"
49+
} else {
50+
$tag = (gh release view --json tagName -q .tagName)
51+
}
52+
echo "RELEASE_TAG=$tag" >> $env:GITHUB_ENV
53+
Write-Host "Resolved release tag: $tag"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: 'Setup Frontend'
2+
description: 'Install Node.js and run npm ci for the frontend'
3+
4+
inputs:
5+
node-version:
6+
description: 'Node.js version'
7+
required: false
8+
default: '20'
9+
cache:
10+
description: 'Package manager for caching (npm/yarn/pnpm, or empty to disable)'
11+
required: false
12+
default: ''
13+
cache-dependency-path:
14+
description: 'Path to lockfile for cache key (only used when cache is set)'
15+
required: false
16+
default: 'app/frontend/package-lock.json'
17+
18+
runs:
19+
using: 'composite'
20+
steps:
21+
- name: Setup Node.js
22+
uses: actions/setup-node@v6
23+
with:
24+
node-version: ${{ inputs.node-version }}
25+
cache: ${{ inputs.cache }}
26+
cache-dependency-path: ${{ inputs.cache-dependency-path }}
27+
28+
- name: Install frontend dependencies
29+
shell: bash
30+
working-directory: ./app/frontend
31+
run: npm ci

.github/workflows/release.yml

Lines changed: 24 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,12 @@ jobs:
5252
- uses: actions/checkout@v6
5353

5454
- name: Resolve release tag
55-
env:
56-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57-
run: |
58-
if [ "${{ github.event_name }}" = "release" ]; then
59-
tag="${{ github.event.release.tag_name }}"
60-
elif [ "${{ inputs.release-tag }}" != "latest" ] && [ -n "${{ inputs.release-tag }}" ]; then
61-
tag="${{ inputs.release-tag }}"
62-
else
63-
tag="$(gh release view --json tagName -q .tagName)"
64-
fi
65-
echo "RELEASE_TAG=$tag" >> "$GITHUB_ENV"
66-
echo "Resolved release tag: $tag"
55+
uses: ./.github/actions/resolve-release-tag
56+
with:
57+
event-name: ${{ github.event_name }}
58+
release-tag: ${{ inputs.release-tag }}
59+
release-tag-name: ${{ github.event.release.tag_name }}
60+
github-token: ${{ secrets.GITHUB_TOKEN }}
6761

6862
- name: Setup Tauri build environment
6963
uses: ./.github/actions/setup-tauri-build
@@ -147,18 +141,12 @@ jobs:
147141
- uses: actions/checkout@v6
148142

149143
- name: Resolve release tag
150-
env:
151-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
152-
run: |
153-
if [ "${{ github.event_name }}" = "release" ]; then
154-
tag="${{ github.event.release.tag_name }}"
155-
elif [ "${{ inputs.release-tag }}" != "latest" ] && [ -n "${{ inputs.release-tag }}" ]; then
156-
tag="${{ inputs.release-tag }}"
157-
else
158-
tag="$(gh release view --json tagName -q .tagName)"
159-
fi
160-
echo "RELEASE_TAG=$tag" >> "$GITHUB_ENV"
161-
echo "Resolved release tag: $tag"
144+
uses: ./.github/actions/resolve-release-tag
145+
with:
146+
event-name: ${{ github.event_name }}
147+
release-tag: ${{ inputs.release-tag }}
148+
release-tag-name: ${{ github.event.release.tag_name }}
149+
github-token: ${{ secrets.GITHUB_TOKEN }}
162150

163151
- name: Setup Docker builder
164152
uses: useblacksmith/setup-docker-builder@v1
@@ -202,19 +190,12 @@ jobs:
202190
- uses: actions/checkout@v6
203191

204192
- name: Resolve release tag
205-
shell: pwsh
206-
env:
207-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
208-
run: |
209-
if ("${{ github.event_name }}" -eq "release") {
210-
$tag = "${{ github.event.release.tag_name }}"
211-
} elseif ("${{ inputs.release-tag }}" -ne "latest" -and "${{ inputs.release-tag }}" -ne "") {
212-
$tag = "${{ inputs.release-tag }}"
213-
} else {
214-
$tag = (gh release view --json tagName -q .tagName)
215-
}
216-
echo "RELEASE_TAG=$tag" >> $env:GITHUB_ENV
217-
Write-Host "Resolved release tag: $tag"
193+
uses: ./.github/actions/resolve-release-tag
194+
with:
195+
event-name: ${{ github.event_name }}
196+
release-tag: ${{ inputs.release-tag }}
197+
release-tag-name: ${{ github.event.release.tag_name }}
198+
github-token: ${{ secrets.GITHUB_TOKEN }}
218199

219200
- name: Setup Tauri build environment
220201
uses: ./.github/actions/setup-tauri-build
@@ -322,18 +303,12 @@ jobs:
322303
token: ${{ secrets.GITHUB_TOKEN }}
323304

324305
- name: Resolve release tag
325-
env:
326-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
327-
run: |
328-
if [ "${{ github.event_name }}" = "release" ]; then
329-
tag="${{ github.event.release.tag_name }}"
330-
elif [ "${{ inputs.release-tag }}" != "latest" ] && [ -n "${{ inputs.release-tag }}" ]; then
331-
tag="${{ inputs.release-tag }}"
332-
else
333-
tag="$(gh release view --json tagName -q .tagName)"
334-
fi
335-
echo "RELEASE_TAG=$tag" >> "$GITHUB_ENV"
336-
echo "Resolved release tag: $tag"
306+
uses: ./.github/actions/resolve-release-tag
307+
with:
308+
event-name: ${{ github.event_name }}
309+
release-tag: ${{ inputs.release-tag }}
310+
release-tag-name: ${{ github.event.release.tag_name }}
311+
github-token: ${{ secrets.GITHUB_TOKEN }}
337312

338313
- name: Update download table
339314
env:

.github/workflows/test-local.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,12 @@ jobs:
4040
- name: Checkout code
4141
uses: actions/checkout@v6
4242

43-
- name: Setup Node.js
44-
uses: actions/setup-node@v5
43+
- name: Setup frontend
44+
uses: ./.github/actions/setup-frontend
4545
with:
46-
node-version: '20'
4746
cache: 'npm'
4847
cache-dependency-path: app/frontend/package-lock.json
4948

50-
- name: Install frontend dependencies
51-
working-directory: ./app/frontend
52-
run: npm ci
53-
5449
- name: Run Vitest with coverage
5550
working-directory: ./app/frontend
5651
run: npm run test:coverage

.github/workflows/test.yml

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -160,17 +160,12 @@ jobs:
160160
steps:
161161
- uses: actions/checkout@v6
162162

163-
- name: Setup Node.js
164-
uses: actions/setup-node@v6
163+
- name: Setup frontend
164+
uses: ./.github/actions/setup-frontend
165165
with:
166-
node-version: '20'
167166
cache: 'npm'
168167
cache-dependency-path: app/frontend/package-lock.json
169168

170-
- name: Install frontend dependencies
171-
working-directory: ./app/frontend
172-
run: npm ci
173-
174169
- name: Run Vitest with coverage
175170
working-directory: ./app/frontend
176171
run: npm run test:coverage
@@ -210,14 +205,8 @@ jobs:
210205
steps:
211206
- uses: actions/checkout@v6
212207

213-
- name: Setup Node.js
214-
uses: actions/setup-node@v6
215-
with:
216-
node-version: '20'
217-
218-
- name: Install frontend dependencies
219-
working-directory: ./app/frontend
220-
run: npm ci
208+
- name: Setup frontend
209+
uses: ./.github/actions/setup-frontend
221210

222211
- name: Install Playwright browsers
223212
working-directory: ./app/frontend
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { vi } from 'vitest';
2+
3+
/**
4+
* @param {Record<string,unknown>} [overrides] - additional api namespaces to merge in
5+
*/
6+
export function createApiMock(overrides = {}) {
7+
return {
8+
api: {
9+
favorites: {
10+
check: vi.fn().mockResolvedValue({ is_favorite: false }),
11+
add: vi.fn().mockResolvedValue({}),
12+
remove: vi.fn().mockResolvedValue({}),
13+
},
14+
library: {
15+
getArtwork: vi.fn().mockResolvedValue(null),
16+
updatePlayCount: vi.fn().mockResolvedValue({}),
17+
},
18+
lastfm: {
19+
getSettings: vi.fn().mockResolvedValue({ enabled: false, authenticated: false, scrobble_threshold: 90 }),
20+
updateNowPlaying: vi.fn().mockResolvedValue({ status: 'disabled' }),
21+
scrobble: vi.fn().mockResolvedValue({ status: 'disabled' }),
22+
},
23+
...overrides,
24+
},
25+
};
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { vi } from 'vitest';
2+
3+
const VOID_CMDS = new Set([
4+
'audio_seek',
5+
'audio_set_volume',
6+
'audio_stop',
7+
'audio_play',
8+
'audio_pause',
9+
'queue_clear',
10+
]);
11+
12+
const DEFAULT_RETURNS = {
13+
audio_get_status: { volume: 1.0 },
14+
audio_load: { duration_ms: 180000 },
15+
};
16+
17+
/**
18+
* @param {{ invokeReturns?: Record<string,unknown>, voidCmds?: string[] }} [opts]
19+
*/
20+
export function createTauriMock({ invokeReturns = {}, voidCmds = [] } = {}) {
21+
const returns = { ...DEFAULT_RETURNS, ...invokeReturns };
22+
const voidSet = new Set([...VOID_CMDS, ...voidCmds]);
23+
return {
24+
__TAURI__: {
25+
core: {
26+
invoke: vi.fn((cmd) => {
27+
if (voidSet.has(cmd)) return Promise.resolve();
28+
return Promise.resolve(returns[cmd] ?? {});
29+
}),
30+
},
31+
event: {
32+
listen: vi.fn(() => Promise.resolve(() => {})),
33+
},
34+
},
35+
};
36+
}

app/frontend/__tests__/playback-regression.test.js

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,28 @@
1111
*/
1212

1313
import { describe, it, expect, beforeEach, vi } from 'vitest';
14+
import { createTauriMock } from './mocks/tauri.js';
1415

15-
// Mock Tauri IPC — must be set before module imports
16-
global.window = {
17-
__TAURI__: {
18-
core: {
19-
invoke: vi.fn((cmd) => {
20-
if (cmd === 'audio_get_status') return Promise.resolve({ volume: 1.0, state: 'Stopped' });
21-
if (cmd === 'queue_get') return Promise.resolve({ items: [], current_index: -1 });
22-
if (cmd === 'queue_clear') return Promise.resolve();
23-
if (cmd === 'queue_get_playback_state') return Promise.resolve({ shuffle: false, loop: 'none', current_index: -1 });
24-
return Promise.resolve({});
25-
}),
26-
},
27-
event: {
28-
listen: vi.fn(() => Promise.resolve(() => {})),
29-
},
16+
global.window = createTauriMock({
17+
invokeReturns: {
18+
audio_get_status: { volume: 1.0, state: 'Stopped' },
19+
queue_get: { items: [], current_index: -1 },
20+
queue_get_playback_state: { shuffle: false, loop: 'none', current_index: -1 },
3021
},
31-
};
22+
});
3223

33-
vi.mock('../js/api.js', () => ({
34-
api: {
35-
favorites: {
36-
check: vi.fn().mockResolvedValue({ is_favorite: false }),
37-
add: vi.fn().mockResolvedValue({}),
38-
remove: vi.fn().mockResolvedValue({}),
39-
},
40-
library: {
41-
getArtwork: vi.fn().mockResolvedValue(null),
42-
updatePlayCount: vi.fn().mockResolvedValue({}),
43-
},
44-
lastfm: {
45-
getSettings: vi.fn().mockResolvedValue({ enabled: false, authenticated: false, scrobble_threshold: 90 }),
46-
updateNowPlaying: vi.fn().mockResolvedValue({ status: 'disabled' }),
47-
scrobble: vi.fn().mockResolvedValue({ status: 'disabled' }),
48-
},
24+
vi.mock('../js/api.js', async () => {
25+
const { createApiMock } = await import('./mocks/api.js');
26+
return createApiMock({
4927
queue: {
5028
get: vi.fn().mockResolvedValue({ items: [], current_index: -1 }),
5129
clear: vi.fn().mockResolvedValue({}),
5230
add: vi.fn().mockResolvedValue({}),
5331
remove: vi.fn().mockResolvedValue({}),
5432
reorder: vi.fn().mockResolvedValue({}),
5533
},
56-
},
57-
}));
34+
});
35+
});
5836

5937
import { createPlayerStore } from '../js/stores/player.js';
6038
import { createQueueStore } from '../js/stores/queue.js';

0 commit comments

Comments
 (0)