Skip to content

Commit 1c45b25

Browse files
fix(ci): add missing library count mocks, fix property test dedup, gate Playwright on tests
- Add /api/library/count mock to Playwright specs with inline library routes (error-states, library-playlist-features, sidebar) to match paginated loading added in cb6876d - Set totalTracks=0 in library-browser empty state test to match HTML condition on library.totalTracks - Fix queue property test newCount to deduplicate within playNextTracks input batch, not just against existing queue - Gate Playwright on deno-lint, rust, vitest-tests so it doesn't run when upstream jobs fail - Gate builds on all test jobs (rust, vitest, playwright) with !failure() && !cancelled() so skipped playwright doesn't block - Remove continue-on-error from vitest step - Add setup-test mode to composite action for cargo-nextest via binstall Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 23ec368 commit 1c45b25

8 files changed

Lines changed: 131 additions & 42 deletions

File tree

.github/actions/setup-tauri-build/action.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ inputs:
66
description: 'Node.js version'
77
default: '20'
88
mode:
9-
description: 'Setup mode: "full" installs everything for Tauri builds; "check" installs only Rust + Task for cargo check/clippy'
9+
description: 'Setup mode: "full" installs everything for Tauri builds; "check" installs only Rust + Task for cargo check/clippy; "test" adds cargo-nextest via binstall'
1010
default: 'full'
1111

1212
runs:
@@ -107,6 +107,11 @@ runs:
107107
shell: bash
108108
run: task ci:setup-check
109109

110+
- name: Setup CI environment (macOS/Linux, test)
111+
if: inputs.mode == 'test' && runner.os != 'Windows'
112+
shell: bash
113+
run: task ci:setup-test
114+
110115
- name: Install system dependencies (Windows)
111116
if: runner.os == 'Windows'
112117
shell: pwsh
@@ -146,7 +151,7 @@ runs:
146151
}
147152
148153
- name: Install cargo-binstall (Windows)
149-
if: inputs.mode == 'full' && runner.os == 'Windows'
154+
if: (inputs.mode == 'full' || inputs.mode == 'test') && runner.os == 'Windows'
150155
shell: pwsh
151156
run: |
152157
if (-not (Get-Command cargo-binstall -ErrorAction SilentlyContinue)) {
@@ -169,6 +174,11 @@ runs:
169174
shell: pwsh
170175
run: task ci:setup-check PINNED_RUST=${{ steps.rust-version-windows.outputs.toolchain }}
171176

177+
- name: Setup CI environment (Windows, test)
178+
if: inputs.mode == 'test' && runner.os == 'Windows'
179+
shell: pwsh
180+
run: task ci:setup-test PINNED_RUST=${{ steps.rust-version-windows.outputs.toolchain }}
181+
172182
- name: Windows Defender exclusions
173183
if: runner.os == 'Windows'
174184
shell: pwsh

.github/workflows/test.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
- name: Setup Tauri build environment
5050
uses: ./.github/actions/setup-tauri-build
5151
with:
52-
mode: check
52+
mode: test
5353

5454
- name: Cargo fmt check
5555
run: cargo fmt --all -- --check
@@ -122,7 +122,8 @@ jobs:
122122
name: Build (${{ matrix.platform }})
123123
# Gate on lint/format/test jobs so builds don't burn CI minutes
124124
# when basic checks fail.
125-
needs: [deno-lint, rust]
125+
needs: [deno-lint, rust, vitest-tests, playwright-tests]
126+
if: ${{ !failure() && !cancelled() }}
126127
strategy:
127128
fail-fast: false
128129
matrix:
@@ -220,7 +221,6 @@ jobs:
220221
- name: Run Vitest with coverage
221222
working-directory: ./app/frontend
222223
run: npm run test:coverage
223-
continue-on-error: true
224224

225225
- name: Upload coverage report
226226
if: always()
@@ -249,8 +249,8 @@ jobs:
249249
250250
playwright-tests:
251251
name: Playwright E2E Tests
252-
needs: [changes]
253-
if: needs.changes.outputs.frontend == 'true' || github.event_name == 'push'
252+
needs: [changes, deno-lint, rust, vitest-tests]
253+
if: ${{ !failure() && !cancelled() && (needs.changes.outputs.frontend == 'true' || github.event_name == 'push') }}
254254
runs-on: [macOS, ARM64]
255255
timeout-minutes: 20
256256

app/frontend/__tests__/queue.props.test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,13 @@ describe('Queue Store - Property-Based Tests', () => {
626626
});
627627

628628
const originalIds = tracks.map((t) => t.id);
629-
// playNextTracks uses move semantics — duplicates are moved, not rejected
629+
// playNextTracks uses move semantics — duplicates within the input are
630+
// also deduplicated, so count only unique new IDs
630631
const existingIds = new Set(originalIds);
631-
const newCount = playNextTracks.filter((t) => !existingIds.has(t.id)).length;
632+
const uniqueNewIds = new Set(
633+
playNextTracks.filter((t) => !existingIds.has(t.id)).map((t) => t.id),
634+
);
635+
const newCount = uniqueNewIds.size;
632636

633637
const promise = store.playNextTracks(playNextTracks);
634638
resolvePromise();

app/frontend/tests/error-states.spec.js

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
1-
import { test, expect } from '@playwright/test';
2-
import {
3-
waitForAlpine,
4-
getAlpineStore,
5-
setAlpineStoreProperty,
6-
} from './fixtures/helpers.js';
7-
import {
8-
createLibraryState,
9-
setupLibraryMocks,
10-
} from './fixtures/mock-library.js';
11-
import {
12-
createPlaylistState,
13-
setupPlaylistMocks,
14-
} from './fixtures/mock-playlists.js';
1+
import { expect, test } from '@playwright/test';
2+
import { getAlpineStore, setAlpineStoreProperty, waitForAlpine } from './fixtures/helpers.js';
3+
import { createLibraryState, setupLibraryMocks } from './fixtures/mock-library.js';
4+
import { createPlaylistState, setupPlaylistMocks } from './fixtures/mock-playlists.js';
155

166
/**
177
* Error States and Toast Notification Tests
@@ -59,6 +49,18 @@ test.describe('Network Failure Handling', () => {
5949
let requestCount = 0;
6050
const libraryState = createLibraryState({ trackCount: 10 });
6151

52+
// Mock /api/library/count (pagination support)
53+
await page.route(/\/api\/library\/count(\?.*)?$/, async (route) => {
54+
await route.fulfill({
55+
status: 200,
56+
contentType: 'application/json',
57+
body: JSON.stringify({
58+
total: libraryState.tracks.length,
59+
total_duration: libraryState.tracks.reduce((sum, t) => sum + (t.duration || 0), 0),
60+
}),
61+
});
62+
});
63+
6264
// First request fails, subsequent succeed
6365
await page.route(/\/api\/library(\?.*)?$/, async (route) => {
6466
requestCount++;
@@ -91,7 +93,9 @@ test.describe('Network Failure Handling', () => {
9193
await waitForAlpine(page);
9294

9395
// Wait for tracks to load
94-
await page.waitForSelector('[data-track-id]', { state: 'visible', timeout: 5000 }).catch(() => {});
96+
await page.waitForSelector('[data-track-id]', { state: 'visible', timeout: 5000 }).catch(
97+
() => {},
98+
);
9599

96100
const libraryStore = await getAlpineStore(page, 'library');
97101
expect(libraryStore.tracks.length).toBeGreaterThan(0);
@@ -850,6 +854,18 @@ test.describe('Concurrent Request Handling', () => {
850854
let requestCount = 0;
851855
const libraryState = createLibraryState({ trackCount: 10 });
852856

857+
// Mock /api/library/count (pagination support)
858+
await page.route(/\/api\/library\/count(\?.*)?$/, async (route) => {
859+
await route.fulfill({
860+
status: 200,
861+
contentType: 'application/json',
862+
body: JSON.stringify({
863+
total: libraryState.tracks.length,
864+
total_duration: libraryState.tracks.reduce((sum, t) => sum + (t.duration || 0), 0),
865+
}),
866+
});
867+
});
868+
853869
await page.route(/\/api\/library(\?.*)?$/, async (route) => {
854870
requestCount++;
855871
// Small delay to simulate server processing

app/frontend/tests/library-browser.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ test.describe('Library Browser', () => {
6060
await page.evaluate(() => {
6161
window.Alpine.store('library').tracks = [];
6262
window.Alpine.store('library').filteredTracks = [];
63+
window.Alpine.store('library').totalTracks = 0;
6364
window.Alpine.store('library').loading = false;
6465
});
6566

app/frontend/tests/library-playlist-features.spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ test.describe('Playlist Feature Parity - Library Browser (task-150)', () => {
1818
clearApiCalls(playlistState);
1919
// Setup playlist API mocks before navigation
2020
await setupPlaylistMocks(page, playlistState);
21+
// Mock library count endpoint (pagination support)
22+
await page.route(/\/api\/library\/count(\?.*)?$/, async (route) => {
23+
await route.fulfill({
24+
status: 200,
25+
contentType: 'application/json',
26+
body: JSON.stringify({ total: 2, total_duration: 380 }),
27+
});
28+
});
2129
// Also mock library tracks endpoint
2230
await page.route(/\/api\/library(\?.*)?$/, async (route) => {
2331
await route.fulfill({
@@ -293,6 +301,15 @@ test.describe('Playlist load regression guard (task-179)', () => {
293301

294302
await setupPlaylistMocks(page, playlistState);
295303

304+
// Mock library count endpoint (pagination support)
305+
await page.route(/\/api\/library\/count(\?.*)?$/, async (route) => {
306+
await route.fulfill({
307+
status: 200,
308+
contentType: 'application/json',
309+
body: JSON.stringify({ total: 1, total_duration: 180 }),
310+
});
311+
});
312+
296313
await page.route(/\/api\/library(\?.*)?$/, async (route, request) => {
297314
if (request.method() !== 'GET') {
298315
await route.continue();

app/frontend/tests/sidebar.spec.js

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { test, expect } from '@playwright/test';
1+
import { expect, test } from '@playwright/test';
2+
import { getAlpineStore, waitForAlpine } from './fixtures/helpers.js';
23
import {
3-
waitForAlpine,
4-
getAlpineStore,
5-
} from './fixtures/helpers.js';
6-
import {
7-
createPlaylistState,
8-
setupPlaylistMocks,
94
clearApiCalls,
5+
createPlaylistState,
106
findApiCalls,
7+
setupPlaylistMocks,
118
} from './fixtures/mock-playlists.js';
129

1310
test.describe('Sidebar Navigation', () => {
@@ -104,7 +101,9 @@ test.describe('Sidebar Collapse/Expand', () => {
104101
const initialBox = await sidebar.boundingBox();
105102

106103
// Click collapse button
107-
const collapseButton = page.locator('aside button[title*="Collapse"], aside button[title*="Expand"]').last();
104+
const collapseButton = page.locator(
105+
'aside button[title*="Collapse"], aside button[title*="Expand"]',
106+
).last();
108107
await collapseButton.click();
109108

110109
// Wait for transition
@@ -129,7 +128,9 @@ test.describe('Sidebar Collapse/Expand', () => {
129128
const collapsedBox = await sidebar.boundingBox();
130129

131130
// Click expand button
132-
const expandButton = page.locator('aside button[title*="Expand"], aside button[title*="Collapse"]').last();
131+
const expandButton = page.locator(
132+
'aside button[title*="Expand"], aside button[title*="Collapse"]',
133+
).last();
133134
await expandButton.click();
134135

135136
// Wait for transition
@@ -457,14 +458,36 @@ test.describe('Playlist Feature Parity (task-150)', () => {
457458
test.beforeEach(async ({ page }) => {
458459
clearApiCalls(playlistState);
459460
await setupPlaylistMocks(page, playlistState);
461+
// Mock library count endpoint (pagination support)
462+
await page.route(/\/api\/library\/count(\?.*)?$/, async (route) => {
463+
await route.fulfill({
464+
status: 200,
465+
contentType: 'application/json',
466+
body: JSON.stringify({ total: 2, total_duration: 380 }),
467+
});
468+
});
460469
await page.route(/\/api\/library(\?.*)?$/, async (route) => {
461470
await route.fulfill({
462471
status: 200,
463472
contentType: 'application/json',
464473
body: JSON.stringify({
465474
tracks: [
466-
{ id: 101, title: 'Track A', artist: 'Artist A', album: 'Album A', duration: 180, filepath: '/music/track-a.mp3' },
467-
{ id: 102, title: 'Track B', artist: 'Artist B', album: 'Album B', duration: 200, filepath: '/music/track-b.mp3' },
475+
{
476+
id: 101,
477+
title: 'Track A',
478+
artist: 'Artist A',
479+
album: 'Album A',
480+
duration: 180,
481+
filepath: '/music/track-a.mp3',
482+
},
483+
{
484+
id: 102,
485+
title: 'Track B',
486+
artist: 'Artist B',
487+
album: 'Album B',
488+
duration: 200,
489+
filepath: '/music/track-b.mp3',
490+
},
468491
],
469492
total: 2,
470493
}),
@@ -545,7 +568,9 @@ test.describe('Playlist Feature Parity (task-150)', () => {
545568

546569
test('AC#6: should show drag handle in playlist view', async ({ page }) => {
547570
await page.evaluate(() => {
548-
const libraryBrowser = window.Alpine.$data(document.querySelector('[x-data="libraryBrowser"]'));
571+
const libraryBrowser = window.Alpine.$data(
572+
document.querySelector('[x-data="libraryBrowser"]'),
573+
);
549574
libraryBrowser.currentPlaylistId = 1;
550575
});
551576

@@ -557,7 +582,9 @@ test.describe('Playlist Feature Parity (task-150)', () => {
557582

558583
test('AC#6: should hide drag handle outside playlist view', async ({ page }) => {
559584
await page.evaluate(() => {
560-
const libraryBrowser = window.Alpine.$data(document.querySelector('[x-data="libraryBrowser"]'));
585+
const libraryBrowser = window.Alpine.$data(
586+
document.querySelector('[x-data="libraryBrowser"]'),
587+
);
561588
libraryBrowser.currentPlaylistId = null;
562589
});
563590

@@ -829,7 +856,7 @@ test.describe('Playlist Multi-Select and Batch Delete (task-161)', () => {
829856
await playlist1.click({ modifiers: ['Meta'] });
830857
await playlistList.focus();
831858

832-
page.once('dialog', async dialog => {
859+
page.once('dialog', async (dialog) => {
833860
expect(dialog.type()).toBe('confirm');
834861
expect(dialog.message()).toContain('Delete playlist');
835862
await dialog.dismiss();
@@ -845,7 +872,7 @@ test.describe('Playlist Multi-Select and Batch Delete (task-161)', () => {
845872
await playlist1.click({ modifiers: ['Meta'] });
846873
await playlistList.focus();
847874

848-
page.once('dialog', async dialog => {
875+
page.once('dialog', async (dialog) => {
849876
expect(dialog.type()).toBe('confirm');
850877
await dialog.dismiss();
851878
});
@@ -860,7 +887,7 @@ test.describe('Playlist Multi-Select and Batch Delete (task-161)', () => {
860887
await playlist1.click({ modifiers: ['Meta'] });
861888
await playlistList.focus();
862889

863-
page.once('dialog', async dialog => {
890+
page.once('dialog', async (dialog) => {
864891
await dialog.accept();
865892
});
866893

@@ -878,7 +905,7 @@ test.describe('Playlist Multi-Select and Batch Delete (task-161)', () => {
878905
await playlist1.click({ modifiers: ['Meta'] });
879906
await playlistList.focus();
880907

881-
page.once('dialog', async dialog => {
908+
page.once('dialog', async (dialog) => {
882909
await dialog.dismiss();
883910
});
884911

@@ -942,7 +969,7 @@ test.describe('Playlist Multi-Select and Batch Delete (task-161)', () => {
942969
await playlist2.click({ modifiers: ['Meta'] });
943970
await playlistList.focus();
944971

945-
page.once('dialog', async dialog => {
972+
page.once('dialog', async (dialog) => {
946973
expect(dialog.message()).toContain('Delete selected playlists');
947974
expect(dialog.message()).toContain('Test Playlist 1');
948975
expect(dialog.message()).toContain('Test Playlist 2');

taskfiles/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ tasks:
3434
desc: "Setup CI check environment (Rust only, no cargo-binstall/tauri-cli)"
3535
deps: [setup-system-deps, setup-rust, setup-rust-windows]
3636

37+
setup-test:
38+
desc: "Setup CI test environment (Rust + cargo-nextest)"
39+
deps: [setup-system-deps, setup-rust, setup-rust-windows]
40+
cmds:
41+
- task: _install-cargo-binstall
42+
- task: _install-cargo-nextest
43+
3744
setup-rust:
3845
desc: "Install rustup and nightly toolchain (macOS/Linux)"
3946
platforms: [darwin, linux]
@@ -79,6 +86,13 @@ tasks:
7986
status:
8087
- test -x "{{.CARGO_BIN}}/cargo-binstall"
8188

89+
_install-cargo-nextest:
90+
internal: true
91+
cmds:
92+
- cargo binstall cargo-nextest --locked --no-confirm
93+
status:
94+
- command -v cargo-nextest
95+
8296
_install-tauri-cli:
8397
internal: true
8498
cmds:

0 commit comments

Comments
 (0)